# 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 base64
import collections
import copy
import fnmatch
import mimetypes
import os
import re
import socket
import sys
import tempfile
import time
import random
import datetime
import itertools
import hashlib
import json

try:
    from PIL import Image
except ImportError:
    Image = None

from .storage import atomic_write

try:
    from feedparser import _sanitizeHTML
except ImportError:
    _sanitizeHTML = None

try:
    import DNS
    DNS.ParseResolvConf()
except ImportError:
    DNS = None

try:
    import magic
except ImportError:
    magic = None

import quixote
import quixote.form.widget

from quixote import get_publisher, get_request, get_response, get_session
from quixote.http_request import Upload
from quixote.form import *
from quixote.html import htmltext, htmltag, htmlescape, TemplateIO
from quixote.util import randbytes

from django.utils.encoding import force_bytes, force_text
from django.utils import six
from django.utils.six.moves.html_parser import HTMLParser
from django.utils.six import StringIO

from django.conf import settings
from django.utils.safestring import mark_safe

from .template import render as render_template, Template, TemplateError
from ..portfolio import has_portfolio
from wcs.conditions import Condition, ValidationError

from . import _, ngettext, force_str
from . import misc
from .humantime import humanduration2seconds, seconds2humanduration, timewords
from .misc import strftime, C_, HAS_PDFTOPPM, json_loads
from .publisher import get_cfg
from .template_utils import render_block_to_string

QuixoteForm = Form

Widget.REQUIRED_ERROR = N_('required field')
get_error_orig = Widget.get_error

def get_i18n_error(self, request=None):
    error = get_error_orig(self, request)
    if error == Widget.REQUIRED_ERROR:
        return _(error)
    return error

def is_prefilled(self):
    if hasattr(self, 'prefilled'):
        return self.prefilled
    else:
        return False

def render_title(self, title):
    if title:
        if self.required:
            title += htmltext('<span title="%s" class="required">*</span>') % _(
                    'This field is required.')
        return htmltext('<div class="title"><label for="form_%s">%s</label></div>') % (
                        self.name, title)
    else:
        return ''

def get_template_names(widget):
    template_names = []
    widget_template_name = getattr(widget, 'template_name', None)
    for extra_css_class in (getattr(widget, 'extra_css_class', '') or '').split():
        if not extra_css_class.startswith('template-'):
            continue
        template_name = extra_css_class.split('-', 1)[1]
        # full template
        template_names.append('qommon/forms/widgets/%s.html' % template_name)
        if widget_template_name:
            # widget specific variation
            template_names.append(widget_template_name.replace(
                '.html', '--%s.html' % template_name))
    if widget_template_name:
        template_names.append(widget_template_name)
    template_names.append('qommon/forms/widget.html')
    return template_names

def render(self):
    # quixote/form/widget.py, Widget::render
    def safe(text):
        return mark_safe(str(htmlescape(text)))
    if hasattr(self, 'add_media'):
        self.add_media()
    self.class_name = self.__class__.__name__
    self.rendered_title = lambda: safe(self.render_title(self.get_title()))
    self.rendered_error = lambda: safe(self.render_error(self.get_error()))
    self.rendered_hint = lambda: safe(self.render_hint(self.get_hint()))
    if get_publisher():
        context = get_publisher().substitutions.get_context_variables()
    else:
        context = {}
    context['widget'] = self
    template_names = get_template_names(self)
    return htmltext(render_template(template_names, context))

Widget.get_error = get_i18n_error
Widget.render = render
Widget.cleanup = None
Widget.render_title = render_title
Widget.is_prefilled = is_prefilled

def string_render_content(self):
    attrs = {'id': 'form_' + self.name}
    if self.required:
        attrs['aria-required'] = 'true'
    if getattr(self, 'prefill_attributes', None) and 'autocomplete' in self.prefill_attributes:
        attrs['autocomplete'] = self.prefill_attributes['autocomplete']
    if self.attrs:
        attrs.update(self.attrs)
    return htmltag("input", xml_end=True, type=self.HTML_TYPE, name=self.name,
                   value=self.value, **attrs)
StringWidget.render_content = string_render_content


def file_render_content(self):
    attrs = {'id': 'form_' + str(self.name).split('$')[0]}
    if self.required:
        attrs['aria-required'] = 'true'
    if self.attrs:
        attrs.update(self.attrs)
    return htmltag("input", xml_end=True, type=self.HTML_TYPE, name=self.name,
                   value=self.value, **attrs)
FileWidget.render_content = file_render_content


def text_render_content(self):
    attrs = {'id': 'form_' + self.name}
    if self.required:
        attrs['aria-required'] = 'true'
    if self.attrs:
        attrs.update(self.attrs)
    if not attrs.get('cols'):
        attrs['cols'] = 72
    if not attrs.get('rows'):
        attrs['rows'] = 5
    if attrs.get('readonly') and not self.value:
        attrs['rows'] = 1
    return (htmltag("textarea", name=self.name, **attrs) +
        htmlescape(self.value or "") + htmltext("</textarea>"))
TextWidget.render_content = text_render_content


class SubmitWidget(quixote.form.widget.SubmitWidget):
    def render_content(self):
        if self.name in ('cancel', 'previous', 'save-draft'):
            self.attrs['formnovalidate'] = 'formnovalidate'
        value = (self.label and htmlescape(self.label) or None)
        return htmltag('button', name=self.name, value=value, **self.attrs) + \
                self.label + htmltext('</button>')


class RadiobuttonsWidget(quixote.form.RadiobuttonsWidget):
    template_name = 'qommon/forms/widgets/radiobuttons.html'

    def __init__(self, name, value=None, **kwargs):
        self.options_with_attributes = kwargs.pop('options_with_attributes', None)
        super(RadiobuttonsWidget, self).__init__(name, value=value, **kwargs)

    def get_options(self):
        options = self.options_with_attributes or self.options
        for i, option in enumerate(options):
            object, description, key = option[:3]
            yield {
                'value': key,
                'label': description,
                'disabled': bool(self.options_with_attributes and option[-1].get('disabled')),
                'selected': self.is_selected(object)
            }

def checkbox_render_content(self, standalone=True):
    attrs = {'id': 'form_' + self.name}
    if self.required:
        attrs['aria-required'] = 'true'
    if self.attrs:
        attrs.update(self.attrs)
    checkbox = htmltag("input", xml_end=True, type="checkbox", name=self.name,
                       value="yes", checked=self.value and "checked" or None,
                       **attrs)
    if standalone:
        return htmltext('<label>%s<span></span></label>' % checkbox)  # for custom style
    return checkbox
CheckboxWidget.render_content = checkbox_render_content


def select_render_content(self):
    attrs = {'id': 'form_' + self.name}
    if self.required:
        attrs['aria-required'] = 'true'
    if self.attrs:
        attrs.update(self.attrs)
    tags = [htmltag("select", name=self.name, **attrs)]
    for object, description, key in self.options:
        if self.is_selected(object):
            selected = 'selected'
        else:
            selected = None
        if description is None:
            description = ""
        r = htmltag("option", value=key, selected=selected)
        tags.append(r + htmlescape(description) + htmltext('</option>'))
    tags.append(htmltext("</select>"))
    return htmltext("\n").join(tags)
SelectWidget.render_content = select_render_content


def get_selection_error_text(*args):
    return _('invalid value selected')

SelectWidget.SELECTION_ERROR = property(get_selection_error_text)


