"""quixote.form.widget

Provides the basic web widget classes: Widget itself, plus StringWidget,
TextWidget, CheckboxWidget, etc.
"""

# created 2000/09/20, GPW

__revision__ = "$Id: widget.py,v 1.5 2001/10/09 21:07:10 abenard Exp $"

from types import *
from quixote import get_request
from quixote.html import html_quote, render_tag, ValuelessAttr


class FormValueError (Exception):
    """Raised whenever a widget has problems parsing its value."""


class Widget:
    """Abstract base class for web widgets.  The key elements
    of a web widget are:
      - name
      - data type (what the widget returns)
      - widget type (how the widget looks/works in the browser)
      - value

    The name and value are instance attributes (because they're specific to
    a particular widget in a particular context); the data type and widget
    type are class attributes.

    Instance attributes:
      name : string
      value : any        # depends on data_type class attribute

    Feel free to access these directly; to set them, use the 'set_*()'
    modifier methods.
    """

    # Subclasses must define these.  'widget_type' is just a string,
    # e.g.  "string", "text", "checkbox".  'data_type' is a string
    # describing the (Python) type of data returned by the widget, eg.
    # "string", "int", "boolean", "[string]", "(string, number)".
    widget_type = None
    data_type = None

    def __init__ (self, name, value=None):
        assert self.__class__ is not Widget, "abstract class"
        self.name = name
        self.value = value

    def __repr__ (self):
        return "<%s at %x: %s>" % (self.__class__.__name__,
                                   id(self),
                                   self.name)

    def __str__ (self):
        return "%s: %s" % (self.widget_type, self.name)

    def set_name (self, name):
        self.name = name

    def set_value (self, value):
        self.value = value

    def clear (self):
        self.value = None


    # -- Subclasses must implement these -------------------------------

    def render (self, request):
        """render(request) -> HTML text"""
        raise NotImplementedError

    def parse (self, request):
        """parse(request) -> any (depends on data_type)"""
        value = request.form.get(self.name)
        if type(value) is StringType and value.strip():
            self.value = value
        else:
            self.value = None

        return self.value


    # -- Convenience methods for subclasses ----------------------------

    # This one's really only for composite widgets; lives here until
    # we have a demonstrated need for a CompositeWidget class.
    def get_subwidget_name (self, name):
        return "%s.%s" % (self.name, name)

    def create_subwidget (self, widget_type, widget_name, value=None, **args):
        from quixote.form.form import get_widget_class
        klass = get_widget_class(widget_type)
        name = self.get_subwidget_name(widget_name)
        return apply(klass, (name, value), args)


# class Widget


# -- Fundamental widget types ------------------------------------------
# These correspond to the standard types of input tag in HTML:
#   text     StringWidget
#   password PasswordWidget
#   radio    RadiobuttonWidget
#   checkbox CheckboxWidget
# 
# and also to the other basic form elements:
#   <textarea>  TextWidget
#   <select>    SingleSelectWidget
#   <select multiple>
#               MultipleSelectWidget


class StringWidget (Widget):
    """Widget for entering a single string: corresponds to
    "<input type=text>" in HTML.

    Instance attributes:
      value : string
      size : int
      maxlength : int
    """

    widget_type = "string"
    data_type = "string"

    # This lets PasswordWidget be a trivial subclass
    html_type = "text"

    def __init__ (self, name, value=None,
                  size=None, maxlength=None):
        Widget.__init__(self, name, value)
        self.size = size
        self.maxlength = maxlength


    def render (self, request):
        return render_tag("input",
                          type=self.html_type,
                          name=self.name,
                          size=self.size,
                          maxlength=self.maxlength,
                          value=self.value)


class PasswordWidget (StringWidget):
    """Trivial subclass of StringWidget for entering passwords (different
    widget type because HTML does it that way).

    Instance attributes: none
    """

    widget_type = "password"
    html_type = "password"


