From 05e201a7493efa106b5391f080e42ba71e362db5 Mon Sep 17 00:00:00 2001 From: Ferry Boender Date: Tue, 31 Mar 2015 22:39:35 +0200 Subject: [PATCH] Improved error reporting and validation. --- src/scriptform.py | 178 ++++++++++++++++++++++++++++------------------ 1 file changed, 107 insertions(+), 71 deletions(-) diff --git a/src/scriptform.py b/src/scriptform.py index 62bdf89..7f3a7d1 100755 --- a/src/scriptform.py +++ b/src/scriptform.py @@ -160,25 +160,32 @@ class FormDefinition: """ Validate all relevant fields for this form against form_values. """ + errors = {} values = {} - for field_name in form_values: - if field_name == 'form_name' or \ - form_values[field_name].filename: - continue - v = self.validate_field(field_name, - form_values.getfirst(field_name)) - if v is not None: - values[field_name] = v - # Make sure all required fields are there + # First make sure all required fields are there for field in self.fields: if 'required' in field and \ field['required'] is True and \ - field['name'] not in values: - raise ValueError( - "Required field {0} not present".format(field['name'])) + field['name'] not in form_values: + errors.setdefault(field['name'], []).append( + "This field is required" + ) - return values + # Now validate their actual values. + for field_name in form_values: + if field_name == 'form_name' or \ + form_values[field_name].filename: + continue + try: + v = self.validate_field(field_name, + form_values.getfirst(field_name)) + if v is not None: + values[field_name] = v + except Exception, e: + errors.setdefault(field_name, []).append(str(e)) + + return (errors, values) def validate_field(self, field_name, value): """ @@ -191,36 +198,68 @@ class FormDefinition: field_type = field_def['type'] validate_cb = getattr(self, 'validate_{0}'.format(field_type), None) - if not validate_cb: - return value - else: - return validate_cb(field_def, value) + return validate_cb(field_def, value) + + def validate_string(self, field_def, value): + maxlen = field_def.get('maxlen', None) + minlen = field_def.get('minlen', None) + + if minlen is not None and len(value) < minlen: + raise Exception("Minimum length is {0}".format(minlen)) + if maxlen is not None and len(value) > maxlen: + raise Exception("Maximum length is {0}".format(maxlen)) + + return value def validate_integer(self, field_def, value): + max = field_def.get('max', None) + min = field_def.get('min', None) + try: - int(value) - return value + value = int(value) except ValueError: - if field_def.get('required', False): - raise - return None + raise Exception("Must be an integer number") + + if min is not None and value < min: + raise Exception("Minimum value is {0}".format(min)) + if max is not None and value > max: + raise Exception("Maximum value is {0}".format(max)) + + return int(value) def validate_float(self, field_def, value): + max = field_def.get('max', None) + min = field_def.get('min', None) + try: - return float(value) + value = float(value) except ValueError: - if field_def.get('required', False): - raise - return None + raise Exception("Must be an real (float) number") + + if min is not None and value < min: + raise Exception("Minimum value is {0}".format(min)) + if max is not None and value > max: + raise Exception("Maximum value is {0}".format(max)) + + return float(value) def validate_date(self, field_def, value): - m = re.match('([0-9]{4})-([0-9]{2})-([0-9]{2})', value) - if m: - return value - elif field_def.get('required', False): - raise ValueError( - "Invalid value for date field: {0}".format(value)) - return None + max = field_def.get('max', None) + min = field_def.get('min', None) + + try: + value = datetime.datetime.strptime(value, '%Y-%m-%d').date() + except ValueError: + raise Exception("Invalid date, must be in form YYYY-MM-DD") + + if min is not None: + if value < datetime.datetime.strptime(min, '%Y-%m-%d').date(): + raise Exception("Minimum value is {0}".format(min)) + if max is not None: + if value > datetime.datetime.strptime(max, '%Y-%m-%d').date(): + raise Exception("maximum value is {0}".format(max)) + + return value def validate_radio(self, field_def, value): if not value in [o[0] for o in field_def['options']]: @@ -377,7 +416,7 @@ class ScriptFormWebApp(WebAppHandler): self.end_headers() self.wfile.write(output) - def h_form(self, form_name): + def h_form(self, form_name, errors={}): if not self.auth(): return @@ -394,7 +433,7 @@ class ScriptFormWebApp(WebAppHandler): "radio": '{2}
', } - def render_field(field): + def render_field(field, errors): tpl = field_tpl[field['type']] required = '' @@ -438,23 +477,35 @@ class ScriptFormWebApp(WebAppHandler): return ('''
  • {title}

    -

    {input}

    +

    {input} {errors}

  • - '''.format(title=field['title'], - input=input)) + '''.format( + title=field['title'], + input=input, + errors=', '.join(errors) + ) + ) form_def = self.scriptform.get_form(form_name) if form_def.allowed_users is not None and \ self.username not in form_def.allowed_users: raise Exception("Not authorized") + html_errors = '' + if errors: + html_errors = '' + output = html_form.format( header=html_header.format(title=self.scriptform.title), footer=html_footer, title=form_def.title, description=form_def.description, + errors=html_errors, name=form_def.name, - fields=''.join([render_field(f) for f in form_def.fields]), + fields=''.join([render_field(f, errors.get(f['name'], [])) for f in form_def.fields]), submit_title=form_def.submit_title ) self.send_response(200) @@ -490,41 +541,23 @@ class ScriptFormWebApp(WebAppHandler): file_fields[field_name] = tmpfile # Validate the form values - form_values = form_def.validate(form_values) + form_errors, form_values = form_def.validate(form_values) - # Repopulate form values with uploaded tmp filenames - form_values.update(file_fields) + if not form_errors: + # Repopulate form values with uploaded tmp filenames + form_values.update(file_fields) - # Call user's callback. If a result is returned, we assume the callback - # was not a raw script, so we wrap its output in some nice HTML. - # Otherwise the callback should have written its own response to the - # self.wfile filehandle. - try: + # Call user's callback. If a result is returned, we assume the callback + # was not a raw script, so we wrap its output in some nice HTML. + # Otherwise the callback should have written its own response to the + # self.wfile filehandle. result = self.scriptform.callback(form_name, form_values, self.wfile) if result: if result['exitcode'] != 0: msg = '{0}'.format(result['stderr']) else: msg = '
    {0}
    '.format(result['stdout']) - output = ''' - {header} -
    -

    {title}

    -

    Result

    -
    {msg}
    - -
    - {footer} - '''.format( + output = html_submit_response.format( header=html_header.format(title=self.scriptform.title), footer=html_footer, title=form_def.title, @@ -535,11 +568,14 @@ class ScriptFormWebApp(WebAppHandler): self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(output) - finally: - # Clean up uploaded files - for file_name in file_fields.values(): - if os.path.exists(file_name): - os.unlink(file_name) + else: + # Form had errors + self.h_form(form_name, form_errors) + + # Clean up uploaded files + for file_name in file_fields.values(): + if os.path.exists(file_name): + os.unlink(file_name) class ScriptForm: """