class Form(QuixoteForm):
    TOKEN_NOTICE = N_(
            "The form you have submitted is invalid.  Most "
            "likely it has been successfully submitted once "
            "already.  Please review the form data "
            "and submit the form again.")
    ERROR_NOTICE = N_("There were errors processing your form.  See below for details.")

    info = None
    captcha = None
    advanced_label = '+'

    def __init__(self, *args, **kwargs):
        if kwargs.get('advanced_label'):
            self.advanced_label = kwargs.pop('advanced_label')
        QuixoteForm.__init__(self, *args, **kwargs)
        self.global_error_messages = None

    def add_captcha(self, hint=None):
        if not self.captcha and not (get_session().won_captcha or get_session().user):
            self.captcha = CaptchaWidget('captcha', hint=hint)

    def add_submit(self, name, value=None, **kwargs):
        return self.add(SubmitWidget, name, value, **kwargs)

    def add(self, widget_class, name, *args, **kwargs):
        if kwargs and 'render_br' not in kwargs:
            kwargs['render_br'] = False
        advanced = False
        if kwargs and kwargs.get('advanced', False):
            advanced = True
            del kwargs['advanced']
        QuixoteForm.add(self, widget_class, name, *args, **kwargs)
        widget = self._names[name]
        widget.advanced = advanced
        return widget

    def remove(self, name):
        widget = self._names.get(name)
        if widget:
            del self._names[name]
            self.widgets.remove(widget)

    def get_all_widgets(self):
        l = QuixoteForm.get_all_widgets(self)
        if self.captcha:
            l.append(self.captcha)
        return l

    def add_global_errors(self, error_messages):
        self.global_error_messages = error_messages

    def _get_default_action(self):
        if get_request().get_header('x-popup') == 'true':
            # do not leave action empty for popups, as they get embedded into
            # another URI
            return get_request().get_path()
        return QuixoteForm._get_default_action(self)

    def render_button(self, button):
        r = TemplateIO(html=True)
        classnames = '%s widget %s-button %s' % (
                button.__class__.__name__, button.name,
                getattr(button, 'extra_css_class', ''))
        r += htmltext('<div class="%s">') % classnames
        r += htmltext('<div class="content">')
        r += button.render_content()
        r += htmltext('</div>')
        r += htmltext('</div>')
        r += htmltext('\n')
        return r.getvalue()

    def _render_submit_widgets(self):
        r = TemplateIO(html=True)
        if self.submit_widgets:
            r += htmltext('<div class="buttons submit">')
            for widget in self.submit_widgets:
                if widget.name == 'prefill':
                    continue
                r += self.render_button(widget)
            r += htmltext('</div>')
        return r.getvalue()

    def _render_info_notice(self):
        return htmltext('<div class="infonotice">%s</div>' % _(self.info))

    def _render_error_notice(self):
        errors = []
        classnames = ['errornotice']
        if self.has_errors():
            errors.append(_(QuixoteForm._render_error_notice(self)))
        if self.global_error_messages:
            errors.extend(self.global_error_messages)
            classnames.append('global-errors')
        t = TemplateIO(html=True)
        t += htmltext('<div class="%s">' % ' '.join(classnames))
        for error in errors:
            t += htmltext('<p>%s</p>') % error
        t += htmltext('</div>')
        return t.getvalue()

    def _render_body(self):
        r = TemplateIO(html=True)
        if self.has_errors() or self.global_error_messages:
            r += self._render_error_notice()
        if self.info:
            r += self._render_info_notice()
        if 'prefill' in self._names:
            r += self._render_prefill_widgets()

        r += self._render_widgets()
        if self.captcha:
            r += self.captcha.render()
        r += self._render_submit_widgets()
        return r.getvalue()

    def _render_prefill_widgets(self):
        r = TemplateIO(html=True)
        if self.submit_widgets:
            r += htmltext('<div class="prefill buttons">')
            for widget in self.submit_widgets:
                if widget.name == 'prefill':
                    r += widget.render()
            r += htmltext('</div>')
        return r.getvalue()

    def _render_widgets(self):
        r = TemplateIO(html=True)
        advanced_widgets = []
        for widget in self.widgets:
            if not hasattr(widget, 'advanced') or not widget.advanced:
                r += widget.render()
            else:
                advanced_widgets.append(widget)
        if advanced_widgets:
            get_response().add_javascript(['jquery.js', 'qommon.forms.js'])
            r += htmltext('<fieldset class="form-plus">')
            r += htmltext('<legend>%s</legend>') % self.advanced_label
            for widget in advanced_widgets:
                r += widget.render()
            r += htmltext('</fieldset>')
        return r.getvalue()

    def add_media(self):
        for widget in self.get_all_widgets():
            if hasattr(widget, 'add_media'):
                widget.add_media()


class HtmlWidget(object):
    error = None
    name = None

    def __init__(self, string, title = None, *args, **kwargs):
        self.string = string
        self.title = title

    def render(self):
        return self.render_content()

    def render_content(self):
        content = self.title or self.string or ''
        if getattr(self, 'is_hidden', False):
            content = htmltext(str(content).replace('>', ' style="display: none">', 1))
        return htmltext(content)

    def has_error(self, request):
        return False

    def parse(self, *args):
        pass

    def clear_error(self, request=None):
        pass


class CommentWidget(Widget):
    template_name = 'qommon/forms/widgets/comment.html'

    def __init__(self, content, extra_css_class):
        super(CommentWidget, self).__init__(name='')
        self.content = content
        self.extra_css_class = extra_css_class

    def has_error(self, request):
        return False

    def parse(self, *args, **kwargs):
        pass

    def clear_error(self, request=None):
        pass


class CompositeWidget(quixote.form.CompositeWidget):
    def render_as_thead(self):
        r = TemplateIO(html=True)
        r += htmltext('<tr>\n')
        for widget in self.get_widgets():
            r += htmltext('<th>')
            r += widget.get_title()
            r += htmltext('</th>')
        r += htmltext('</tr>\n')
        return r.getvalue()

    def render_content_as_tr(self):
        r = TemplateIO(html=True)
        r += htmltext('<tr>\n')
        for widget in self.get_widgets():
            extra_attributes = ''
            classnames = '%s widget' % widget.__class__.__name__
            if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
                classnames += ' ' + widget.extra_css_class
            if hasattr(widget, 'content_extra_attributes'):
                extra_attributes = ' '.join(['%s=%s' % x for x in widget.content_extra_attributes.items()])
            r += htmltext('<td><div class="%s"><div class="content" %s>' % (
                    classnames, extra_attributes))
            r += widget.render_content()
            r += htmltext('</div></div></td>')
        r += htmltext('</tr>\n')
        return r.getvalue()


class StringWidget(quixote.form.StringWidget):
    def __init__(self, name, *args, **kwargs):
        self.validation_function = kwargs.pop('validation_function', None)
        super(StringWidget, self).__init__(name, *args, **kwargs)

    def _parse(self, request):
        quixote.form.StringWidget._parse(self, request)
        if self.value and self.validation_function:
            try:
                self.validation_function(self.value)
            except ValueError as e:
                self.error = str(e)


class DurationWidget(StringWidget):
    def __init__(self, name, value=None, **kwargs):
        if value:
            value = seconds2humanduration(int(value))
        if 'hint' in kwargs:
            kwargs['hint'] += htmltext('<br>')
        else:
            kwargs['hint'] = ''
        kwargs['hint'] += htmltext(
                _('Usable units of time: %s.')) % ', '.join(timewords())
        super(DurationWidget, self).__init__(name, value=value, **kwargs)

    def parse(self, request=None):
        value = super(DurationWidget, self).parse(request)
        return str(humanduration2seconds(self.value)) if value else None


class TextWidget(quixote.form.TextWidget):
    def __init__(self, name, *args, **kwargs):
        self.validation_function = kwargs.pop('validation_function', None)
        super(TextWidget, self).__init__(name, *args, **kwargs)

    def _parse(self, request):
        quixote.form.TextWidget._parse(self, request)
        if self.value is not None:
            try:
                maxlength = int(self.attrs.get('maxlength', 0))
            except (TypeError, ValueError):
                maxlength = 0
            if maxlength:
                uvalue = self.value if six.PY3 else self.value.decode(get_publisher().site_charset)
                if len(uvalue) > maxlength:
                    self.error = _('too many characters (limit is %d)') % maxlength
            if self.validation_function:
                try:
                    self.validation_function(self.value)
                except ValueError as e:
                    self.error = str(e)

class CheckboxWidget(quixote.form.CheckboxWidget):
    '''
    Widget just like CheckboxWidget but with an effective support for the
    required attribute, if required the checkbox will have to be checked.
    '''
    template_name = 'qommon/forms/widgets/checkbox.html'

    def _parse(self, request):
        self.value = self.name in request.form and not request.form[self.name] in (False, '', 'False')
        if self.required and not self.value:
            self.set_error(self.REQUIRED_ERROR)

    def set_value(self, value):
        if value in (None, False, '', 'False'):
            self.value = False
        else:
            self.value = True

class UploadedFile: #pylint: disable=C1001
    def __init__(self, directory, filename, upload):
        self.directory = directory
        self.base_filename = upload.base_filename
        self.content_type = upload.content_type

        # Find a good filename
        if filename:
            self.filename = filename
        elif self.base_filename:
            self.filename = self.base_filename
        else:
            t = datetime.datetime.now().isoformat()
            fd, path = tempfile.mkstemp(prefix=t, suffix='.upload',
                    dir=self.dir_path())
            os.close(fd)
            self.filename = os.path.basename(filename)

        if upload.fp:
            self.set_content(upload.fp.read())

    def set_content(self, content):
        self.size = len(content)
        file_path = self.build_file_path()
        if not os.path.exists(self.dir_path()):
            os.mkdir(self.dir_path())
        open(file_path, 'wb').write(content)

    def dir_path(self):
        return os.path.join(get_publisher().app_dir, self.directory)

    def build_file_path(self):
        return os.path.join(get_publisher().app_dir, self.directory,
                self.filename)

    def get_file(self):
        return open(self.build_file_path(), 'rb')

    def get_content(self):
        return self.get_file().read()

    def build_response(self):
        response = get_response()
        response.content_type = self.content_type
        response.set_header('content-disposition',
                'attachment; filename="%s"' % self.base_filename)
        return self.get_file().read()


class UploadValidationError(Exception):
    pass


class UploadWidget(CompositeWidget):
    def __init__(self, name, value=None, directory=None, filename=None,
            validation=None, **kwargs):
        CompositeWidget.__init__(self, name, **kwargs)
        del kwargs['title']
        kwargs.pop('hint')
        self.value = value
        self.directory = directory or 'uploads'
        self.filename = filename
        self.validation = validation
        if value:
            self.add(CheckboxWidget, 'orexisting',
                            title = _('Use previous file.'), value=True)
            self.widgets[-1].render_br = False
            self.add(FileWidget, 'file', title = _('Or upload a new one'), **kwargs)
        else:
            self.add(FileWidget, 'file', **kwargs)

    def _parse(self, request):
        if self.value and self.get('orexisting'):
            pass
        elif self.get('file'):
            upload = self.get('file')
            self.value = UploadedFile(self.directory, self.filename, upload)
            if self.validation:
                try:
                    self.validation(upload)
                except UploadValidationError as e:
                    self.error = str(e)
        else:
            self.value = None


