Source code for zope.formlib.textwidgets

##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Browser widgets with text-based input
"""
import decimal
from xml.sax import saxutils
from zope.interface import implementer
from zope.datetime import parseDatetimetz
from zope.datetime import DateTimeError
from zope.i18n.format import DateTimeParseError

from zope.formlib._compat import toUnicode, unicode, PY3
from zope.formlib.interfaces import ConversionError
from zope.formlib.i18n import _
from zope.formlib.interfaces import ITextBrowserWidget
from zope.formlib.widget import SimpleInputWidget, renderElement
from zope.formlib.widget import DisplayWidget


def escape(str):
    if str is not None:
        str = saxutils.escape(str)
    return str


[docs]@implementer(ITextBrowserWidget) class TextWidget(SimpleInputWidget): """Text widget. Single-line text (unicode) input >>> from zope.publisher.browser import TestRequest >>> from zope.schema import TextLine >>> field = TextLine(__name__='foo', title=u'on') >>> request = TestRequest(form={'field.foo': u'Bob'}) >>> widget = TextWidget(field, request) >>> widget.hasInput() True >>> widget.getInputValue() u'Bob' >>> def normalize(s): ... return '\\n '.join(filter(None, s.split(' '))) >>> print(normalize( widget() )) <input class="textType" id="field.foo" name="field.foo" size="20" type="text" value="Bob" /> >>> print(normalize( widget.hidden() )) <input class="hiddenType" id="field.foo" name="field.foo" type="hidden" value="Bob" /> Calling `setRenderedValue` will change what gets output: >>> widget.setRenderedValue("Barry") >>> print(normalize( widget() )) <input class="textType" id="field.foo" name="field.foo" size="20" type="text" value="Barry" /> Check that HTML is correctly encoded and decoded: >>> request = TestRequest( ... form={'field.foo': u'<h1>&copy;</h1>'}) >>> widget = TextWidget(field, request) >>> widget.getInputValue() u'<h1>&copy;</h1>' >>> print(normalize( widget() )) <input class="textType" id="field.foo" name="field.foo" size="20" type="text" value="&lt;h1&gt;&amp;copy;&lt;/h1&gt;" /> """ default = '' displayWidth = 20 displayMaxWidth = "" extra = '' style = '' convert_missing_value = True def __init__(self, *args): super(TextWidget, self).__init__(*args) def __call__(self): value = self._getFormValue() if value is None or value == self.context.missing_value: value = '' kwargs = {'type': self.type, 'name': self.name, 'id': self.name, 'value': value, 'cssClass': self.cssClass, 'style': self.style, 'size': self.displayWidth, 'extra': self.extra} if self.displayMaxWidth: # TODO This is untested. kwargs['maxlength'] = self.displayMaxWidth return renderElement(self.tag, **kwargs) def _toFieldValue(self, input): if self.convert_missing_value and input == self._missing: value = self.context.missing_value else: # We convert everything to unicode. This might seem a bit crude, # but anything contained in a TextWidget should be representable # as a string. Note that you always have the choice of overriding # the method. try: value = toUnicode(input) except ValueError as v: raise ConversionError(_("Invalid text data"), v) return value
[docs]class Text(SimpleInputWidget): def _toFieldValue(self, input): return super(Text, self)._toFieldValue(input)
[docs]class Bytes(SimpleInputWidget): def _toFieldValue(self, input): value = super(Bytes, self)._toFieldValue(input) if isinstance(value, unicode): try: value = value.encode('ascii') except UnicodeError as v: raise ConversionError(_("Invalid textual data"), v) return value
[docs]class BytesWidget(Bytes, TextWidget): """Bytes widget. Single-line data (string) input >>> from zope.publisher.browser import TestRequest >>> from zope.schema import BytesLine >>> field = BytesLine(__name__='foo', title=u'on') >>> request = TestRequest(form={'field.foo': u'Bob'}) >>> widget = BytesWidget(field, request) >>> widget.hasInput() True >>> widget.getInputValue() 'Bob' """
[docs]class BytesDisplayWidget(DisplayWidget): """Bytes display widget""" def __call__(self): if self._renderedValueSet(): content = self._data else: content = self.context.default return renderElement("pre", contents=escape(content))
# for things which are of the str type on both Python 2 and 3 if PY3: # pragma NO COVER NativeString = Text NativeStringWidget = TextWidget NativeStringDisplayWidget = DisplayWidget else: # pragma NO COVER NativeString = Bytes NativeStringWidget = BytesWidget NativeStringDisplayWidget = BytesDisplayWidget
[docs]class ASCII(NativeString): """ASCII"""
[docs]class ASCIIWidget(NativeStringWidget): """ASCII widget. Single-line data (string) input """
[docs]class ASCIIDisplayWidget(NativeStringDisplayWidget): """ASCII display widget"""
[docs]class URIDisplayWidget(DisplayWidget): """URI display widget. :ivar linkTarget: The value of the ``target`` attribute for the generated hyperlink. If this is not set, no ``target`` attribute is generated. """ linkTarget = None def __call__(self): if self._renderedValueSet(): content = self._data else: content = self.context.default if not content: # If there is no content it is not useful to render an anchor. return '' content = escape(content) kw = dict(contents=content, href=content) if self.linkTarget: kw["target"] = self.linkTarget return renderElement("a", **kw)
[docs]class TextAreaWidget(SimpleInputWidget): """TextArea widget. Multi-line text (unicode) input. >>> from zope.publisher.browser import TestRequest >>> from zope.schema import Text >>> field = Text(__name__='foo', title=u'on') >>> request = TestRequest(form={'field.foo': u'Hello\\r\\nworld!'}) >>> widget = TextAreaWidget(field, request) >>> widget.hasInput() True >>> widget.getInputValue() u'Hello\\nworld!' >>> def normalize(s): ... return '\\n '.join(filter(None, s.split(' '))) >>> print(normalize( widget() )) <textarea cols="60" id="field.foo" name="field.foo" rows="15" >Hello\r world!</textarea> >>> print(normalize( widget.hidden() )) <input class="hiddenType" id="field.foo" name="field.foo" type="hidden" value="Hello&#13;&#10;world!" /> Calling `setRenderedValue` will change what gets output: >>> widget.setRenderedValue("Hey\\ndude!") >>> print(normalize( widget() )) <textarea cols="60" id="field.foo" name="field.foo" rows="15" >Hey\r dude!</textarea> Check that HTML is correctly encoded and decoded: >>> request = TestRequest( ... form={'field.foo': u'<h1>&copy;</h1>'}) >>> widget = TextAreaWidget(field, request) >>> widget.getInputValue() u'<h1>&copy;</h1>' >>> print(normalize( widget() )) <textarea cols="60" id="field.foo" name="field.foo" rows="15" >&lt;h1&gt;&amp;copy;&lt;/h1&gt;</textarea> There was a but which caused the content of <textarea> tags not to be rendered correctly when there was a conversion error. Make sure the quoting works correctly:: >>> from zope.schema import Text >>> field = Text(__name__='description', title=u'Description') >>> from zope.formlib.interfaces import ConversionError >>> class TestTextAreaWidget(TextAreaWidget): ... def _toFieldValue(self, input): ... if 'foo' in input: ... raise ConversionError("I don't like foo.") ... return input ... >>> request = TestRequest(form={'field.description': u'<p>bar</p>'}) >>> widget = TestTextAreaWidget(field, request) >>> widget.getInputValue() u'<p>bar</p>' >>> print(normalize( widget() )) <textarea cols="60" id="field.description" name="field.description" rows="15" >&lt;p&gt;bar&lt;/p&gt;</textarea> >>> request = TestRequest(form={'field.description': u'<p>foo</p>'}) >>> widget = TestTextAreaWidget(field, request) >>> try: ... widget.getInputValue() ... except ConversionError as error: ... print(error.doc()) I don't like foo. >>> print(normalize( widget() )) <textarea cols="60" id="field.description" name="field.description" rows="15" >&lt;p&gt;foo&lt;/p&gt;</textarea> """ default = "" width = 60 height = 15 extra = "" style = '' def _toFieldValue(self, value): value = super(TextAreaWidget, self)._toFieldValue(value) if value: try: value = toUnicode(value) except ValueError as v: raise ConversionError(_("Invalid unicode data"), v) else: value = value.replace("\r\n", "\n") return value def _toFormValue(self, value): value = super(TextAreaWidget, self)._toFormValue(value) if value: value = value.replace("\n", "\r\n") else: value = u'' return value def __call__(self): return renderElement("textarea", name=self.name, id=self.name, cssClass=self.cssClass, rows=self.height, cols=self.width, style=self.style, contents=escape(self._getFormValue()), extra=self.extra)
[docs]class BytesAreaWidget(Bytes, TextAreaWidget): """BytesArea widget. Multi-line string input. >>> from zope.publisher.browser import TestRequest >>> from zope.schema import Bytes >>> field = Bytes(__name__='foo', title=u'on') >>> request = TestRequest(form={'field.foo': u'Hello\\r\\nworld!'}) >>> widget = BytesAreaWidget(field, request) >>> widget.hasInput() True >>> widget.getInputValue() 'Hello\\nworld!' """
[docs]class ASCIIAreaWidget(NativeString, TextAreaWidget): """ASCIIArea widget. Multi-line string input. >>> from zope.publisher.browser import TestRequest >>> from zope.schema import ASCII >>> field = ASCII(__name__='foo', title=u'on') >>> request = TestRequest(form={'field.foo': u'Hello\\r\\nworld!'}) >>> widget = ASCIIAreaWidget(field, request) >>> widget.hasInput() True >>> widget.getInputValue() 'Hello\\nworld!' """
[docs]class PasswordWidget(TextWidget): """Password Widget""" type = 'password' def __call__(self): displayMaxWidth = self.displayMaxWidth or 0 if displayMaxWidth > 0: return renderElement(self.tag, type=self.type, name=self.name, id=self.name, value='', cssClass=self.cssClass, style=self.style, size=self.displayWidth, maxlength=displayMaxWidth, extra=self.extra) else: return renderElement(self.tag, type=self.type, name=self.name, id=self.name, value='', cssClass=self.cssClass, style=self.style, size=self.displayWidth, extra=self.extra) def _toFieldValue(self, input): try: existing = self.context.get(self.context.context) except AttributeError: existing = False if (not input) and existing: return self.context.UNCHANGED_PASSWORD return super(PasswordWidget, self)._toFieldValue(input) def hidden(self): raise NotImplementedError( 'Cannot get a hidden tag for a password field')
[docs]class FileWidget(TextWidget): """File Widget""" type = 'file' def __call__(self): displayMaxWidth = self.displayMaxWidth or 0 hidden = renderElement(self.tag, type='hidden', name=self.name + ".used", id=self.name + ".used", value="") if displayMaxWidth > 0: elem = renderElement(self.tag, type=self.type, name=self.name, id=self.name, cssClass=self.cssClass, size=self.displayWidth, maxlength=displayMaxWidth, extra=self.extra) else: elem = renderElement(self.tag, type=self.type, name=self.name, id=self.name, cssClass=self.cssClass, size=self.displayWidth, extra=self.extra) return "%s %s" % (hidden, elem) def _toFieldValue(self, input): if input is None or input == '': return self.context.missing_value try: seek = input.seek read = input.read except AttributeError as e: raise ConversionError(_('Form input is not a file object'), e) else: seek(0) data = read() if data or getattr(input, 'filename', ''): return data else: return self.context.missing_value
[docs] def hasInput(self): return ((self.name + ".used" in self.request.form) or (self.name in self.request.form) )
[docs]class IntWidget(TextWidget): """Integer number widget. Let's make sure that zeroes are rendered properly: >>> from zope.schema import Int >>> field = Int(__name__='foo', title=u'on') >>> widget = IntWidget(field, None) >>> widget.setRenderedValue(0) >>> 'value="0"' in widget() True """ displayWidth = 10 def _toFieldValue(self, input): if input == self._missing: return self.context.missing_value else: try: return int(input) except ValueError as v: raise ConversionError(_("Invalid integer data"), v)
[docs]class FloatWidget(TextWidget): displayWidth = 10 def _toFieldValue(self, input): if input == self._missing: return self.context.missing_value else: try: return float(input) except ValueError as v: raise ConversionError(_("Invalid floating point data"), v)
[docs]class DecimalWidget(TextWidget): displayWidth = 10 def _toFieldValue(self, input): if input == self._missing: return self.context.missing_value else: try: return decimal.Decimal(input) except decimal.InvalidOperation as v: raise ConversionError(_("Invalid decimal data"), v) def _toFormValue(self, value): if value == self.context.missing_value: value = self._missing else: return toUnicode(value)
[docs]class DatetimeWidget(TextWidget): """Datetime entry widget.""" displayWidth = 20 def _toFieldValue(self, input): if input == self._missing: return self.context.missing_value else: try: # TODO: Currently datetimes return in local (server) # time zone if no time zone information was given. # Maybe offset-naive datetimes should be returned in # this case? (DV) return parseDatetimetz(input) except (DateTimeError, ValueError, IndexError) as v: raise ConversionError(_("Invalid datetime data"), v)
[docs]class DateWidget(DatetimeWidget): """Date entry widget. """ def _toFieldValue(self, input): v = super(DateWidget, self)._toFieldValue(input) if v != self.context.missing_value: v = v.date() return v
[docs]class DateI18nWidget(TextWidget): """I18n date entry widget. The `displayStyle` attribute may be set to control the formatting of the value. `displayStyle` must be one of 'full', 'long', 'medium', 'short', or None ('' is accepted an an alternative to None to support provision of a value from ZCML). """ _category = "date" displayWidth = 20 displayStyle = None def _toFieldValue(self, input): if input == self._missing: return self.context.missing_value else: try: formatter = self.request.locale.dates.getFormatter( self._category, (self.displayStyle or None)) return formatter.parse(input) except (DateTimeParseError, ValueError) as v: raise ConversionError(_("Invalid datetime data"), "%s (%r)" % (v, input)) def _toFormValue(self, value): value = super(DateI18nWidget, self)._toFormValue(value) if value: formatter = self.request.locale.dates.getFormatter( self._category, (self.displayStyle or None)) value = formatter.format(value) return value
[docs]class DatetimeI18nWidget(DateI18nWidget): """I18n datetime entry widget. The `displayStyle` attribute may be set to control the formatting of the value. `displayStyle` must be one of 'full', 'long', 'medium', 'short', or None ('' is accepted an an alternative to None to support provision of a value from ZCML). NOTE: If you need timezone information you need to set `displayStyle` to either 'long' or 'full' since other display styles just ignore it. """ _category = "dateTime"
[docs]class DateDisplayWidget(DisplayWidget): """Date display widget. The `cssClass` and `displayStyle` attributes may be set to control the formatting of the value. `displayStyle` must be one of 'full', 'long', 'medium', 'short', or None ('' is accepted an an alternative to None to support provision of a value from ZCML). """ cssClass = "date" displayStyle = None _category = "date" def __call__(self): if self._renderedValueSet(): content = self._data else: content = self.context.default if content == self.context.missing_value: return "" formatter = self.request.locale.dates.getFormatter( self._category, (self.displayStyle or None)) content = formatter.format(content) return renderElement("span", contents=escape(content), cssClass=self.cssClass)
[docs]class DatetimeDisplayWidget(DateDisplayWidget): """Datetime display widget. The `cssClass` and `displayStyle` attributes may be set to control the formatting of the value. `displayStyle` must be one of 'full', 'long', 'medium', 'short', or None ('' is accepted an an alternative to None to support provision of a value from ZCML). """ cssClass = "dateTime" _category = "dateTime"