class TextWidget (Widget):
    """Widget for entering a long, multi-line string; corresponds to
    the HTML "<textarea>" tag.

    Instance attributes:
      value : string
      cols : int
      rows : int
      wrap : string
        (see an HTML book for details on text widget wrap options)
    """

    widget_type = "text"
    data_type = "string"

    def __init__ (self, name, value=None,
                  cols=None, rows=None, wrap='physical'):
        Widget.__init__(self, name, value)
        self.cols = cols
        self.rows = rows
        self.wrap = wrap

    def render (self, request):
        return (render_tag("textarea", name=self.name,
                           cols=self.cols,
                           rows=self.rows,
                           wrap=self.wrap) +
                html_quote(self.value, "") +
                "</textarea>")


    def parse (self, request):
        value = Widget.parse(self, request)
        if value:
            value = value.replace("\r\n", "\n")
            self.value = value
        return self.value


class CheckboxWidget (Widget):
    """Widget for a single checkbox: corresponds to "<input
    type=checkbox>".  Do not put multiple CheckboxWidgets with the same
    name in the same form.

    Instance attributes:
      value : boolean
    """

    widget_type = "checkbox"
    data_type = "boolean"

    def __init__ (self, name, value=None):
        Widget.__init__(self, name, value)

    def render (self, request):
        return render_tag("input",
                          type="checkbox",
                          name=self.name,
                          value="yes",
                          checked=self.value and ValuelessAttr or None)

    def parse (self, request):
        self.value = request.form.has_key(self.name)
        return self.value


class SelectWidget (Widget):
    """Widget for single or multiple selection; corresponds to
    <select name=...>
      <option value=foo>Foo
      ...
    </select>

    Instance attributes:
      allowed_values : [any]
      descriptions : [string]
      quote : boolean = true
      size : int
    """

    # NB. 'data_type' and 'widget_type' not set here because this is
    # an abstract class: they're set by subclasses SingleSelectWidget
    # and MultipleSelectWidget.

    def __init__ (self, name, value=None,
                  allowed_values=None,
                  descriptions=None,
                  quote=1,
                  size=None):
        assert self.__class__ is not SelectWidget, "abstract class"
        Widget.__init__(self, name, value)
        self.quote = quote
        self.set_allowed_values(allowed_values, descriptions)
        self.size = size


    def set_allowed_values (self, allowed_values, descriptions):
        assert type(allowed_values) in (ListType, TupleType), (
            "allowed_values for '%s' not a list: got %s" % (self.name,
                                                            `allowed_values`))
        self.allowed_values = allowed_values
        if descriptions is None:
            self.descriptions = []
            for v in self.allowed_values:
                if v is None:
                    v = ""
                else:   
                    v = str(v)
                self.descriptions.append(v)
        else:
            assert type(descriptions) in (ListType, TupleType), (
                "descriptions for '%s' not a list: got %s" % (self.name,
                                                              `descriptions`))
            assert len(self.allowed_values) == len(descriptions), (
                "allowed_values and descriptions must be the same length: "
                "not %s and %s" % (len(self.allowed_values),
                                   len(descriptions)))
            self.descriptions = descriptions
        if self.quote:
            self.descriptions = map(html_quote, self.descriptions)


    def is_selected (self, value):
        return value == self.value


    def render (self, request):
        if self.widget_type == "multiple_select":
            multiple = ValuelessAttr
        else:
            multiple = None
        if self.widget_type == "option_select":
            onchange = "submit()"
        else:
            onchange = None
        tags = [render_tag("select", name=self.name,
                           multiple=multiple, onchange=onchange,
                           size=self.size)]
        
        for i in range(len(self.allowed_values)):
            if (self.value is not None and
                self.is_selected(self.allowed_values[i])):
                selected = ValuelessAttr
            else:
                selected = None
            r = render_tag("option",
                           value=str(i),
                           selected=selected)
            tags.append(str(r) + self.descriptions[i])
        tags.append("</select>")
        return "\n".join(tags)


