"""
open/DurusWorks/qp/fill/form.qpy
"""
from qp.fill.html import htmltag
from qp.fill.widget import CheckboxWidget, SingleSelectWidget, IntWidget
from qp.fill.widget import HiddenWidget, StringWidget, TextWidget
from qp.fill.widget import PasswordWidget, subname, FileWidget
from qp.fill.widget import RadiobuttonsWidget, MultipleSelectWidget
from qp.fill.widget import SubmitWidget, FloatWidget, ResetWidget
from qp.fill.widget import OptionSelectWidget, ButtonWidget, ButtonSubmitWidget
from qp.pub.common import get_request, get_hit, get_user, get_fields
from qpy import xml
def get_javascript_code():
if 'javascript_code' not in get_hit().get_info():
get_hit().get_info()['javascript_code'] = {}
return get_hit().get_info().get('javascript_code')
def add_javascript_code(name, javascript):
if 'javascript_code' not in get_hit().get_info():
get_hit().get_info()['javascript_code'] = {}
get_hit().get_info()['javascript_code'][name] = javascript
class FormTokenWidget(HiddenWidget):
def _parse(self, request):
token = request.get_field(self.name)
tokens = get_user().get_tokens()
if token not in tokens:
self.error = 'invalid'
else:
tokens.remove(token)
def render_error(self, error):
return ''
def render(self):
self.value = get_user().get_tokens().new_token()
return HiddenWidget.render(self)
class Form(object):
"""
Provides a high-level mechanism for collecting and processing user
input that is based on HTML forms.
Instance attributes:
widgets : [Widget]
widgets that are not subclasses of SubmitWidget or HiddenWidget
submit_widgets : [SubmitWidget]
subclasses of SubmitWidget, normally rendered at the end of the
form
hidden_widgets : [HiddenWidget]
subclasses of HiddenWidget, normally rendered at the beginning
of the form
_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 = xml('\n')
TOKEN_NOTICE = xml(
'
'
'The form you have submitted has already been submitted once. '
'Please review the form data and submit the form again.'
'
')
ERROR_NOTICE = xml(
''
'There were errors processing your form.
'
'See details below.'
'
')
def __init__(self,
method="post",
action=None,
enctype=None,
use_tokens=True,
prefix=None,
log_errors=False,
**attrs):
if method not in ("post", "get"):
raise ValueError("Form method must be 'post' or 'get', "
"not %r" % method)
self.method = method
self.action = action or self._get_default_action()
if 'class' not in attrs:
attrs['class'] = 'qp'
self.prefix = prefix or attrs.get('id')
self.attrs = attrs
self.log_errors = log_errors
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
if use_tokens and self.method == "post":
# 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)
def _get_default_action(self):
return get_request().get_path_query()
def __getitem__(self, name):
"""(name:string) -> any
Return a widget's value. Raises KeyError if widget named 'name'
does not exist.
"""
try:
return self._names[name].parse()
except KeyError:
raise KeyError('no widget named %r' % name)
def gen_all_widgets(self):
for widget in self.widgets + self.submit_widgets + self.hidden_widgets:
for w in widget:
yield w
def has_key(self, name):
"""Return true if the widget named 'name' is in the form."""
return name in self._names
def get(self, name, default=None):
"""(name:string, default=None) -> any
Return a widget's value. Returns 'default' if widget named 'name'
does not exist.
"""
widget = self._names.get(name)
if widget is not None:
return widget.parse()
else:
return default
def get_widget(self, name):
"""(name:string) -> Widget | None
Return the widget named 'name'. Returns None if the widget does
not exist.
"""
return self._names.get(name)
def get_submit_widgets(self):
"""() -> [SubmitWidget]
"""
return self.submit_widgets
def get_all_widgets(self):
"""() -> [Widget]
Return all the widgets that have been added to the form. Note that
this while this list includes submit widgets and hidden widgets, it
does not include sub-widgets (e.g. widgets that are part of
CompositeWidgets)
"""
return list(self._names.values())
def __iter__(self):
for widget in self._names.values():
for w in widget:
yield w
def is_submitted(self):
"""() -> bool
Has this form been submitted? This is true if any widget
name appears in the fields of the request.
If the form has been submitted, this *modifies* the
request fields as necessary to make sure that every widget
of this form has a key in the fields dictionary.
"""
request = get_request()
for widget in self:
if widget.is_submitted(request=request):
fields = get_fields()
for widget in self:
fields.setdefault(widget.get_name(), '')
return True
return False
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()
has_errors = False
if self.is_submitted():
for widget in self:
widget.parse(request)
for widget in self:
if widget.has_error(request=request):
has_errors = True
return has_errors
def clear_errors(self):
"""Ensure that all components of the form have parsed themselves.
Clear any errors that might have occurred during parsing.
"""
request = get_request()
for widget in self:
widget.clear_error(request)
def clear(self):
"""Clear submitted values.
"""
request = get_request()
for widget in self:
widget.clear(request)
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 any known
form-level SubmitWidget then return True. Otherwise, return False.
"""
request = get_request()
for button in self.submit_widgets:
if button.parse(request=request):
return button.name
return self.is_submitted()
def set_error(self, name, error):
"""(name : string, error : string)
Set the error attribute of the widget named 'name'.
"""
widget = self._names.get(name)
if not widget:
raise KeyError("unknown name %r" % name)
widget.set_error(error)
def add_widget(self, widget_class, name, *args, **kwargs):
if name in self._names:
raise ValueError("form already has '%s' widget" % name)
if self.prefix is not None:
widget_name = subname(self.prefix, name)
else:
widget_name = name
widget = widget_class(widget_name, *args, **kwargs)
self._names[name] = widget
self.widgets.append(widget)
# Ensure the new widget appears in the fields of the request
request = get_request()
for w in self:
if w.is_submitted(request=request):
fields = get_fields()
for sub_w in widget:
fields.setdefault(sub_w.get_name(), '')
break
return widget
def add(self, widget_class, name, *args, **kwargs):
widget = self.add_widget(widget_class, name, *args, **kwargs)
if isinstance(widget, SubmitWidget):
self.widgets.remove(widget)
self.submit_widgets.append(widget)
elif isinstance(widget, HiddenWidget):
self.widgets.remove(widget)
self.hidden_widgets.append(widget)
if isinstance(widget, FileWidget):
assert self.enctype == "multipart/form-data"
def add_button(self, name, value=None, **kwargs):
self.add(ButtonWidget, name, value, **kwargs)
def add_submit(self, name, value=None, **kwargs):
self.add(SubmitWidget, name, value, **kwargs)
def add_button_submit(self, name, value=None, **kwargs):
self.add(ButtonSubmitWidget, 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_password(self, name, value=None, **kwargs):
self.add(PasswordWidget, 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_option_select(self, name, value=None, **kwargs):
self.add(OptionSelectWidget, 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)
def add_file(self, name, value=None, **kwargs):
self.add(FileWidget, name, value, **kwargs)
# -- Layout (rendering) methods ------------------------------------
def render:xml(self):
self._render_start()
self._render_body()
self._render_finish()
if self.log_errors:
self._log_errors()
def _log_errors(self):
errors = []
for widget in self.gen_all_widgets():
has_errors = True
if widget.has_error():
errors.append((widget.get_name(), widget.get_error()))
if errors:
print('')
for error in errors:
print('Widget error in %s: %s' % (error[0], error[1]))
print('Form fields: %s' % get_fields())
print('')
def _render_start:xml(self):
'\n'
htmltag('form', method=self.method,
enctype=self.enctype, action=self.action or "",
**self.attrs)
'\n\n'
code = get_javascript_code()
if code:
self._render_javascript(code)
'\n'
def _render_widgets:xml(self):
for widget in self.widgets:
'\n'
widget.render()
def _render_hidden_widgets:xml(self):
for widget in self.hidden_widgets:
'\n'
widget.render()
def _render_submit_widgets:xml(self):
if self.submit_widgets:
'\n'
for widget in self.submit_widgets:
'\n'
widget.render()
'\n
\n'
def _render_error_notice(self):
token_widget = self.get_widget(self.TOKEN_NAME)
if token_widget is not None and token_widget.has_error():
# form tokens are enabled but the token data in the request
# does not match anything in the session. It could be an
# a cross-site attack but most likely the back button has
# been used
return self.TOKEN_NOTICE
else:
return self.ERROR_NOTICE
def _render_javascript(self, javascript_code):
"""Render javacript code for the form. Insert code lexically
sorted by code_id.
"""
form_code = []
code_ids = sorted(javascript_code.keys())
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 % xml(''.join(form_code))
else:
return ''
def _render_body:xml(self):
if self.has_errors():
self._render_error_notice()
self._render_widgets()
self._render_submit_widgets()