class FileWithPreviewWidget(CompositeWidget):
    """Widget that proposes a File Upload widget but that stores the file
    ondisk so it has a "readonly" mode where the filename is shown."""

    template_name = 'qommon/forms/widgets/file.html'

    extra_css_class = 'file-upload-widget'
    max_file_size = None
    file_type = None

    max_file_size_bytes = None # will be filled automatically

    def __init__(self, name, value=None, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)
        self.value = value
        self.readonly = kwargs.get('readonly')
        self.max_file_size = kwargs.pop('max_file_size', None)
        self.automatic_image_resize = kwargs.pop('automatic_image_resize', False)
        self.allow_portfolio_picking = has_portfolio() and kwargs.pop('allow_portfolio_picking', True)
        if self.max_file_size:
            self.max_file_size_bytes = FileSizeWidget.parse_file_size(self.max_file_size)
        self.add(HiddenWidget, 'token')
        if not self.readonly:
            attrs = {'data-url': get_publisher().get_root_url() + 'tmp-upload'}
            self.file_type = kwargs.pop('file_type', None)
            if self.file_type:
                attrs['accept'] = ','.join(self.file_type)
            if self.max_file_size_bytes:
                # this could be used for client size validation of file size
                attrs['data-max-file-size'] = str(self.max_file_size_bytes)
            self.add(FileWidget, 'file', render_br=False, attrs=attrs)
        if value:
            self.set_value(value)

    def is_image(self):
        if getattr(self, 'file_type', None):
            return all([x.startswith('image/') for x in getattr(self, 'file_type', None) or []])
        return False

    def set_value(self, value):
        self.value = value
        if hasattr(self.value, 'token'):
            self.get_widget('token').set_value(self.value.token)
            if not get_session().get_tempfile(self.value.token):
                # oops, it has a token but it's not in the session; this is
                # probably because it was restored from a draft file created
                # from an expired session.
                self.value.token = get_session().add_tempfile(self.value).get('token')
                self.get_widget('token').set_value(self.value.token)

    def add_media(self):
        get_response().add_javascript(['jquery.js', 'jquery-ui.js',
                        'jquery.iframe-transport.js', 'jquery.fileupload.js',
                        'qommon.fileupload.js'])
        if not self.readonly and get_request().user and self.allow_portfolio_picking:
            get_response().add_javascript(['fargo.js'])

    def tempfile(self):
        return get_session().get_tempfile(self.get('token')) or {}

    def has_tempfile_image(self):
        temp = self.tempfile()
        if not temp:
            return False

        filetype = (mimetypes.guess_type(temp.get('orig_filename', '')) or [''])[0]
        if not filetype:
            return False

        if filetype == 'application/pdf':
            return HAS_PDFTOPPM

        if not filetype.startswith('image/'):
            return False

        if Image:
            image_content = get_session().get_tempfile_content(self.get('token'))
            try:
                image = Image.open(image_content.fp)
            except Exception:
                return False

        return True

    def _parse(self, request):
        self.value = None
        if self.get('token'):
            token = self.get('token')
        elif self.get('file'):
            token = get_session().add_tempfile(self.get('file'))['token']
            request.form[self.get_widget('token').get_name()] = token
        else:
            token = None

        session = get_session()
        if token:
            self.value = session.get_tempfile_content(token)

        if self.value is None:
            # there's no file, the other checks are irrelevant.
            return

        # Don't trust the browser supplied MIME type, update the Upload object
        # with a MIME type created with magic (or based on the extension if the
        # module is missing).
        #
        # This also helps people uploading PDF files that were downloaded from
        # sites setting a wrong MIME type (like application/force-download) for
        # various reasons.
        if magic:
            if hasattr(magic, 'MagicException'):
                mime = magic.Magic(mime=True)
                filetype = mime.from_file(self.value.fp.name)
            else:  # bindings from libmagic package, obsolete after jessie
                magic_object = magic.open(magic.MIME)
                magic_object.load()
                filetype = magic_object.file(self.value.fp.name).split(';')[0]
                magic_object.close()
        else:
            filetype, encoding = mimetypes.guess_type(self.value.base_filename)

        if not filetype:
            filetype = 'application/octet-stream'

        self.value.content_type = filetype

        if self.max_file_size:
            # validate file size
            file_size = os.path.getsize(self.value.fp.name)
            if file_size > self.max_file_size_bytes:
                self.error = _('over file size limit (%s)') % self.max_file_size

        if self.file_type:
            # validate file type
            accepted_file_types = []
            for file_type in self.file_type:
                accepted_file_types.extend(file_type.split(','))

            valid_file_type = False
            for accepted_file_type in accepted_file_types:
                # fnmatch is used to handle generic mimetypes, like
                # image/*
                if fnmatch.fnmatch(self.value.content_type, accepted_file_type):
                    valid_file_type = True
                    break
            if not valid_file_type:
                self.error = _('invalid file type')


class PicklableUpload(Upload):
    def __getstate__(self):
        odict = self.__dict__.copy()
        if 'fp' in odict:
            del odict['fp']

        basedir = os.path.join(get_publisher().app_dir, 'uploads')
        if not os.path.exists(basedir):
            os.mkdir(basedir)
        if 'qfilename' in odict:
            filepath = os.path.join(basedir, self.qfilename)
        else:
            self.qfilename = misc.file_digest(self.fp)
            filepath = os.path.join(basedir, self.qfilename)

        if 'fp' in self.__dict__ and not self.fp.closed:
            self.fp.seek(0)
            atomic_write(filepath, self.fp, async_op=False)

        odict['qfilename'] = self.qfilename
        return odict

    def get_file_pointer(self):
        if 'fp' in self.__dict__ and self.__dict__.get('fp') is not None:
            return self.__dict__.get('fp')
        elif hasattr(self, 'qfilename'):
            basedir = os.path.join(get_publisher().app_dir, 'uploads')
            self.fp = open(os.path.join(basedir, self.qfilename), 'rb')
            return self.fp
        return None

    def __setstate__(self, dict):
        self.__dict__.update(dict)
        if hasattr(self, 'data'):
            # backward compatibility with older w.c.s. version
            self.fp = StringIO(self.data)
            del self.data

    def get_file(self):
        # quack like UploadedFile
        return self.get_file_pointer()

    def get_filename(self):
        if not hasattr(self, 'qfilename'):
            raise AttributeError('filename')
        basedir = os.path.join(get_publisher().app_dir, 'uploads')
        return os.path.join(basedir, self.qfilename)

    def get_content(self):
        if hasattr(self, 'qfilename'):
            filename = os.path.join(get_publisher().app_dir, 'uploads', self.qfilename)
            return open(filename, 'rb').read()
        return None


class EmailWidget(StringWidget):
    HTML_TYPE = 'email'

    def __init__(self, *args, **kwargs):
        StringWidget.__init__(self, *args, **kwargs)
        if not 'size' in kwargs:
            self.attrs['size'] = '35'

    def _parse(self, request):
        StringWidget._parse(self, request)
        if self.value is not None:
            # basic tests first
            if not '@' in self.value[1:-1]:
                self.error = _('must be a valid email address')
            elif self.value[0] != '"' and ' ' in self.value:
                self.error = _('must be a valid email address')
            elif self.value[0] != '"' and self.value.count('@') != 1:
                self.error = _('must be a valid email address')
            elif get_cfg('emails', {}).get('check_domain_with_dns', True):
                # testing for domain existence
                domain = self.value.split('@')[-1]
                if [x for x in domain.split('.') if not x]:
                    # empty parts in domain, ex: @example..net, or
                    # @.example.net
                    self.error = _('invalid address domain')
                    return
                domain = force_text(domain, 'utf-8', errors='ignore')
                try:
                    domain = domain.encode('idna')
                except UnicodeError:
                    self.error = _('invalid address domain')
                    return
                # simply lookup host name; note it will fail if hostname
                # doesn't have an A entry
                try:
                    socket.gethostbyname(domain)
                except socket.error:
                    # will fail on lack of DNS module or MX lookup failure
                    try:
                        l = len(DNS.mxlookup(domain))
                    except:
                        l = 0
                    if l == 0:
                        self.error = _('invalid address domain')
                else:
                    # and then, localpart could be tested
                    pass


class ValidationCondition(Condition):
    def __init__(self, django_condition, value):
        super(ValidationCondition, self).__init__({'type': 'django', 'value': django_condition})
        self.evaluated_value = value

    def get_data(self):
        return {'value': self.evaluated_value}


class ValidationWidget(CompositeWidget):
    validation_methods = collections.OrderedDict([
        ('digits', {'title': N_('Digits'), 'regex': '\d+'}),
        ('phone-fr', {'title': N_('Phone Number (France)'), 'regex': '0[\d\.\s]{9}'}),
        ('zipcode-fr', {'title': N_('Zip Code (France)'), 'regex': '\d{5}'}),
        ('siren-fr', {'title': N_('SIREN Code (France)'), 'function': 'validate_siren'}),
        ('siret-fr', {'title': N_('SIRET Code (France)'), 'function': 'validate_siret'}),
        ('nir-fr', {'title': N_('NIR (France)'), 'function': 'validate_nir'}),
        ('iban', {'title': N_('IBAN'), 'function': 'validate_iban'}),
        ('regex', {'title': N_('Regular Expression')}),
        ('django', {'title': N_('Django Condition')}),
    ])

    def __init__(self, name, value=None, **kwargs):
        super(ValidationWidget, self).__init__(name, value=value, **kwargs)
        if not value:
            value = {}

        options = [(None, _('None'))] + [(x, _(y['title'])) for x, y in self.validation_methods.items()]

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

        validation_labels = collections.OrderedDict(options)
        self.add(RegexStringWidget, 'value_regex', size=80,
                 value=value.get('value') if value.get('type') == 'regex' else None,
                 attrs={'data-dynamic-display-child-of': 'validation$type',
                        'data-dynamic-display-value': validation_labels.get('regex')})
        self.add(DjangoConditionWidget, 'value_django', size=80,
                 value=value.get('value') if value.get('type') == 'django' else None,
                 attrs={'data-dynamic-display-child-of': 'validation$type',
                        'data-dynamic-display-value': validation_labels.get('django')})
        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

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

    @classmethod
    def get_validation_function(cls, validation):
        pattern = cls.get_validation_pattern(validation)
        if pattern:
            def regex_validation(value):
                match = re.match(pattern, value)
                return bool(match and match.group() == value)
            return regex_validation
        if validation['type'] == 'django':
            def django_validation(value):
                condition = ValidationCondition(validation['value'], value=value)
                return condition.evaluate()
            return django_validation
        validation_method = cls.validation_methods.get(validation['type'])
        if validation_method and 'function' in validation_method:
            return getattr(misc, validation_method['function'])

    @classmethod
    def get_validation_pattern(cls, validation):
        validation_method = cls.validation_methods.get(validation['type'])
        if validation_method and validation_method.get('regex'):
            return validation_method.get('regex')
        if validation['type'] == 'regex':
            return validation.get('value')
        return None


