"""$URL: svn+ssh://svn/repos/trunk/quixote/form2/form.py $
$Id: form.py 23744 2004-03-18 21:41:32Z rmasse $

Provides the Form class and related classes.  Forms are a convenient
way of building HTML forms that are composed of Widget objects.
"""

from quixote import get_request, get_session, get_publisher
from quixote.html import url_quote, htmltag, htmltext, nl2br, TemplateIO
from quixote.form2.widget import HiddenWidget, StringWidget, TextWidget, \
    CheckboxWidget, SingleSelectWidget, RadiobuttonsWidget, \
    MultipleSelectWidget, ResetWidget, SubmitWidget, FloatWidget, \
    IntWidget


try:
    True, False, bool
except NameError:
    True = 1
    False = 0
    def bool(v):
        return not not v


class FormTokenWidget(HiddenWidget):
    def render(self):
        self.value = get_session().create_form_token()
        return HiddenWidget.render(self)


class Form:
    # XXX needs a nice docstring that explains typical usage!
    """
    Instance attributes:
      widgets : [Widget]
      submit_widgets : [SubmitWidget]
      hidden_widgets : [HiddenWidget]
      _names : { name:string : Widget }
        names used in the form and the widgets associated with them
    """

    TOKEN_NAME = "_form_id" # name of hidden token widget

    JAVASCRIPT_MARKUP = htmltext('<script type="text/javascript">\n'
                                 '<!--\n'
                                 '%s\n'
                                 '// -->p\n'
                                 '</script>\n')

    def __init__(self,
                 name=None,
                 method="post",
                 action_url=None,
                 enctype=None,
                 use_tokens=True,
                 component_class=None):

        if method not in ("post", "get"):
            raise ValueError("Form method must be 'post' or 'get', "
                             "not %r" % method)
        self.name = name
        self.method = method
        self.action_url = action_url or self._get_default_action_url()
        self.widgets = []
        self.submit_widgets = []
        self.hidden_widgets = []
        self._names = {}

        if enctype is not None and enctype not in (
            "application/x-www-form-urlencoded", "multipart/form-data"):
            raise ValueError, ("Form enctype must be "
                               "'application/x-www-form-urlencoded' or "
                               "'multipart/form-data', not %r" % enctype)
        self.enctype = enctype

        self.use_form_tokens = False
        if use_tokens and self.method == "post":
            config = get_publisher().config
            if config.form_tokens:
                # unique token for each form, this prevents many cross-site
                # attacks and prevents a form from being submitted twice
                self.add(FormTokenWidget, self.TOKEN_NAME, value=None)
                self.use_form_tokens = True


    def _get_default_action_url(self):
        request = get_request()
        action_url = url_quote(request.get_path())
        query = request.get_environ("QUERY_STRING")
        if query:
            action_url += "?" + query
        return action_url

    # -- Form data access methods --------------------------------------

    def __getitem__(self, name):
        """(name:string) -> any
        Return a widget's value.
        """
        try:
            return self._names[name].parse()
        except KeyError:
            raise KeyError, 'no widget named %r' % name

    def has_key(self, name):
        """Return true if the widget named 'name' is in the form."""
        return self._names.has_key(name)

    def get(self, name):
        widget = self._names.get(name)
        if widget is not None:
            return widget.parse()
        else:
            return None

    def get_widget(self, name):
        """(name:string) -> Widget | None
        """
        return self._names.get(name)

    def get_submit_widgets(self):
        return self.submit_widgets

    def get_all_widgets(self):
        return self._names.values()

    # -- Form processing and error checking ----------------------------

    def is_submitted(self):
        """() -> bool

        Return true if a form was submitted.
        """
        request = get_request()
        if self.method == 'post':
            if request.get_method() == 'POST':
                return True
            else:
                return False
        else:
            return bool(request.form)

    def has_errors(self):
        """() -> bool

        Ensure that all components of the form have parsed themselves. Return
        true if any of them have errors.
        """
        request = get_request()
        if request.form:
            for widget in self.get_all_widgets():
                widget.parse(request)
            for widget in self.get_all_widgets():
                if widget.get_error():
                    return True
        return False

    def clear_errors(self):
        for widget in self.get_all_widgets():
            widget.set_error(None)

    def get_submit(self):
        """() -> string | bool

        Get the name of the submit button that was used to submit the
        current form.  If the form is submitted but not by a button added by
        add_submit() then return True.  Otherwise, return False.

        """
        request = get_request()
        for button in self.submit_widgets:
            if button.parse(request):
                return button.name
        else:
            if self.is_submitted():
                return True
            else:
                return False

    def set_error(self, name, error):
        widget = self._names.get(name)
        if not widget:
            raise KeyError, "unknown name %r" % name
        widget.set_error(error)

    # -- Form population methods ---------------------------------------

    def add(self, widget_class, name, *args, **kwargs):
        if self._names.has_key(name):
            raise ValueError, "form already has '%s' widget" % name
        widget = widget_class(name, *args, **kwargs)
        self._names[name] = widget
        if isinstance(widget, SubmitWidget):
            self.submit_widgets.append(widget) # will be rendered at end
        elif isinstance(widget, HiddenWidget):
            self.hidden_widgets.append(widget) # will be render at beginning
        else:
            self.widgets.append(widget)

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

    def add_reset(self, name, value=None, **kwargs):
        self.add(ResetWidget, name, value, **kwargs)

    def add_hidden(self, name, value=None, **kwargs):
        self.add(HiddenWidget, name, value, **kwargs)

    def add_string(self, name, value=None, **kwargs):
        self.add(StringWidget, name, value, **kwargs)

    def add_text(self, name, value=None, **kwargs):
        self.add(TextWidget, name, value, **kwargs)

    def add_checkbox(self, name, value=None, **kwargs):
        self.add(CheckboxWidget, name, value, **kwargs)

    def add_single_select(self, name, value=None, **kwargs):
        self.add(SingleSelectWidget, name, value, **kwargs)

    def add_multiple_select(self, name, value=None, **kwargs):
        self.add(MultipleSelectWidget, name, value, **kwargs)

    def add_radiobuttons(self, name, value=None, **kwargs):
        self.add(RadiobuttonsWidget, name, value, **kwargs)

    def add_float(self, name, value=None, **kwargs):
        self.add(FloatWidget, name, value, **kwargs)

    def add_int(self, name, value=None, **kwargs):
        self.add(IntWidget, name, value, **kwargs)


    # -- Layout (rendering) methods ------------------------------------

    def render(self):
        """() -> HTML text
        Render a form as HTML.
        """
        r = TemplateIO(html=True)
        r += self._render_start()
        r += self._render_body()
        r += self._render_finish()
        return r.getvalue()

    def _render_start(self):
        r = TemplateIO(html=True)
        r += htmltag('form', name=self.name, method=self.method,
                     enctype=self.enctype, action=self.action_url)
        r += self._render_hidden_widgets()
        return r.getvalue()

    def _render_finish(self):
        r = TemplateIO(html=True)
        r += htmltext('</form>')
        code = get_request().response.javascript_code
        if code:
            r += self._render_javascript(code)
        return r.getvalue()

    def _render_sep(self, text, line=True):
        return htmltext('<tr><td colspan="3">%s<strong><big>%s'
                        '</big></strong></td></tr>') % \
                                      (line and htmltext('<hr>') or '', text)

    def _render_widgets(self):
        r = TemplateIO(html=True)
        for widget in self.widgets:
            r += widget.form_render(self)
        return r.getvalue()

    def _render_hidden_widgets(self):
        r = TemplateIO(html=True)
        for widget in self.hidden_widgets:
            r += widget.render()
        return r.getvalue()

    def _render_submit_widgets(self, ncols=3):
        r = TemplateIO(html=True)
        r += htmltext('<tr><td colspan="%d">\n') % ncols
        for widget in self.submit_widgets:
            r += widget.render()
        r += htmltext('</td></tr>')
        return r.getvalue()

    def _render_error_notice(self):
        return htmltext('<tr><td colspan="3">'
                        '<font color="red"><strong>Warning:</strong></font> '
                        'there were errors processing your form.  '
                        'See below for details.'
                        '</td></tr>')

    def render_error(self, error):
        return htmltext('<font color="red">%s</font><br />') % nl2br(error)

    def render_hint(self, hint):
        return htmltext('<em>%s</em>') % hint

    def render_field(self, title, contents, hint, error, required=False):
        title = title or ''
        if title and required:
            title = title + htmltext('&nbsp;*')
        r = TemplateIO(html=True)
        r += htmltext('<tr><th colspan="3" align="left">')
        r += title
        r += htmltext('</th></tr>'
                      '<tr><td>&nbsp;&nbsp;</td><td>')
        r += contents
        r += htmltext('</td><td>')
        if error:
            r += self.render_error(error)
        if hint:
            r += self.render_hint(hint)
        r += htmltext('</td></tr>')
        return r.getvalue()

    def _render_javascript(self, javascript_code):
        """Render javacript code for the form.  Insert code lexically
        sorted by code_id.
        """
        form_code = []
        code_ids = javascript_code.keys()
        code_ids.sort()
        for code_id in code_ids:
            code = javascript_code[code_id]
            if code:
                form_code.append(code)
                javascript_code[code_id] = ''
        if form_code:
            return self.JAVASCRIPT_MARKUP % htmltext(''.join(form_code))
        else:
            return ''

    def _render_body(self):
        r = TemplateIO(html=True)
        r += htmltext('<table>')
        if self.has_errors():
            r += self._render_error_notice()
        r += self._render_widgets()
        r += self._render_submit_widgets()
        r += htmltext('</table>')
        return r.getvalue()


