"""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.26 2002/09/20 22:45:39 nascheme 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
      - 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); widget type is a
    class attributes.

    Instance attributes:
      name : string

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

    # Subclasses must define.  'widget_type' is just a string, e.g.
    # "string", "text", "checkbox".
    widget_type = None

    def __init__ (self, name, value=None):
        assert self.__class__ is not Widget, "abstract class"
        self.set_name(name)
        self.set_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"""
        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"

    # 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", xml_end=1,
                          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"

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

    def render (self, request):
        return (render_tag("textarea", name=self.name,
                           cols=self.cols,
                           rows=self.rows) +
                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"

    def render (self, request):
        return render_tag("input", xml_end=1,
                          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</option>
      ...
    </select>

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

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

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


    def set_value (self, value):
        if value in self.allowed_values:
            self.value = value
        else:
            self.value = None


    def set_allowed_values (self, allowed_values, descriptions, sort=0):
        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)
        if sort:
            def compare(a, b):
                if a[0] is None:
                    return -1
                if b[0] is None:
                    return 1
                return cmp(a[0].lower(), b[0].lower())
            pairs = zip(self.descriptions, self.allowed_values)
            pairs.sort(compare)
            self.allowed_values = []
            self.descriptions = []
            for description, value in pairs:
                self.allowed_values.append(value)
                self.descriptions.append(description)


    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.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] + '</option>')
        tags.append("</select>")
        return "\n".join(tags)


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

    Instance attributes:
      value : any
    """

    widget_type = "single_select"

    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"

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

    def render (self, request):
        tags = []
        for i in range(len(self.allowed_values)):
            if self.is_selected(self.allowed_values[i]):
                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] +  '</input>')
        return self.delim.join(tags)


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

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

    def set_value (self, value):
        if value in self.allowed_values:
            self.value = [value]
        elif type(value) in (ListType, TupleType):
            self.value = [val for val in value
                          if val in self.allowed_values] or None
        else:
            self.value = None


    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"

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

    def render (self, request):
        value = (self.value and html_quote(self.value) or None)
        return render_tag("input", xml_end=1, type="submit",
                          name=self.name, value=value)
            
    def parse (self, request):
        return request.form.get(self.name)

    def is_submitted (self):
        return self.parse(get_request())

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

    widget_type = "hidden"

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

    def set_current_value (self, value):
        self.value = value
        request = get_request()
        if request.form:
            request.form[self.name] = value

    def get_current_value (self):
        request = get_request()
        if request.form:
            return self.parse(request)
        else:
            return self.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"
        assert value is None or type(value) is self.type_object, (
            "form value '%s' not a %s: got %s" % (name,
                                                  self.type_object,
                                                  `value`))
        StringWidget.__init__(self, name, value, size, maxlength)


    def set_value (self, value):
        if value is None:
            self.value = None
        else:
            self.value = str(value)


    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"
    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"
    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 and early
    parsing.  This widget parses the request when it is created.  This
    allows its value to be used to decide what other widgets need to be
    created in a form.  It's a powerful feature but it can be hard to
    understand what's going on.

    Instance attributes:
      value : any
    """

    widget_type = "option_select"

    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"

    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)

        self.added_elements = int(
            self.added_elements_widget.get_current_value() 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