class WcsExtraStringWidget(StringWidget):
    field = None
    prefill = False
    prefill_attributes = None

    def add_media(self):
        if self.prefill_attributes and 'geolocation' in self.prefill_attributes:
            get_response().add_javascript(['qommon.geolocation.js'])

    def _parse(self, request):
        StringWidget._parse(self, request)
        if self.field and self.field.validation and self.value is not None:
            validation_function = ValidationWidget.get_validation_function(self.field.validation)
            if validation_function and not validation_function(self.value):
                self.error = _('invalid value')


class DateWidget(StringWidget):
    '''StringWidget which checks the value entered is a correct date'''
    template_name = 'qommon/forms/widgets/date.html'

    minimum_date = None
    maximum_date = None
    content_extra_css_class = 'date'

    def __init__(self, name, value=None, **kwargs):
        minimum_date = kwargs.pop('minimum_date', None)
        if minimum_date:
            self.minimum_date = misc.get_as_datetime(minimum_date).timetuple()[:3]
        maximum_date = kwargs.pop('maximum_date', None)
        if maximum_date:
            self.maximum_date = misc.get_as_datetime(maximum_date).timetuple()[:3]
        if kwargs.pop('minimum_is_future', False):
            if kwargs.get('date_can_be_today'):
                self.minimum_date = time.localtime()
            else:
                self.minimum_date = (datetime.datetime.today() + datetime.timedelta(1)).timetuple()
        if kwargs.pop('date_in_the_past', False):
            if kwargs.get('date_can_be_today'):
                self.maximum_date = time.localtime()
            else:
                self.maximum_date = (datetime.datetime.today() - datetime.timedelta(1)).timetuple()

        if 'date_can_be_today' in kwargs:
            del kwargs['date_can_be_today']

        if isinstance(value, datetime.datetime) or isinstance(value, datetime.date):
            value = value.strftime(misc.date_format())

        StringWidget.__init__(self, name, value=value, **kwargs)
        self.attrs['size'] = '12'
        self.attrs['maxlength'] = '10'

    def parse(self, request=None):
        StringWidget.parse(self, request=request)
        if six.PY2 and type(self.value) is unicode:
            self.value = self.value.encode(get_publisher().site_charset)
        return self.value

    @classmethod
    def get_format_string(cls):
        return misc.date_format()

    def _parse(self, request):
        StringWidget._parse(self, request)
        if self.value is not None:
            try:
                value = misc.get_as_datetime(self.value).timetuple()
                self.value = strftime(self.get_format_string(), value)
            except ValueError:
                self.error = _('invalid date')
                self.value = None
                return
            if value[0] < 1800 or value[0] > 2099:
                self.error = _('invalid date')
                self.value = None
            elif self.minimum_date and value[:3] < self.minimum_date[:3]:
                self.error = _('invalid date: date must be on or after %s') % strftime(
                        misc.date_format(), datetime.datetime(*self.minimum_date[:6]))
            elif self.maximum_date and value[:3] > self.maximum_date[:3]:
                format_string = misc.date_format()
                self.error = _('invalid date; date must be on or before %s') % strftime(
                        misc.date_format(), datetime.datetime(*self.maximum_date[:6]))

    def add_media(self):
        self.prepare_javascript()

    @classmethod
    def prepare_javascript(cls):
        get_response().add_javascript([
            'jquery.js', 'bootstrap-datetimepicker.js', 'qommon.forms.js'])
        current_language = get_request().language
        if current_language != 'en' and current_language in get_publisher().supported_languages or []:
            get_response().add_javascript(['bootstrap-datetimepicker.%s.js' % current_language])

        get_response().add_css_include('datetimepicker.css')

    def date_format(self):
        return self.get_format_string().replace('%Y', 'yyyy').replace(
                '%m', 'mm').replace('%d', 'dd').replace('%H', 'hh').replace(
                '%M',  'ii').replace('%S', 'ss')

    def start_date(self):
        return self.date_format().replace(
                    'yyyy', '%04d' % self.minimum_date[0]).replace(
                    'mm', '%02d' % self.minimum_date[1]).replace(
                    'dd', '%02d' % self.minimum_date[2]).replace(
                    'hh', '00').replace('ii', '00').replace('ss', '00')

    def end_date(self):
        return self.date_format().replace(
                    'yyyy', '%04d' % self.maximum_date[0]).replace(
                    'mm', '%02d' % self.maximum_date[1]).replace(
                    'dd', '%02d' % self.maximum_date[2]).replace(
                    'hh', '00').replace('ii', '00').replace('ss', '00')


class DateTimeWidget(DateWidget):
    '''StringWidget which checks the value entered is a correct date/time'''
    content_extra_css_class = 'date'

    def __init__(self, name, value=None, **kwargs):
        DateWidget.__init__(self, name, value=value, **kwargs)
        self.attrs['size'] = '16'
        self.attrs['maxlength'] = '16'

    def get_format_string(self):
        return misc.datetime_format()


class RegexStringWidget(StringWidget):
    '''StringWidget which checks the value entered is a correct regex'''
    def _parse(self, request):
        StringWidget._parse(self, request)
        if self.value is not None:
            try:
                re.compile(self.value)
            except:
                self.error = _('invalid regular expression')
                self.value = None


class CheckboxesWidget(CompositeWidget):
    readonly = False
    template_name = 'qommon/forms/widgets/checkboxes.html'

    def __init__(self, name, value=None, options=None, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)
        self.element_names = collections.OrderedDict()

        if 'title' in kwargs:
            del kwargs['title']
        if 'readonly' in kwargs:
            del kwargs['readonly']
            self.readonly = True
        self.inline = kwargs.get('inline', True)
        self.max_choices = int(kwargs.get('max_choices', 0) or 0)

        self.options_with_attributes = kwargs.pop('options_with_attributes', None)
        self.disabled_options = []
        if self.options_with_attributes:
            options = self.options_with_attributes

        for i, option in enumerate(options):
            if len(option) == 2:
                key, title = option[:2]
                name = 'element%d' % i
            else:
                _, title, key  = option[:3]
                name = 'element%s' % str(key)

            key = str(key)

            element_kwargs = kwargs.copy()
            if self.options_with_attributes and option[-1].get('disabled'):
                element_kwargs['disabled'] = 'disabled'
                self.disabled_options.append(name)

            if value and key in value:
                checked = True
            else:
                checked = False
            self.add(CheckboxWidget, name, title=title, value=checked, **element_kwargs)
            self.element_names[name] = key

    def _parse(self, request):
        if self.readonly:
            return
        values = []
        for name in self.element_names:
            if name in self.disabled_options:
                continue
            value = self.get(name)
            if value is True:
                values.append(self.element_names[name])
        self.value = values
        if self.required and not self.value:
            self.set_error(self.REQUIRED_ERROR)
        if self.value and self.max_choices and len(self.value) > self.max_choices:
            self.set_error(_('You must select at most %d answers.') % self.max_choices)

    def set_value(self, value):
        self.value = value
        for name in self.element_names:
            widget = self.get_widget(name)
            if self.element_names[name] in self.value:
                widget.set_value(True)
            else:
                widget.set_value(False)
        self._parsed = True

    def has_error(self, request=None):
        return Widget.has_error(self, request=request)

    def render_content(self):
        r = TemplateIO(html=True)
        if self.inline:
            r += htmltext('<ul class="inline">')
        else:
            r += htmltext('<ul>')
        for widget in self.get_widgets():
            if widget.attrs and 'disabled' in widget.attrs:
                r += htmltext('<li class="disabled"><label>')
            else:
                r += htmltext('<li><label>')
            if self.readonly:
                widget.attrs['disabled'] = 'disabled'
                if widget.value:
                    r += htmltext('<input type="hidden" name="%s" value="yes" >') % widget.name
                widget.name = widget.name + 'xx'
            r += widget.render_content(standalone=False)
            r += htmltext('<span>%s</span>') % widget.title
            r += htmltext('</label>')
            r += htmltext('</li>')
        r += htmltext('</ul>')
        return r.getvalue()

class ValidatedStringWidget(StringWidget):
    '''StringWidget which checks the value entered is correct according to a regex'''
    regex = None

    def __init__(self, *args, **kwargs):
        if 'regex' in kwargs:
            self.regex = kwargs.pop('regex')
        super(ValidatedStringWidget, self).__init__(*args, **kwargs)

    def _parse(self, request):
        StringWidget._parse(self, request)
        if self.regex and self.value is not None:
            match = re.match(self.regex, self.value)
            if not match or not match.group() == self.value:
                self.error = _('wrong format')