class SingleSelectWidget (SelectWidget):
    """Widget for single selection.

    Instance attributes:
      value : any
    """

    widget_type = "single_select"
    data_type = "any"

    def parse (self, request):
        value = request.form.get(self.name)
        self.value = None
        if value:
            if type(value) is ListType:
                raise FormValueError, "cannot select multiple values"
            try:
                index = int(value)
            except ValueError:
                pass
            else:
                if 0 <= index < len(self.allowed_values):
                    self.value = self.allowed_values[index]
        return self.value


class RadiobuttonsWidget (SingleSelectWidget):
    """Widget for a *set* of related radiobuttons -- all have the
    same name, but different values (and only one of those values
    is returned by the whole group).

    Instance attributes:
      value : any
      allowed_values : [any] or (any,)
        the list of possible radiobutton values; in the absence of bugs
        or mischievous clients, 'value' will be one of these.
      descriptions : [string]
        the user-visible radiobutton values: should correspond one-to-one
        to the elements of 'allowed_values', or be None or empty (in
        which case the user-visible values will be the same as the
        behind-the-scenes "real" values)
      quote : boolean = true
        if true, descriptions are HTML-quoted at render time.  If
        your descriptions are known to be HTML text already, set
        this to false
      delim : string = None
        string to emit between each radiobutton in the group.  If
        None, a single newline is emitted.
    """

    widget_type = "radiobuttons"
    data_type = "any"

    def __init__ (self, name, value=None,
                  allowed_values=None,
                  descriptions=None,
                  quote=1,
                  delim=None):
        Widget.__init__(self, name, value)
        self.quote = quote
        self.set_allowed_values(allowed_values, descriptions)
        self.delim = delim or "\n"


    def render (self, request):
        tags = []
        for i in range(len(self.allowed_values)):
            if self.value is not None and (
                self.allowed_values[i] is self.value or # XXX Not sure if this
                self.allowed_values[i] == self.value):  # is the right check
                checked = ValuelessAttr
            else:
                checked = None
            r = render_tag("input",
                           type="radio",
                           name=self.name,
                           value=str(i),
                           checked=checked)
            tags.append(str(r) + self.descriptions[i])
        return self.delim.join(tags)


class MultipleSelectWidget (SelectWidget):
    """Widget for multiple selection.

    Instance attributes:
      value : [any]
    """
        
    widget_type = "multiple_select"
    data_type = "[any]"

    def is_selected (self, value):
        if type(self.value) in (ListType, TupleType) and value in self.value:
            return 1
        return value == self.value


    def append_value (self, value):
        try:
            index = int(value)
        except ValueError:
            pass
        else:
            if 0 <= index < len(self.allowed_values):
                self.value.append(self.allowed_values[index])


    def parse (self, request):
        value = request.form.get(self.name)
        self.value = []
        if value:
            if type(value) is ListType:
                for val in value:
                    self.append_value(val)
            else:
                self.append_value(value)
        if not self.value:
            self.value = None

        return self.value


class SubmitButtonWidget (Widget):
    """
    Instance attributes:
      value : boolean
    """

    widget_type = "submit_button"
    data_type = "boolean"               # ??? whatever...

    def render (self, request):
        assert self.value is not None, "submit button must have a value"
        return render_tag("input",
                          type="submit",
                          name=self.name,
                          value=html_quote(self.value))


    def parse (self, request):
        return request.form.get(self.name)


class HiddenWidget (Widget):
    """
    Instance attributes:
      value : string
    """

    widget_type = "hidden"
    data_type = "string"

    def render (self, request):
        if self.value is None:
            value = None
        else:
            value = html_quote(self.value)
        return render_tag("input",
                          type="hidden",
                          name=self.name,
                          value=value)


# -- Derived widget types ----------------------------------------------
# (these don't correspond to fundamental widget types in HTML,
# so they're separated)

