web/formwidgets.py
author Aurelien Campeas <aurelien.campeas@logilab.fr>
Wed, 14 Apr 2010 16:57:55 +0200
branchstable
changeset 5258 9ff357e82e36
parent 5111 9f3ea34f98d1
child 5367 4176a50c81c9
permissions -rw-r--r--
[doc/book] add a better standard theme

"""widget classes for form construction

:organization: Logilab
:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"

from datetime import date
from warnings import warn

from logilab.mtconverter import xml_escape
from logilab.common.deprecation import deprecated
from logilab.common.date import todatetime

from cubicweb import tags, uilib
from cubicweb.web import stdmsgs, INTERNAL_FIELD_VALUE, ProcessFormError


class FieldWidget(object):
    """abstract widget class"""
    # javascript / css files required by the widget
    needs_js = ()
    needs_css = ()
    # automatically set id and tabindex attributes ?
    setdomid = True
    settabindex = True
    # to ease usage as a sub-widgets (eg widget used by another widget)
    suffix = None
    # does this widget expect a vocabulary
    vocabulary_widget = False

    def __init__(self, attrs=None, setdomid=None, settabindex=None, suffix=None):
        if attrs is None:
            attrs = {}
        self.attrs = attrs
        if setdomid is not None:
            # override class's default value
            self.setdomid = setdomid
        if settabindex is not None:
            # override class's default value
            self.settabindex = settabindex
        if suffix is not None:
            self.suffix = suffix

    def add_media(self, form):
        """adds media (CSS & JS) required by this widget"""
        if self.needs_js:
            form._cw.add_js(self.needs_js)
        if self.needs_css:
            form._cw.add_css(self.needs_css)


    def render(self, form, field, renderer=None):
        self.add_media(form)
        return self._render(form, field, renderer)

    def _render(self, form, field, renderer):
        raise NotImplementedError()

    def format_value(self, form, field, value):
        return field.format_value(form._cw, value)

    def values_and_attributes(self, form, field):
        """found field's *string* value in:
        1. previously submitted form values if any (eg on validation error)
        2. req.form
        3. extra form values given to render()
        4. field's typed value

        values found in 1. and 2. are expected te be already some 'display'
        value while those found in 3. and 4. are expected to be correctly typed.

        3 and 4 are handle by the .typed_value(form, field) method
        """
        attrs = dict(self.attrs)
        if self.setdomid:
            attrs['id'] = field.dom_id(form, self.suffix)
        if self.settabindex and not 'tabindex' in attrs:
            attrs['tabindex'] = form._cw.next_tabindex()
        return self.values(form, field), attrs

    def values(self, form, field):
        values = None
        if not field.ignore_req_params:
            qname = field.input_name(form, self.suffix)
            # value from a previous post that has raised a validation error
            if qname in form.form_previous_values:
                values = form.form_previous_values[qname]
            # value specified using form parameters
            elif qname in form._cw.form:
                values = form._cw.form[qname]
            elif field.name != qname and field.name in form._cw.form:
                # XXX compat: accept attr=value in req.form to specify value of
                # attr-subject
                values = form._cw.form[field.name]
        if values is None:
            values = self.typed_value(form, field)
            if values != INTERNAL_FIELD_VALUE:
                values = self.format_value(form, field, values)
        if not isinstance(values, (tuple, list)):
            values = (values,)
        return values

    def typed_value(self, form, field):
        """return field's *typed* value specified in:
        3. extra form values given to render()
        4. field's typed value
        """
        qname = field.input_name(form)
        for key in ((field, form), qname):
            try:
                return form.formvalues[key]
            except KeyError:
                continue
        if field.name != qname and field.name in form.formvalues:
            return form.formvalues[field.name]
        return field.typed_value(form)

    def process_field_data(self, form, field):
        posted = form._cw.form
        val = posted.get(field.input_name(form, self.suffix))
        if isinstance(val, basestring):
            val = val.strip()
        return val

    @deprecated('[3.6] use values_and_attributes')
    def _render_attrs(self, form, field):
        """return html tag name, attributes and a list of values for the field
        """
        values, attrs = self.values_and_attributes(form, field)
        return field.input_name(form, self.suffix), values, attrs


class Input(FieldWidget):
    """abstract widget class for <input> tag based widgets"""
    type = None

    def _render(self, form, field, renderer):
        """render the widget for the given `field` of `form`.

        Generate one <input> tag for each field's value
        """
        values, attrs = self.values_and_attributes(form, field)
        # ensure something is rendered
        if not values:
            values = (INTERNAL_FIELD_VALUE,)
        inputs = [tags.input(name=field.input_name(form, self.suffix),
                             type=self.type, value=value, **attrs)
                  for value in values]
        return u'\n'.join(inputs)


# basic html widgets ###########################################################

class TextInput(Input):
    """<input type='text'>"""
    type = 'text'


class PasswordInput(Input):
    """<input type='password'> and its confirmation field (using
    <field's name>-confirm as name)
    """
    type = 'password'

    def _render(self, form, field, renderer):
        assert self.suffix is None, 'suffix not supported'
        values, attrs = self.values_and_attributes(form, field)
        assert len(values) == 1
        domid = attrs.pop('id')
        inputs = [tags.input(name=field.input_name(form),
                             value=values[0], type=self.type, id=domid, **attrs),
                  '<br/>',
                  tags.input(name=field.input_name(form, '-confirm'),
                             value=values[0], type=self.type, **attrs),
                  '&#160;', tags.span(form._cw._('confirm password'),
                                      **{'class': 'emphasis'})]
        return u'\n'.join(inputs)

    def process_field_data(self, form, field):
        passwd1 = super(PasswordInput, self).process_field_data(form, field)
        passwd2 = form._cw.form.get(field.input_name(form, '-confirm'))
        if passwd1 == passwd2:
            if passwd1 is None:
                return None
            return passwd1.encode('utf-8')
        raise ProcessFormError(form._cw._("password and confirmation don't match"))


class PasswordSingleInput(Input):
    """<input type='password'> without a confirmation field"""
    type = 'password'

    def process_field_data(self, form, field):
        value = super(PasswordSingleInput, self).process_field_data(form, field)
        if value is not None:
            return value.encode('utf-8')
        return value


class FileInput(Input):
    """<input type='file'>"""
    type = 'file'

    def values_and_attributes(self, form, field):
        # ignore value which makes no sense here (XXX even on form validation error?)
        values, attrs = super(FileInput, self).values_and_attributes(form, field)
        return ('',), attrs


class HiddenInput(Input):
    """<input type='hidden'>"""
    type = 'hidden'
    setdomid = False # by default, don't set id attribute on hidden input
    settabindex = False


class ButtonInput(Input):
    """<input type='button'>

    if you want a global form button, look at the Button, SubmitButton,
    ResetButton and ImgButton classes below.
    """
    type = 'button'


class TextArea(FieldWidget):
    """<textarea>"""

    def _render(self, form, field, renderer):
        values, attrs = self.values_and_attributes(form, field)
        attrs.setdefault('onkeyup', 'autogrow(this)')
        if not values:
            value = u''
        elif len(values) == 1:
            value = values[0]
        else:
            raise ValueError('a textarea is not supposed to be multivalued')
        lines = value.splitlines()
        linecount = len(lines)
        for line in lines:
            linecount += len(line) / 80
        attrs.setdefault('cols', 80)
        attrs.setdefault('rows', min(15, linecount + 2))
        return tags.textarea(value, name=field.input_name(form, self.suffix),
                             **attrs)


class FCKEditor(TextArea):
    """FCKEditor enabled <textarea>"""
    def __init__(self, *args, **kwargs):
        super(FCKEditor, self).__init__(*args, **kwargs)
        self.attrs['cubicweb:type'] = 'wysiwyg'

    def _render(self, form, field, renderer):
        form._cw.fckeditor_config()
        return super(FCKEditor, self)._render(form, field, renderer)


class Select(FieldWidget):
    """<select>, for field having a specific vocabulary"""
    vocabulary_widget = True

    def __init__(self, attrs=None, multiple=False, **kwargs):
        super(Select, self).__init__(attrs, **kwargs)
        self._multiple = multiple

    def render(self, form, field, renderer):
        curvalues, attrs = self.values_and_attributes(form, field)
        if not 'size' in attrs:
            attrs['size'] = self._multiple and '5' or '1'
        options = []
        optgroup_opened = False
        for option in field.vocabulary(form):
            try:
                label, value, oattrs = option
            except ValueError:
                label, value = option
                oattrs = {}
            if value is None:
                # handle separator
                if optgroup_opened:
                    options.append(u'</optgroup>')
                oattrs.setdefault('label', label or '')
                options.append(u'<optgroup %s>' % uilib.sgml_attributes(oattrs))
                optgroup_opened = True
            elif value in curvalues:
                options.append(tags.option(label, value=value,
                                           selected='selected', **oattrs))
            else:
                options.append(tags.option(label, value=value, **oattrs))
        if optgroup_opened:
            options.append(u'</optgroup>')
        return tags.select(name=field.input_name(form, self.suffix),
                           multiple=self._multiple, options=options, **attrs)


class CheckBox(Input):
    """<input type='checkbox'>, for field having a specific vocabulary. One
    input will be generated for each possible value.
    """
    type = 'checkbox'
    vocabulary_widget = True

    def render(self, form, field, renderer):
        curvalues, attrs = self.values_and_attributes(form, field)
        domid = attrs.pop('id', None)
        sep = attrs.pop('separator', u'<br/>\n')
        options = []
        for i, option in enumerate(field.vocabulary(form)):
            try:
                label, value, oattrs = option
            except ValueError:
                label, value = option
                oattrs = {}
            iattrs = attrs.copy()
            iattrs.update(oattrs)
            if i == 0 and domid is not None:
                iattrs.setdefault('id', domid)
            if value in curvalues:
                iattrs['checked'] = u'checked'
            tag = tags.input(name=field.input_name(form, self.suffix),
                             type=self.type, value=value, **iattrs)
            options.append(u'%s&#160;%s' % (tag, label))
        return sep.join(options)


class Radio(CheckBox):
    """<input type='radio'>, for field having a specific vocabulary. One
    input will be generated for each possible value.
    """
    type = 'radio'


# compound widgets #############################################################

class IntervalWidget(FieldWidget):
    """custom widget to display an interval composed by 2 fields. This widget
    is expected to be used with a CompoundField containing the two actual
    fields.

    Exemple usage::

from uicfg import autoform_field, autoform_section
autoform_field.tag_attribute(('Concert', 'minprice'),
                              CompoundField(fields=(IntField(name='minprice'),
                                                    IntField(name='maxprice')),
                                            label=_('price'),
                                            widget=IntervalWidget()
                                            ))
# we've to hide the other field manually for now
autoform_section.tag_attribute(('Concert', 'maxprice'), 'generated')
    """
    def render(self, form, field, renderer):
        actual_fields = field.fields
        assert len(actual_fields) == 2
        return u'<div>%s %s %s %s</div>' % (
            form._cw._('from_interval_start'),
            actual_fields[0].render(form, renderer),
            form._cw._('to_interval_end'),
            actual_fields[1].render(form, renderer),
            )


class HorizontalLayoutWidget(FieldWidget):
    """custom widget to display a set of fields grouped together horizontally
    in a form. See `IntervalWidget` for example usage.
    """
    def render(self, form, field, renderer):
        if self.attrs.get('display_label', True):
            subst = self.attrs.get('label_input_substitution', '%(label)s %(input)s')
            fields = [subst % {'label': renderer.render_label(form, f),
                              'input': f.render(form, renderer)}
                      for f in field.subfields(form)]
        else:
            fields = [f.render(form, renderer) for f in field.subfields(form)]
        return u'<div>%s</div>' % ' '.join(fields)


# javascript widgets ###########################################################

class DateTimePicker(TextInput):
    """<input type='text' + javascript date/time picker for date or datetime
    fields
    """
    monthnames = ('january', 'february', 'march', 'april',
                  'may', 'june', 'july', 'august',
                  'september', 'october', 'november', 'december')
    daynames = ('monday', 'tuesday', 'wednesday', 'thursday',
                'friday', 'saturday', 'sunday')

    needs_js = ('cubicweb.calendar.js',)
    needs_css = ('cubicweb.calendar_popup.css',)

    @classmethod
    def add_localized_infos(cls, req):
        """inserts JS variables defining localized months and days"""
        # import here to avoid dependancy from cubicweb to simplejson
        _ = req._
        monthnames = [_(mname) for mname in cls.monthnames]
        daynames = [_(dname) for dname in cls.daynames]
        req.html_headers.define_var('MONTHNAMES', monthnames)
        req.html_headers.define_var('DAYNAMES', daynames)

    def render(self, form, field, renderer):
        txtwidget = super(DateTimePicker, self).render(form, field, renderer)
        self.add_localized_infos(form._cw)
        cal_button = self._render_calendar_popup(form, field)
        return txtwidget + cal_button

    def _render_calendar_popup(self, form, field):
        value = field.typed_value(form)
        if not value:
            value = date.today()
        inputid = field.dom_id(form)
        helperid = '%shelper' % inputid
        year, month = value.year, value.month
        return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
<img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
                % (helperid, inputid, year, month,
                   form._cw.external_resource('CALENDAR_ICON'),
                   form._cw._('calendar'), helperid) )


class JQueryDatePicker(FieldWidget):
    """use jquery.ui.datepicker to define a date time picker"""
    needs_js = ('jquery.ui.js', )
    needs_css = ('jquery.ui.css',)

    def __init__(self, datestr=None, **kwargs):
        super(JQueryDatePicker, self).__init__(**kwargs)
        self.datestr = datestr

    def _render(self, form, field, renderer):
        req = form._cw
        domid = field.dom_id(form, self.suffix)
        # XXX find a way to understand every format
        fmt = req.property_value('ui.date-format')
        fmt = fmt.replace('%Y', 'yy').replace('%m', 'mm').replace('%d', 'dd')
        req.add_onload(u'jqNode("%s").datepicker('
                       '{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
                       ' showOn: "button", buttonImageOnly: true})' % (
                           domid, req.external_resource('CALENDAR_ICON'), fmt))
        if self.datestr is None:
            value = self.values(form, field)[0]
        else:
            value = self.datestr
        return tags.input(id=domid, name=domid, value=value,
                          type='text', size='10')


class JQueryTimePicker(FieldWidget):
    """use jquery.timePicker.js to define a js time picker"""
    needs_js = ('jquery.timePicker.js',)
    needs_css = ('jquery.timepicker.css',)

    def __init__(self, timestr=None, timesteps=30, separator=u':', **kwargs):
        super(JQueryTimePicker, self).__init__(**kwargs)
        self.timestr = timestr
        self.timesteps = timesteps
        self.separator = separator

    def _render(self, form, field, renderer):
        req = form._cw
        domid = field.dom_id(form, self.suffix)
        req.add_onload(u'jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})' % (
            domid, self.timestr, self.timesteps, self.separator))
        if self.timestr is None:
            value = self.values(form, field)[0]
        else:
            value = self.timestr
        return tags.input(id=domid, name=domid, value=value,
                          type='text', size='5')


class JQueryDateTimePicker(FieldWidget):
    def __init__(self, initialtime=None, timesteps=15, **kwargs):
        super(JQueryDateTimePicker, self).__init__(**kwargs)
        self.initialtime = initialtime
        self.timesteps = timesteps

    def _render(self, form, field, renderer):
        """render the widget for the given `field` of `form`.

        Generate one <input> tag for each field's value
        """
        req = form._cw
        dateqname = field.input_name(form, 'date')
        timeqname = field.input_name(form, 'time')
        if dateqname in form.form_previous_values:
            datestr = form.form_previous_values[dateqname]
            timestr = form.form_previous_values[timeqname]
        else:
            datestr = timestr = u''
            if field.name in req.form:
                value = req.parse_datetime(req.form[field.name])
            else:
                value = self.typed_value(form, field)
            if value:
                datestr = req.format_date(value)
                timestr = req.format_time(value)
            elif self.initialtime:
                timestr = req.format_time(self.initialtime)
        datepicker = JQueryDatePicker(datestr=datestr, suffix='date')
        timepicker = JQueryTimePicker(timestr=timestr, timesteps=self.timesteps,
                                      suffix='time')
        return u'<div id="%s">%s%s</div>' % (field.dom_id(form),
                                            datepicker.render(form, field),
                                            timepicker.render(form, field))

    def process_field_data(self, form, field):
        req = form._cw
        datestr = req.form.get(field.input_name(form, 'date')).strip() or None
        timestr = req.form.get(field.input_name(form, 'time')).strip() or None
        if datestr is None:
            return None
        date = todatetime(req.parse_datetime(datestr, 'Date'))
        if timestr is None:
            return date
        time = req.parse_datetime(timestr, 'Time')
        return date.replace(hour=time.hour, minute=time.minute, second=time.second)


# ajax widgets ################################################################

def init_ajax_attributes(attrs, wdgtype, loadtype=u'auto'):
    try:
        attrs['class'] += u' widget'
    except KeyError:
        attrs['class'] = u'widget'
    attrs.setdefault('cubicweb:wdgtype', wdgtype)
    attrs.setdefault('cubicweb:loadtype', loadtype)


class AjaxWidget(FieldWidget):
    """simple <div> based ajax widget"""
    def __init__(self, wdgtype, inputid=None, **kwargs):
        super(AjaxWidget, self).__init__(**kwargs)
        init_ajax_attributes(self.attrs, wdgtype)
        if inputid is not None:
            self.attrs['cubicweb:inputid'] = inputid

    def _render(self, form, field, renderer):
        attrs = self.values_and_attributes(form, field)[-1]
        return tags.div(**attrs)


class AutoCompletionWidget(TextInput):
    """ajax widget for StringField, proposing matching existing values as you
    type.
    """
    needs_js = ('cubicweb.widgets.js', 'jquery.autocomplete.js')
    needs_css = ('jquery.autocomplete.css',)
    wdgtype = 'SuggestField'
    loadtype = 'auto'

    def __init__(self, *args, **kwargs):
        try:
            self.autocomplete_initfunc = kwargs.pop('autocomplete_initfunc')
        except KeyError:
            warn('use autocomplete_initfunc argument of %s constructor '
                 'instead of relying on autocomplete_initfuncs dictionary on '
                 'the entity class' % self.__class__.__name__,
                 DeprecationWarning)
            self.autocomplete_initfunc = None
        super(AutoCompletionWidget, self).__init__(*args, **kwargs)

    def values_and_attributes(self, form, field):
        values, attrs = super(AutoCompletionWidget, self).values_and_attributes(form, field)
        init_ajax_attributes(attrs, self.wdgtype, self.loadtype)
        # XXX entity form specific
        attrs['cubicweb:dataurl'] = self._get_url(form.edited_entity, field)
        if not values:
            values = ('',)
        return values, attrs

    def _get_url(self, entity, field):
        if self.autocomplete_initfunc is None:
            # XXX for bw compat
            fname = entity.autocomplete_initfuncs[field.name]
        else:
            fname = self.autocomplete_initfunc
        return entity._cw.build_url('json', fname=fname, mode='remote',
                                    pageid=entity._cw.pageid)


class StaticFileAutoCompletionWidget(AutoCompletionWidget):
    """XXX describe me"""
    wdgtype = 'StaticFileSuggestField'

    def _get_url(self, entity, field):
        if self.autocomplete_initfunc is None:
            # XXX for bw compat
            fname = entity.autocomplete_initfuncs[field.name]
        else:
            fname = self.autocomplete_initfunc
        return entity._cw.datadir_url + fname


class RestrictedAutoCompletionWidget(AutoCompletionWidget):
    """XXX describe me"""
    wdgtype = 'RestrictedSuggestField'


class LazyRestrictedAutoCompletionWidget(RestrictedAutoCompletionWidget):
    """remote autocomplete """
    wdgtype = 'LazySuggestField'

    def values_and_attributes(self, form, field):
        self.add_media(form)

        """override values_and_attributes to handle initial displayed values"""
        values, attrs = super(LazyRestrictedAutoCompletionWidget, self).values_and_attributes(form, field)
        assert len(values) == 1, "multiple selection is not supported yet by LazyWidget"
        if not values[0]:
            values = form.cw_extra_kwargs.get(field.name,'')
            if not isinstance(values, (tuple, list)):
                values = (values,)
        try:
            values = list(values)
            values[0] = int(values[0])
            attrs['cubicweb:initialvalue'] = values[0]
            values = (self.display_value_for(form, values[0]),)
        except (TypeError, ValueError):
            pass
        return values, attrs

    def display_value_for(self, form, value):
        entity =form._cw.entity_from_eid(value)
        return entity.view('combobox')


class AddComboBoxWidget(Select):
    def values_and_attributes(self, form, field):
        values, attrs = super(AddComboBoxWidget, self).values_and_attributes(form, field)
        init_ajax_attributes(self.attrs, 'AddComboBox')
        # XXX entity form specific
        entity = form.edited_entity
        attrs['cubicweb:etype_to'] = entity.e_schema
        etype_from = entity.e_schema.subjrels[field.name].objects(entity.e_schema)[0]
        attrs['cubicweb:etype_from'] = etype_from
        return values, attrs

    def render(self, form, field, renderer):
        return super(AddComboBoxWidget, self).render(form, field, renderer) + u'''
<div id="newvalue">
  <input type="text" id="newopt" />
  <a href="javascript:noop()" id="add_newopt">&#160;</a></div>
'''

# buttons ######################################################################

class Button(Input):
    """<input type='button'>, base class for global form buttons

    note label is a msgid which will be translated at form generation time, you
    should not give an already translated string.
    """
    type = 'button'
    def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None,
                 setdomid=None, settabindex=None,
                 name='', value='', onclick=None, cwaction=None):
        super(Button, self).__init__(attrs, setdomid, settabindex)
        if isinstance(label, tuple):
            self.label = label[0]
            self.icon = label[1]
        else:
            self.label = label
            self.icon = None
        self.name = name
        self.value = ''
        self.onclick = onclick
        self.cwaction = cwaction
        self.attrs.setdefault('class', 'validateButton')

    def render(self, form, field=None, renderer=None):
        label = form._cw._(self.label)
        attrs = self.attrs.copy()
        if self.cwaction:
            assert self.onclick is None
            attrs['onclick'] = "postForm('__action_%s', \'%s\', \'%s\')" % (
                self.cwaction, self.label, form.domid)
        elif self.onclick:
            attrs['onclick'] = self.onclick
        if self.name:
            attrs['name'] = self.name
            if self.setdomid:
                attrs['id'] = self.name
        if self.settabindex and not 'tabindex' in attrs:
            attrs['tabindex'] = form._cw.next_tabindex()
        if self.icon:
            img = tags.img(src=form._cw.external_resource(self.icon), alt=self.icon)
        else:
            img = u''
        return tags.button(img + xml_escape(label), escapecontent=False,
                           value=label, type=self.type, **attrs)


class SubmitButton(Button):
    """<input type='submit'>, main button to submit a form"""
    type = 'submit'


class ResetButton(Button):
    """<input type='reset'>, main button to reset a form.
    You usually don't want this.
    """
    type = 'reset'


class ImgButton(object):
    """<img> wrapped into a <a> tag with href triggering something (usually a
    javascript call)

    note label is a msgid which will be translated at form generation time, you
    should not give an already translated string.
    """
    def __init__(self, domid, href, label, imgressource):
        self.domid = domid
        self.href = href
        self.imgressource = imgressource
        self.label = label

    def render(self, form, field=None, renderer=None):
        label = form._cw._(self.label)
        imgsrc = form._cw.external_resource(self.imgressource)
        return '<a id="%(domid)s" href="%(href)s">'\
               '<img src="%(imgsrc)s" alt="%(label)s"/>%(label)s</a>' % {
            'label': label, 'imgsrc': imgsrc,
            'domid': self.domid, 'href': self.href}


# more widgets #################################################################

class EditableURLWidget(FieldWidget):
    """custom widget to edit separatly an url path / query string (used by
    default for Bookmark.path for instance), dealing with url quoting nicely
    (eg user edit the unquoted value).
    """

    def _render(self, form, field, renderer):
        """render the widget for the given `field` of `form`.

        Generate one <input> tag for each field's value
        """
        assert self.suffix is None, 'not supported'
        req = form._cw
        pathqname = field.input_name(form, 'path')
        fqsqname = field.input_name(form, 'fqs') # formatted query string
        if pathqname in form.form_previous_values:
            path = form.form_previous_values[pathqname]
            fqs = form.form_previous_values[fqsqname]
        else:
            if field.name in req.form:
                value = req.form[field.name]
            else:
                value = self.typed_value(form, field)
            if value:
                try:
                    path, qs = value.split('?', 1)
                except ValueError:
                    path = value
                    qs = ''
            else:
                path = qs = ''
            fqs = u'\n'.join(u'%s=%s' % (k, v) for k, v in req.url_parse_qsl(qs))
        attrs = dict(self.attrs)
        if self.setdomid:
            attrs['id'] = field.dom_id(form)
        if self.settabindex and not 'tabindex' in attrs:
            attrs['tabindex'] = req.next_tabindex()
        # ensure something is rendered
        inputs = [u'<table><tr><th>',
                  req._('i18n_bookmark_url_path'),
                  u'</th><td>',
                  tags.input(name=pathqname, type='string', value=path, **attrs),
                  u'</td></tr><tr><th>',
                  req._('i18n_bookmark_url_fqs'),
                  u'</th><td>']
        if self.setdomid:
            attrs['id'] = field.dom_id(form, 'fqs')
        if self.settabindex:
            attrs['tabindex'] = req.next_tabindex()
        attrs.setdefault('cols', 60)
        attrs.setdefault('onkeyup', 'autogrow(this)')
        inputs += [tags.textarea(fqs, name=fqsqname, **attrs),
                   u'</td></tr></table>']
        # surrounding div necessary for proper error localization
        return u'<div id="%s">%s</div>' % (
            field.dom_id(form), u'\n'.join(inputs))

    def process_field_data(self, form, field):
        req = form._cw
        values = {}
        path = req.form.get(field.input_name(form, 'path'))
        if isinstance(path, basestring):
            path = path.strip() or None
        fqs = req.form.get(field.input_name(form, 'fqs'))
        if isinstance(fqs, basestring):
            fqs = fqs.strip() or None
            if fqs:
                for i, line in enumerate(fqs.split('\n')):
                    line = line.strip()
                    if line:
                        try:
                            key, val = line.split('=', 1)
                        except ValueError:
                            raise ProcessFormError(req._("wrong query parameter line %s") % (i+1))
                        # value will be url quoted by build_url_params
                        values.setdefault(key.encode(req.encoding), []).append(val)
        if not values:
            return path
        return u'%s?%s' % (path, req.build_url_params(**values))