class UrlWidget(ValidatedStringWidget):
    '''StringWidget which checks the value entered is a correct url starting with http or https'''
    regex = r'^https?://.+'

    def _parse(self, request):
        ValidatedStringWidget._parse(self, request)
        if self.error:
            self.error = _('must start with http:// or https:// and have a domain name')


class VarnameWidget(ValidatedStringWidget):
    '''StringWidget which checks the value entered is a syntactically correct
       variable name.'''
    regex = r'^[a-zA-Z][a-zA-Z0-9_]*'

    def _parse(self, request):
        ValidatedStringWidget._parse(self, request)
        if self.error:
            self.error = _('must only consist of letters, numbers, or underscore')


class FileSizeWidget(ValidatedStringWidget):
    '''StringWidget which checks the value entered is a syntactically correct
       file size.'''
    regex = r'^\s*([\d]+)\s*([MKk]i?)?[oB]?\s*$'

    def __init__(self, *args, **kwargs):
        hint = kwargs.pop('hint', _('Accepted units: MB (megabytes), kB (kilobytes), for example: 3 MB'))
        ValidatedStringWidget.__init__(self, *args, hint=hint, **kwargs)

    @classmethod
    def parse_file_size(cls, value):
        try:
            value, unit = re.search(cls.regex, value).groups()
        except AttributeError: #  None has no .groups()
            raise ValueError()
        coeffs = {
            'Mi': 2**20,
            'Ki': 2**10,
            'ki': 2**10,
            'M':  10**6,
            'K':  10**3,
            'k':  10**3,
            None: 1,
        }
        return int(value) * coeffs.get(unit)

    def _parse(self, request):
        ValidatedStringWidget._parse(self, request)
        if self.error:
            self.error = _('invalid file size')


class CaptchaWidget(CompositeWidget):
    def __init__(self, name, value = None, mode = 'arithmetic-simple', *args, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)
        self.render_br = False
        if value:
            token = value
        else:
            token = get_session().create_captcha_token()
        hidden_input = htmltext('<input type="hidden" name="%s$token" value="%s"></input>')
        self.add(HtmlWidget, 'token', title = hidden_input % (self.name, token['token']))

        # create question, and fill token['answer']
        if mode == 'arithmetic' or mode == 'arithmetic-simple':
            a, b = random.randint(2, 9), random.randint(2, 9)
            while b == a:
                # don't get twice the same number
                b = random.randint(2, 9)
            if mode == 'arithmetic-simple':
                operator = random.choice([N_('plus'), N_('minus')])
            else:
                operator = random.choice([N_('times'), N_('plus'), N_('minus')])
            if operator == 'times':
                answer = a * b
            elif operator == 'plus':
                answer = a + b
            elif operator == 'minus':
                if b > a:
                    a, b = b, a
                answer = a - b
            self.question = _('What is the result of %(a)d %(op)s %(b)d?') % {
                    'a': a, 'b': b, 'op': _(operator)}
            self.hint = kwargs.get('hint')
            if self.hint is None:
                self.hint = _('Please answer this simple mathematical question as proof you are not a bot.')
        self.add(StringWidget, 'q', required=True, attrs={'required': 'required'})
        token['answer'] = str(answer)

    def _parse(self, request):
        v = {'answer': self.get('q'),
             'token': request.form.get('%s$token' % self.name) }
        token = get_session().get_captcha_token(v['token'])
        if v['answer'] and token and token['answer'] == v['answer'].strip():
            get_session().won_captcha = True
            self.value = v
        elif v['answer']:
            self.error = _('wrong answer')

    def get_title(self):
        return self.question

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

class WidgetList(quixote.form.widget.WidgetList):
    def __init__(self, name, value=None,
                 element_type=StringWidget,
                 element_kwargs={},
                 add_element_label="Add row", **kwargs):

        if add_element_label == 'Add row':
            add_element_label = _('Add row')

        super(WidgetList, self).__init__(name, value=value,
                element_type=element_type,
                element_kwargs=element_kwargs,
                add_element_label=add_element_label, **kwargs)

    def add_media(self):
        get_response().add_javascript(['jquery.js', 'widget_list.js'])

    def render(self):
        return render(self)

    def render_content(self):
        r = TemplateIO(html=True)
        add_element_widget = self.get_widget('add_element')
        add_element_widget.render_br = False
        add_element_widget.extra_css_class = 'list-add'
        for widget in self.get_widgets():
            if widget is add_element_widget:
                continue
            r += widget.render()
        r += add_element_widget.render()
        return r.getvalue()

class WidgetDict(quixote.form.widget.WidgetDict):
    # Fix the title and hint setting
    # FIXME: to be fixed in Quixote upstream : title and hint parameters should be removed
    def __init__(self, name, value=None, title='', hint='',
                 element_key_type=StringWidget,
                 element_value_type=StringWidget,
                 element_key_kwargs={},
                 element_value_kwargs={},
                 add_element_label='Add row', **kwargs):

        if add_element_label == 'Add row':
            add_element_label = _('Add row')

        quixote.form.widget.WidgetDict.__init__(self, name, value,
                element_key_type=element_key_type,
                element_value_type=element_value_type,
                element_key_kwargs=element_key_kwargs,
                element_value_kwargs=element_value_kwargs,
                add_element_label=add_element_label,
                **kwargs)
        if title:
            self.title = title
        if hint:
            self.hint = hint

    def render_content(self):
        r = TemplateIO(html=True)

        lines = []
        for name in self.element_names:
            if name in ('add_element', 'added_elements'):
                continue
            key_widget = self.get_widget(name + 'key')
            value_widget = self.get_widget(name + 'value')
            lines.append({'key': key_widget, 'value': value_widget})

        def sort_key(line):
            if not isinstance(line['key'], StringWidget) or not line['key'].value:
                return (1, None)  # empty keys always at the end
            return (0, line['key'].value)
        lines.sort(key=sort_key)

        for line in lines:
            r += htmltext('<div class="dict-key">%s</div>'
                          '<div class="dict-separator">: </div>'
                          '<div class="dict-value">%s</div>') % (
                line['key'].render(),
                line['value'].render())
            r += htmltext('\n')
        add_element_widget = self.get_widget('add_element')
        add_element_widget.render_br = False
        add_element_widget.extra_css_class = 'list-add'
        r += add_element_widget.render()
        r += self.get_widget('added_elements').render()
        return r.getvalue()

class TagsWidget(StringWidget):
    def __init__(self, name, value = None, known_tags = None, **kwargs):
        StringWidget.__init__(self, name, value, **kwargs)
        self.known_tags = known_tags

    def _parse(self, request):
        StringWidget._parse(self, request)
        if self.value is not None:
            self.value = [x.strip() for x in self.value.split(',') if x.strip()]

    def add_media(self):
        get_response().add_javascript(['jquery.js', 'jquery.autocomplete.js'])
        get_response().add_css_include('../js/jquery.autocomplete.css')

    def render_content(self):
        r = TemplateIO(html=True)
        id = 'tags-%s' % randbytes(8)
        if self.value:
            value = ', '.join(self.value) + ', '
        else:
            value = ''
        r += htmltag('input', xml_end = True,
                id = id,
                type = self.HTML_TYPE,
                name = self.name,
                value = value,
                **self.attrs)
        if self.known_tags:
            known_tags = ', '.join([repr(x) for x in sorted(self.known_tags)])
        else:
            known_tags = ''
        r += htmltext('''<script type="text/javascript">
$("#%s").autocompleteArray([
    %s
    ],{mode:'multiple', multipleSeparator:', '});
</script>''' % (id, known_tags))
        return r.getvalue()

class WysiwygTextWidget(TextWidget):
    def _parse(self, request):
        TextWidget._parse(self, request)
        if self.value:
            if _sanitizeHTML:
                self.value = _sanitizeHTML(self.value, get_request().charset, 'text/html')
            elif six.PY2 and isinstance(self.value, unicode):
                self.value = self.value.encode(get_publisher().site_charset)
            if self.value.startswith('<br />'):
                self.value = self.value[6:]
            if self.value.endswith('<br />'):
                self.value = self.value[:-6]
            # unescape Django template tags
            parser = HTMLParser()
            charset = get_publisher().site_charset
            def unquote_django(matchobj):
                return force_str(parser.unescape(force_text(matchobj.group(0), charset)))
            self.value = re.sub('{[{%](.*?)[%}]}', unquote_django, self.value)

    def add_media(self):
        get_response().add_javascript(['qommon.wysiwyg.js'])

    def render_content(self):
        from ckeditor.widgets import DEFAULT_CONFIG as CKEDITOR_DEFAULT_CONFIG
        attrs = self.attrs.copy()
        config = copy.deepcopy(CKEDITOR_DEFAULT_CONFIG)
        config.update(settings.CKEDITOR_CONFIGS['default'])
        attrs['data-config'] = json.dumps(config)
        return (htmltag('textarea', name=self.name, **attrs) +
                htmlescape(self.value or '') +
                htmltext("</textarea>"))