class NumberWidget (StringWidget):
    """
    Instance attributes: none
    """

    # Parameterize the number type (either float or int) through
    # these class attributes:
    type_object = None                  # eg. IntType, FloatType
    type_name = None                    # eg. "int", "float"
    type_converter = None               # eg int(), float() (builtin function)
    type_error = None                   # human-readable error message

    def __init__ (self, name,
                  value=None,
                  size=None, maxlength=None):
        assert self.__class__ is not NumberWidget, "abstract class"
        if value is not None:
            if type(value) is not self.type_object:
                raise TypeError, ("'value' not %s: %s" %
                                  (self.type_name, `value`) )
            value = str(value)
        StringWidget.__init__(self, name, value, size, maxlength)

    def parse (self, request):
        value = StringWidget.parse(self, request)
        if value:
            try:
                self.value = self.type_converter(value)
            except ValueError:
                raise FormValueError, self.type_error
        return self.value


class FloatWidget (NumberWidget):
    """
    Instance attributes:
      value : int
    """

    widget_type = "float"
    data_type = "float"
    type_object = FloatType
    type_name = "float"
    type_converter = float
    type_error = "must be a number"


class IntWidget (NumberWidget):
    """
    Instance attributes:
      value : float
    """

    widget_type = "int"
    data_type = "int"
    type_object = IntType
    type_name = "int"
    type_converter = int
    type_error = "must be an integer"


class OptionSelectWidget (SingleSelectWidget):
    """Widget for single selection with automatic submission.

    Instance attributes:
      value : any
    """

    widget_type = "option_select"
    data_type = "any"

    def __init__(self, *args, **kwargs):
        if kwargs.has_key("options"):
            kwargs["allowed_values"] = kwargs["options"]
            del kwargs["options"]
        SingleSelectWidget.__init__(self, *args, **kwargs)

        request = get_request()
        if request.form:
            SingleSelectWidget.parse(self, request)
        if self.value is None:
            self.value = self.allowed_values[0]

    def render (self, request):
        return (SingleSelectWidget.render(self, request) +
                '<noscript>'
                '<input type=submit name="" value=apply>'
                '</noscript>')

    def parse (self, request):
        return self.value

    def get_current_option (self):
        return self.value


class ListWidget (Widget):
    """Widget for lists of objects.

    Instance attributes:
      value : [any]
    """

    widget_type = "list"
    data_type = "[any]"

    def __init__ (self, name, value=None,
                  element_type=None,
                  element_name="row",
                  **args):
        assert value is None or type(value) is ListType, (
            "form value '%s' not a ListType: got %s" % (name,
                                                        `value`))
        assert type(element_name) is StringType, (
            "form value '%s' element_name not a string: "
            "got %s" % (name, `element_name`))
        
        Widget.__init__(self, name, value)

        if element_type is None:
            self.element_type = "string"
        else:
            self.element_type = element_type
        self.args = args

        self.added_elements_widget = self.create_subwidget(
            "hidden", "added_elements",
            value="1")
        self.add_button = self.create_subwidget(
            "submit_button", "add_element",
            value="Add %s" % element_name)
        
        self.element_widgets = []
        self.element_count = 0

        if self.value is not None:
            for element in self.value:
                self.add_element(element)

        request = get_request()
        
        self.added_elements = int(self.added_elements_widget.parse(request) or
                                  1)

        for i in range(self.added_elements):
            self.add_element()


    def add_element (self, value=None):
        self.element_widgets.append(
            self.create_subwidget(self.element_type,
                                  "element_%d" % self.element_count,
                                  value=value,
                                  **self.args))
        self.element_count += 1


    def render (self, request):
        tags = []
        for element_widget in self.element_widgets:
            tags.append(str(element_widget.render(request)))
        tags.append(self.add_button.render(request))
        tags.append(self.added_elements_widget.render(request))
        return '<br>\n'.join(tags)
        

    def parse (self, request):
        if self.add_button.parse(request):
            self.add_element()
            self.added_elements += 1
            self.added_elements_widget.value = str(self.added_elements)
        self.value = []
        for element_widget in self.element_widgets:
            value = element_widget.parse(request)
            if value is not None:
                self.value.append(value)
        self.value = self.value or None
        return self.value