class TableWidget(CompositeWidget):
    readonly = False

    def __init__(self, name, value = None, rows = None, columns = None, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)
        self.rows = rows
        self.columns = columns

        if 'title' in kwargs:
            del kwargs['title']

        if kwargs.get('readonly'):
            self.readonly = True

        for i, row in enumerate(rows):
            for j, column in enumerate(columns):
                widget = self.add_widget(kwargs, i, j)
                widget = self.get_widget('c-%s-%s' % (i, j))
                if value and self.readonly:
                    if get_request().get_method() == 'POST':
                        if not get_request().form:
                            get_request().form = {}
                        get_request().form[widget.name] = value[i][j]
                    widget.set_value(value[i][j])

    def add_widget(self, kwargs, i, j):
        widget_kwargs = {}
        if kwargs.get('readonly'):
            widget_kwargs['readonly'] = 'readonly'
        return self.add(StringWidget, 'c-%s-%s' % (i, j), **widget_kwargs)

    def render_content(self):
        r = TemplateIO(html=True)
        r += htmltext('<table><thead><tr><td></td>')
        for column in self.columns:
            r += htmltext('<th><span>%s</span></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):
                widget = self.get_widget('c-%s-%s' % (i, j))
                r += htmltext('<td>')
                r += widget.render_content()
                r += htmltext('</td>')
            r += htmltext('</tr>')
        r += htmltext('</tbody></table>')
        return r.getvalue()

    def parse(self, request=None):
        CompositeWidget.parse(self, request=request)
        if request is None:
            request = get_request()
        if (request.form or request.get_method() == 'POST') and self.required:
            if not self.value:
                self.set_error(self.REQUIRED_ERROR)
            else:
                for row in self.value:
                    for column in row:
                        if column:
                            break
                    else:
                        continue
                    break
                else:
                    self.set_error(self.REQUIRED_ERROR)
        return self.value

    def _parse(self, request):
        if self.readonly:
            return
        table = []
        for i, row in enumerate(self.rows):
            row = []
            for j, column in enumerate(self.columns):
                widget = self.get_widget('c-%s-%s' % (i, j))
                row.append(widget.parse())
            table.append(row)
        self.value = table

    def set_value(self, value):
        self.value = value
        if not value:
            return
        for i, row in enumerate(self.rows):
            for j, column in enumerate(self.columns):
                widget = self.get_widget('c-%s-%s' % (i, j))
                try:
                    widget.set_value(value[i][j])
                except IndexError:
                    pass


class SingleSelectTableWidget(TableWidget):
    def add_widget(self, kwargs, i, j):
        widget_kwargs = {'options': kwargs.get('options')}
        if kwargs.get('readonly'):
            widget_kwargs['readonly'] = 'readonly'
        return self.add(SingleSelectWidget, 'c-%s-%s' % (i, j), **widget_kwargs)


class CheckboxesTableWidget(TableWidget):
    def add_widget(self, kwargs, i, j):
        widget_kwargs = {'options': kwargs.get('options')}
        if kwargs.get('readonly'):
            widget_kwargs['readonly'] = 'readonly'
        return self.add(CheckboxWidget, 'c-%s-%s' % (i, j), **widget_kwargs)


class SingleSelectHintWidget(SingleSelectWidget):
    template_name = 'qommon/forms/widgets/select.html'

    def __init__(self, name, value=None, **kwargs):
        self.options_with_attributes = kwargs.pop('options_with_attributes', None)
        self.select2 = kwargs.pop('select2', None)
        super(SingleSelectHintWidget, self).__init__(name, value=value, **kwargs)

    def add_media(self):
        if self.select2:
            get_response().add_javascript(['jquery.js', '../../i18n.js', 'qommon.forms.js', 'select2.js'])
            get_response().add_css_include('../js/select2/select2.css')

    def separate_hint(self):
        return (self.hint and len(self.hint) > 80)

    def get_options(self):
        if self.options_with_attributes:
            options = self.options_with_attributes[:]
        else:
            options = self.options[:]
        if options[0][0] is None:
            options = self.options[1:]

        tags = []
        for option in options:
            object, description, key = option[:3]
            html_attrs = {}
            html_attrs['value'] = key
            if self.is_selected(object):
                html_attrs['selected'] = 'selected'
            if self.options_with_attributes and option[-1].get('disabled'):
                html_attrs['disabled'] = 'disabled'
            if description is None:
                description = ''
            yield {'description': description, 'attrs': html_attrs,
                    'options': option[-1] if self.options_with_attributes else None}

    def has_valid_options(self):
        # helper function for templates, return True if there's at least a
        # valid option.
        for option in self.get_options():
            if not option['attrs'].get('disabled'):
                return True
        return False

    def get_hint(self):
        if self.separate_hint():
            return SingleSelectWidget.get_hint(self)
        return None


class WidgetListAsTable(WidgetList):
    def render_content(self):
        r = TemplateIO(html=True)
        add_element_widget = self.get_widget('add_element')
        add_element_widget.render_br = False
        add_element_widget.extra_css_class = 'list-add'
        for widget in self.get_widgets():
            if widget is add_element_widget:
                continue
            if not hasattr(widget, 'render_content_as_tr'):
                r += widget.render()
        r += htmltext('<table>')
        r += self.get_widgets()[1].render_as_thead()
        for widget in self.get_widgets():
            if widget is add_element_widget:
                continue
            if hasattr(widget, 'render_content_as_tr'):
                r += widget.render_content_as_tr()
        r += htmltext('</table>')
        if not self.readonly:
            r += add_element_widget.render()
        return r.getvalue()

    def render(self):
        return render(self)


class TableRowWidget(CompositeWidget):
    def __init__(self, name, value=None, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)
        for i, column in enumerate(self.columns):
            self.add(StringWidget, name='col%s'%i, title=column, **kwargs)

class TableListRowsWidget(WidgetListAsTable):
    readonly = False

    def create_row_class(self, columns):
        class klass(TableRowWidget): pass
        setattr(klass, 'columns', columns)
        return klass

    def add_element(self, value=None):
        name = "element%d" % len(self.element_names)
        self.add(self.table_row_class, name, value=value, **self.widget_kwargs)
        self.element_names.append(name)
        self.get_widget('added_elements').value = len(self.element_names)

    def __init__(self, name, value=None, columns=None, min_rows=5, **kwargs):
        self.table_row_class = self.create_row_class(columns)
        self.widget_kwargs = {}
        if 'readonly' in kwargs:
            del kwargs['readonly']
            self.readonly = True
            self.widget_kwargs['readonly'] = 'readonly'
        WidgetListAsTable.__init__(self, name, value,
                element_type=self.table_row_class,
                element_kwargs=self.widget_kwargs, **kwargs)
        self.columns = columns

        while len(self.element_names) < min_rows:
            self.add_element()

        self.set_value(value)

    def parse(self, request=None):
        WidgetListAsTable.parse(self, request=request)
        if request is None:
            request = get_request()
        add_element_pushed = self.get_widget('add_element').parse()
        if (request.form or request.get_method() == 'POST') and self.required:
            if not self.value and not add_element_pushed:
                self.set_error(self.REQUIRED_ERROR)
            for row in self.value:
                for column in row:
                    if column:
                        break
                else:
                    continue
                break
            else:
                if not add_element_pushed:
                    self.set_error(self.REQUIRED_ERROR)
        return self.value

    def _parse(self, request):
        if self.readonly:
            return
        table = []
        for i, row_name in enumerate(self.element_names):
            row = []
            row_widget = self.get_widget(row_name)
            notnull = False
            for j, column in enumerate(self.columns):
                widget = row_widget.get_widget('col%s'%j)
                row.append(widget.parse())
                if row[-1]:
                    notnull = True
            if notnull:
                table.append(row)
        self.value = table

    def set_value(self, value):
        self.value = value
        if not value:
            return
        while len(self.element_names) < len(value):
            self.add_element()
        for i, row_name in enumerate(self.element_names):
            widget_row = self.get_widget(row_name)
            for j, column in enumerate(self.columns):
                widget = widget_row.get_widget('col%s'%j)
                try:
                    widget.set_value(value[i][j])
                    if get_request().get_method() == 'POST':
                        if not get_request().form.get(widget.get_name()):
                            get_request().form[widget.get_name()] = value[i][j]
                except IndexError:
                    pass

class RankedItemsWidget(CompositeWidget):
    readonly = False

    def __init__(self, name, value=None, elements=None, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)
        self.element_names = {}

        if 'title' in kwargs:
            del kwargs['title']
        if 'readonly' in kwargs:
            if kwargs['readonly']:
                self.readonly = True
            del kwargs['readonly']
        if 'required' in kwargs:
            if kwargs['required']:
                self.required = True
            del kwargs['required']

        self.randomize_items = False
        if 'randomize_items' in kwargs:
            if kwargs['randomize_items']:
                self.randomize_items = True
            del kwargs['randomize_items']

        for v in elements:
            if type(v) is tuple:
                title = v[1]
                key = v[0]
                if type(key) is int:
                    name = 'element%d' % v[0]
                elif type(key) in (str, htmltext):
                    name = str('element%s' % v[0])
                    key = str(key)
                else:
                    raise NotImplementedError()
            else:
                title = v
                key = v
                name = 'element%d' % len(self.element_names.keys())

            if value:
                position = value.get(key)
            else:
                position = None
            self.add(IntWidget, name, title=title, value=position, size=5,
                     required=False, **kwargs)
            self.element_names[name] = key

        if self.randomize_items:
            random.shuffle(self.widgets)

        if self.readonly:
            self.widgets.sort(key=lambda x: x.value or sys.maxsize)

            if value:
                # in readonly mode, we mark all fields as already parsed, to
                # avoid getting them reinitialized on a has_error() call
                for name in self.element_names:
                    self.get_widget(name)._parsed = True
                self._parsed = True

    def _parse(self, request):
        values = {}
        for name in self.element_names:
            value = self.get(name)
            if value is not None:
                values[self.element_names[name]] = value
            if value is not None and type(value) is not int:
                self.get_widget(name).set_error(IntWidget.TYPE_ERROR)
        self.value = values or None

    def set_value(self, value):
        self.value = value
        if value:
            for name in self.element_names:
                self.get_widget(name).set_value(self.value.get(self.element_names[name]))

    def render_content(self):
        r = TemplateIO(html=True)
        r += htmltext('<ul>')
        for widget in self.get_widgets():
            if widget.has_error():
                r += htmltext('<li class="error"><label>')
            else:
                r += htmltext('<li><label>')
            if self.readonly:
                widget.attrs['disabled'] = 'disabled'
                if widget.value:
                    r += htmltext('<input type="hidden" name="%s" value="%s" >') % (
                            widget.name, widget.value)
                widget.name = widget.name + 'xx'
            r += widget.render_content()
            r += widget.title
            r += htmltext('</label>')
            r += htmltext('</li>')
        r += htmltext('</ul>')
        return r.getvalue()


class JsonpSingleSelectWidget(Widget):
    template_name = 'qommon/forms/widgets/select_jsonp.html'

    def __init__(self, name, value=None, url=None, **kwargs):
        super(JsonpSingleSelectWidget, self).__init__(name, value=value, **kwargs)
        self.url = url

    def add_media(self):
        get_response().add_javascript(['jquery.js', '../../i18n.js', 'qommon.forms.js', 'select2.js'])
        get_response().add_css_include('../js/select2/select2.css')

    def get_display_value(self):
        if self.value is None:
            value = None
        else:
            value = htmlescape(self.value)
        if not value or not get_session().jsonp_display_values:
            return None
        key = '%s_%s' % (self.url, value)
        return get_session().jsonp_display_values.get(key)

    def get_select2_url(self):
        if Template.is_template_string(self.url):
            vars = get_publisher().substitutions.get_context_variables()
            # skip variables that were not set (None)
            vars = dict((x, y) for x, y in vars.items() if y is not None)
            url = misc.get_variadic_url(self.url, vars, encode_query=False)
        else:
            url = self.url
        return url

    def parse(self, request=None):
        if request and request.form.get(self.name) and request.form.get(self.name + '_display'):
            # store text value associated to the jsonp value
            if not get_session().jsonp_display_values:
                get_session().jsonp_display_values = {}
            value = request.form.get(self.name)
            display_value = request.form.get(self.name + '_display')
            get_session().jsonp_display_values['%s_%s' % (self.url, value)] = display_value

        return Widget.parse(self, request=request)

class AutocompleteStringWidget(WcsExtraStringWidget):
    url = None

    def __init__(self, *args, **kwargs):
        WcsExtraStringWidget.__init__(self, *args, **kwargs)
        if kwargs.get('url'):
            self.url = kwargs.get('url')

    def add_media(self):
        get_response().add_javascript(['jquery.js', 'jquery-ui.js'])

    def render_content(self):
        if Template.is_template_string(self.url):
            vars = get_publisher().substitutions.get_context_variables()
            # skip variables that were not set (None)
            vars = dict((x, y) for x, y in vars.items() if y is not None)
            url = misc.get_variadic_url(self.url, vars, encode_query=False)
        else:
            url = self.url

        r = TemplateIO(html=True)
        r += WcsExtraStringWidget.render_content(self)
        if not url:
            # there's no autocomplete URL, get out now.
            return r.getvalue()

        r += htmltext("""
<script id="script_%(id)s">
$(function() {
  $("#form_%(id)s").autocomplete({
    source: function( request, response ) {
      $.ajax({
        url: $("#form_%(id)s").data('uiAutocomplete').options.url,
        dataType: "jsonp",
        data: {
          q: request.term
        },
        success: function( data ) {
          response( $.map(data.data, function(item) {
            return {label: item.text, value: item.label};
           }));
        }
      });
    },
    minLength: 2,
    open: function() {
      $(this).removeClass("ui-corner-all").addClass("ui-corner-top");
    },
    close: function() {
      $(this).removeClass("ui-corner-top").addClass("ui-corner-all");
    }
  });
""" % {'id': self.name})

        if not '[var_' in url:
            r += htmltext("""
$("#form_%(id)s").data('uiAutocomplete').options.url = '%(url)s';
""" % {'id': self.name, 'url': url})

        if '[var_' in url:
            # if this is a parametric url, store template url and hook to the
            # appropriate onchange event to give the url to autocomplete
            r += htmltext("""
$("#form_%(id)s").data('uiAutocomplete').options.wcs_base_url = '%(url)s';
""" % {'id': self.name, 'url': url})
            variables = re.findall(r'\[(var_.+?)\]', url)
            r += htmltext("""
function url_replace_%(id)s() {
    var url = $("#form_%(id)s").data('uiAutocomplete').options.wcs_base_url;""" % {'id': self.name})
            for variable in variables:
                r += htmltext("""
    selector = '#' + $('#%(variable)s').data('valuecontainerid');
    url = url.replace('[%(variable)s]', $(selector).val() || '');""" % {'variable': variable})
            r += htmltext("""
    $("#form_%(id)s").data('uiAutocomplete').options.url = url;
    if ($("form_%(id)s").val() != $("form_%(id)s").attr('value'))
        $("#form_%(id)s").val('');
}
""" % {'id': self.name} )
            for variable in variables:
                r += htmltext("""
$('#%(variable)s').change(url_replace_%(id)s);
$('#%(variable)s').change();
""" % {'id': self.name, 'variable': variable})

        r += htmltext("""
});
</script>""")

        return r.getvalue()


class ColourWidget(SingleSelectWidget):

    def __init__(self, *args, **kwargs):
        colours = ['%s%s%s' % x for x in itertools.product(('00', '66', '99', 'FF'), repeat=3)]
        SelectWidget.__init__(self, options=colours, *args, **kwargs)
        self.attrs['class'] = 'colour-picker'

    def add_media(self):
        get_response().add_javascript(['jquery.js', 'jquery.colourpicker.js'])

    def _parse(self, request):
        SingleSelectWidget._parse(self, request)
        if self.value is not None:
            self.value = self.value.upper()


class PasswordEntryWidget(CompositeWidget):
    min_length = 0
    max_length = 0
    count_uppercase = 0
    count_lowercase = 0
    count_digit = 0
    count_special = 0
    confirmation = True

    def __init__(self, name, value=None, **kwargs):
        # hint will be displayed with pwd1 widget
        hint = kwargs.pop('hint', None)
        CompositeWidget.__init__(self, name, value, **kwargs)
        self.min_length = kwargs.get('min_length', 0)
        self.max_length = kwargs.get('max_length', 0)
        self.count_uppercase = kwargs.get('count_uppercase', 0)
        self.count_lowercase = kwargs.get('count_lowercase', 0)
        self.count_digit = kwargs.get('count_digit', 0)
        self.count_special = kwargs.get('count_special', 0)
        self.confirmation = kwargs.get('confirmation', True)
        confirmation_title = kwargs.get('confirmation_title') or _('Confirmation')
        self.strength_indicator = kwargs.get('strength_indicator', True)

        self.formats = kwargs.get('formats', ['sha1'])
        if not self.attrs.get('readonly'):
            self.add(PasswordWidget, name='pwd1', title='',
                    value='',
                    required=kwargs.get('required', False),
                    autocomplete='off',
                    hint=hint)
            if self.confirmation:
                self.add(PasswordWidget, name='pwd2', title=confirmation_title,
                        required=kwargs.get('required', False),
                        autocomplete='off')
        else:
            encoded_value = force_text(base64.encodestring(force_bytes(json.dumps(value))))
            if value:
                fake_value = '*' * 8
            else:
                fake_value = ''
            self.add(HtmlWidget, 'hashed', title=htmltext(
                    '<input value="%s" readonly="readonly">'\
                    '<input type="hidden" name="%s$encoded" value="%s"></input>' % (
                        fake_value, self.name, encoded_value)))

    def add_media(self):
        get_response().add_javascript(['jquery.js', 'jquery.passstrength.js'])

    def render_content(self):
        if self.attrs.get('readonly') or not self.strength_indicator:
            return CompositeWidget.render_content(self)
        r = TemplateIO(html=True)
        r += CompositeWidget.render_content(self)
        r += htmltext('''<script>
$(function() {
  $('input[id="form_%(form_id)s"]').passStrengthify({
    levels: ["%(veryweak)s", "%(veryweak)s", "%(weak)s", "%(weak)s", "%(moderate)s", "%(good)s", "%(strong)s", "%(verystrong)s"],
    minimum: %(min_length)s,
    labels: {
      passwordStrength: "%(password_strength)s",
      tooShort: "%(tooshort)s"
    }
  });
});
</script>''') % {'form_id': self.get_widget('pwd1').get_name(),
                'min_length': self.min_length,
                'veryweak': _('Very weak'),
                'weak': _('Weak'),
                'moderate': _('Moderate'),
                'good': _('Good'),
                'strong': _('Strong'),
                'verystrong': _('Very strong'),
                'password_strength': _('Password strength:'),
                'tooshort': _('Too short')}
        return r.getvalue()

    def _parse(self, request):
        CompositeWidget._parse(self, request)
        if request.form.get('%s$encoded' % self.name):
            self.value = json_loads(base64.decodestring(
                force_bytes(request.form.get('%s$encoded' % self.name))))
            return
        pwd1 = self.get('pwd1') or ''

        if not self.get_widget('pwd1'):
            # we are in read-only mode, stop here.
            return

        set_errors = []
        min_len = self.min_length
        if len(pwd1) < min_len:
            set_errors.append(_('Password is too short.  It must be at least %d characters.') % min_len)

        max_len = self.max_length
        if max_len and len(pwd1) > max_len:
            set_errors.append(_('Password is too long.  It must be at most %d characters.') % max_len)

        count = self.count_uppercase
        if len([x for x in pwd1 if x.isupper()]) < count:
            set_errors.append(
                ngettext('Password must contain an uppercase character.',
                    'Password must contain at least %(count)d uppercase characters.',
                    count) % {'count': count})

        count = self.count_lowercase
        if len([x for x in pwd1 if x.islower()]) < count:
            set_errors.append(
                ngettext('Password must contain a lowercase character.',
                    'Password must contain at least %(count)d lowercase characters.',
                    count) % {'count': count})

        count = self.count_digit
        if len([x for x in pwd1 if x.isdigit()]) < self.count_digit:
            set_errors.append(
                ngettext('Password must contain a digit.',
                    'Password must contain at least %(count)d digits.',
                    count) % {'count': count})

        count = self.count_special
        if len([x for x in pwd1 if not x.isalnum()]) < count:
            set_errors.append(
                ngettext('Password must contain a special character.',
                    'Password must contain at least %(count)d special characters.',
                    count) % {'count': count})

        if self.confirmation:
            pwd2 = self.get('pwd2') or ''
            if pwd1 != pwd2:
                self.get_widget('pwd2').set_error(_('Passwords do not match'))
                pwd1 = None

        if set_errors:
            self.get_widget('pwd1').set_error(' '.join(set_errors))
            pwd1 = None

        PASSWORD_FORMATS = {
            'cleartext': lambda x: force_str(x),
            'md5': lambda x: force_str(hashlib.md5(force_bytes(x)).hexdigest()),
            'sha1': lambda x: force_str(hashlib.sha1(force_bytes(x)).hexdigest()),
        }

        if pwd1:
            self.value = {}
            for fmt in self.formats:
                self.value[fmt] = PASSWORD_FORMATS[fmt](pwd1)
        else:
            self.value = None


class MapWidget(CompositeWidget):
    template_name = 'qommon/forms/widgets/map.html'

    def __init__(self, name, value=None, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)
        self.add(HiddenWidget, 'latlng', value=value)
        widget = self.get_widget('latlng')
        if (value and get_request().get_method() == 'POST' and
                      get_request().form and
                      not get_request().form.get(widget.name)):
            get_request().form[widget.name] = value
        self.readonly = kwargs.pop('readonly', False)
        self.map_attributes = {}
        self.map_attributes.update(get_publisher().get_map_attributes())
        self.sync_map_and_address_fields = get_publisher().has_site_option('sync-map-and-address-fields')
        for attribute in ('initial_zoom', 'min_zoom', 'max_zoom', 'init_with_geoloc'):
            if attribute in kwargs:
                self.map_attributes['data-' + attribute] = kwargs.pop(attribute)
        if kwargs.get('default_position'):
            self.map_attributes['data-def-lat'] = kwargs['default_position'].split(';')[0]
            self.map_attributes['data-def-lng'] = kwargs['default_position'].split(';')[1]

    def initial_position(self):
        if self.value:
            return {'lat': self.value.split(';')[0],
                    'lng': self.value.split(';')[1]}
        return None

    def add_media(self):
        get_response().add_javascript(['qommon.map.js'])

    def render_widget_content(self):
        # widget content (without label, hint, etc.) is reused on status page;
        # render the appropriate block.
        self.add_media()
        template_names = get_template_names(self)
        context = {'widget': self}
        return htmltext(force_str(render_block_to_string(template_names, 'widget-content', context)))

    def _parse(self, request):
        CompositeWidget._parse(self, request)
        self.value = self.get('latlng')
        if self.value:
            lat, lon = self.value.split(';')
            lat_lon = misc.normalize_geolocation({'lat': lat, 'lon': lon})
            self.value = '%s;%s' % (lat_lon['lat'], lat_lon['lon'])


class HiddenErrorWidget(HiddenWidget):
    def set_error(self, error):
        Widget.set_error(self, error)


class SingleSelectWidgetWithOther(CompositeWidget):
    def __init__(self, name, value=None, **kwargs):
        CompositeWidget.__init__(self, name, value=value, **kwargs)
        if 'title' in kwargs:
            del kwargs['title']
        options = kwargs.get('options')[:]
        if len(options[0]) == 1:
            options = [(x[0], x[0], x[0]) for x in options]
        elif len(options[0]) == 2:
            options = [(x[0], x[1], x[0]) for x in options]
        options.append(('__other', kwargs.pop('other_label', _('Other:')), '__other'))
        kwargs['options'] = options
        if value is None or value in [x[0] for x in options]:
            choice_value = value
            other_value = None
        else:
            choice_value = '__other'
            other_value = value
        self.add(SingleSelectWidget, 'choice', value=choice_value, **kwargs)
        self.add(StringWidget, 'other', value=other_value, size=35)

    def _parse(self, request):
        self.value = self.get('choice')
        if self.value == '__other':
            self.value = self.get('other')


class ComputedExpressionWidget(CompositeWidget):
    '''Widget that checks the entered value is a correct workflow
       expression.'''

    def __init__(self, name, value=None, *args, **kwargs):
        if not value:
            value = {}
        else:
            from wcs.workflows import WorkflowStatusItem
            value = WorkflowStatusItem.get_expression(value)
        CompositeWidget.__init__(self, name, value, **kwargs)

        options = [
            ('text', _('Text'), 'text'),
            ('template', _('Template'), 'template'),
            ('python', _('Python Expression'), 'python'),
        ]

        self.add(StringWidget, 'value_text', size=80,
                 value=value.get('value') if value.get('type') == 'text' else None)

        self.add(StringWidget, 'value_template', size=80,
                 value=value.get('value') if value.get('type') == 'template' else None)

        self.add(StringWidget, 'value_python', size=80,
                 value=value.get('value') if value.get('type') == 'python' else None)

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

    def render_content(self):
        return htmltext('''
           <style>
           span[data-name="%(name)s"].text::after { content: "%(text_label)s"; }
           span[data-name="%(name)s"].template::after { content: "%(template_label)s"; }
           span[data-name="%(name)s"].python::after { content: "%(python_label)s"; }
           </style>
           <span class="text"
                 data-name="%(name)s"
                 data-dynamic-display-value="text"
                 data-dynamic-display-child-of="%(name)s$type"
                 >%(value_text)s</span
          ><span class="template"
                 data-name="%(name)s"
                 data-dynamic-display-value="template"
                 data-dynamic-display-child-of="%(name)s$type"
                 >%(value_template)s</span
          ><span class="python"
                 data-name="%(name)s"
                 data-dynamic-display-value="python"
                 data-dynamic-display-child-of="%(name)s$type"
                 >%(value_python)s</span
          >%(type)s''') % {
                'name': self.name,
                'text_label': _('Text'),
                'template_label': _('Template'),
                'python_label': _('Python'),
                'value_text': self.get_widget('value_text').render_content(),
                'value_template': self.get_widget('value_template').render_content(),
                'value_python': self.get_widget('value_python').render_content(),
                'type': self.get_widget('type').render_content()
            }

    @classmethod
    def validate_template(cls, template):
        try:
            Template(template, raises=True)
        except TemplateError as e:
            raise ValidationError('%s' % e)

    @classmethod
    def validate(cls, expression):
        if not expression:
            return
        from wcs.workflows import WorkflowStatusItem
        expression = WorkflowStatusItem.get_expression(expression)
        if expression['type'] == 'python':
            if '{{' in expression['value']:
                raise ValidationError(_('invalid usage, Python expression cannot contain {{'))
            try:
                compile(expression['value'], '<string>', 'eval')
            except SyntaxError as e:
                raise ValidationError(_('syntax error in Python expression: %s') % e)
        else:
            cls.validate_template(expression['value'])

    def _parse(self, request):
        self.value = None
        if not self.get('type'):
            return
        value_type = self.get('type')
        value_content = self.get('value_%s' % value_type)
        if value_type == 'python':
            self.value = '=' + (value_content or '')
        else:
            self.value = value_content
        if self.value:
            try:
                self.validate(self.value)
            except ValidationError as e:
                self.set_error(str(e))


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

        if not value:
            value = {}

        options = []
        options.append(('django', _('Django Expression'), 'django'))
        if not kwargs.get('django_only'):
            options.append(('python', _('Python Expression'), 'python'))

        self.add(StringWidget, 'value_django', size=80,
                 value=value.get('value') if value.get('type') == 'django' else None)

        self.add(StringWidget, 'value_python', size=80,
                 value=value.get('value') if value.get('type') == 'python' else None)

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

    @property
    def content_extra_attributes(self):
        validation_url = get_publisher().get_root_url() + 'api/validate-condition'
        return {'data-validation-url': validation_url}

    def _parse(self, request):
        self.value = None
        if not self.get('type') or self.get('type') == 'none':
            return

        self.value = {}
        self.value['type'] = self.get('type')

        if self.value['type']:
            self.value['value'] = self.get('value_%s' % self.value['type'])

        if self.value['value']:
            try:
                Condition(self.value).validate()
            except ValidationError as e:
                self.set_error(str(e))
        else:
            self.value = None

    def render_content(self):
        return htmltext('''
           <span class="django"
                 data-dynamic-display-value="django"
                 data-dynamic-display-child-of="%(name)s$type"
                 >%(value_django)s</span
          ><span class="python"
                 data-dynamic-display-value="python"
                 data-dynamic-display-child-of="%(name)s$type"
                 >%(value_python)s</span
          >%(type)s''') % {
                'name': self.name,
                'value_django': self.get_widget('value_django').render_content(),
                'value_python': self.get_widget('value_python').render_content(),
                'type': self.get_widget('type').render_content()
            }


class DjangoConditionWidget(StringWidget):
    def _parse(self, request):
        super(DjangoConditionWidget, self)._parse(request)
        if self.value:
            try:
                Condition({'type': 'django', 'value': self.value}).validate()
            except ValidationError as e:
                self.set_error(str(e))
