Skip to content
This repository has been archived by the owner on Sep 5, 2019. It is now read-only.

Commit

Permalink
Adds the ability to upload files without losing them if the form has …
Browse files Browse the repository at this point in the history
…an unrelated validation error.
  • Loading branch information
Denis Krienbühl committed Jun 2, 2015
1 parent c65d30b commit acd0a09
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 7 deletions.
4 changes: 4 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Changelog
Unreleased
~~~~~~~~~~

- Adds the ability to upload files without losing them if the form has an
unrelated validation error.
[href]

- Divides the submissions into 'pending' and 'complete'.

Pending submissions are temporary and possibly invalid. Complete submissions
Expand Down
7 changes: 3 additions & 4 deletions onegov/form/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ def add(self, form_name, form, state):
# this should happen way earlier, we just double check here
if state == 'complete':
assert form.validate()
else:
form.validate()

# look up the right class depending on the type
_mapper = inspect(FormSubmission).polymorphic_map.get(state)
Expand All @@ -100,10 +102,7 @@ def add(self, form_name, form, state):

# pending submissions are not necessarily valid, however we don't need
# to store invalid state as it is wiped out anyway
if state == 'pending':
form.validate()
for field_id in form.errors:
del submission.data[field_id]
submission.prune(form)

# never include the csrf token
if form.meta.csrf and form.meta.csrf_field_name in submission.data:
Expand Down
10 changes: 10 additions & 0 deletions onegov/form/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ def submitted(self, request):
""" Returns true if the given request is a successful post request. """
return request.POST and self.validate()

def ignore_csrf_error(self):
""" Removes the csrf error from the form if found, after validation.
Use this only if you know what you are doing (really, never).
"""
if self.meta.csrf_field_name in self.errors:
del self.errors[self.meta.csrf_field_name]
self.csrf_token.errors = []


class Fieldset(object):
""" Defines a fieldset with a list of fields. """
Expand Down
23 changes: 21 additions & 2 deletions onegov/form/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import magic
import zlib

from onegov.form.widgets import UploadWidget
from wtforms import FileField, StringField, SelectMultipleField, widgets
from wtforms.widgets import html5 as html5_widgets

Expand All @@ -21,16 +22,34 @@ class UploadField(FileField):
"""

widget = UploadWidget()

def process_formdata(self, valuelist):
# the upload widget optionally includes an action with the request,
# indicating if the existing file should be replaced, kept or deleted
if valuelist:
self.data = self.process_fieldstorage(valuelist[0])
if len(valuelist) == 2:
action, fieldstorage = valuelist
else:
action = 'replace'
fieldstorage = valuelist[0]

if action == 'replace':
self.data = self.process_fieldstorage(fieldstorage)
elif action == 'delete':
self.data = {}
elif action == 'keep':
pass
else:
raise NotImplementedError()
else:
self.data = {}

def process_fieldstorage(self, fs):
if not fs or not hasattr(fs, 'file'):
if not hasattr(fs, 'file'):
return {}

fs.file.seek(0)
file_data = fs.file.read()

mimetype_by_introspection = magic.from_buffer(file_data, mime=True)
Expand Down
18 changes: 18 additions & 0 deletions onegov/form/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ def complete(self):

self.state = 'complete'

def prune(self, form=None):
""" Pending submissions are not necessarily valid. This function
removes the invalid bits from the data if called.
On 'completed' submissions it has no effect.
"""
if self.state == 'pending':

if not form:
form = self.form_class(data=self.data)
form.validate()

for field_id in form.errors:
if field_id in self.data:
del self.data[field_id]
form._fields[field_id].data = ''


class PendingFormSubmission(FormSubmission):
__mapper_args__ = {'polymorphic_identity': 'pending'}
Expand Down
3 changes: 2 additions & 1 deletion onegov/form/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ class ExpectedExtensions(WhitelistedMimeType):
"""

def __init__(self, extensions):
mimetypes = set(types_map.get(ext, None) for ext in extensions)
mimetypes = set(
types_map.get('.' + ext.lstrip('.'), None) for ext in extensions)
super(ExpectedExtensions, self).__init__(whitelist=mimetypes)
70 changes: 70 additions & 0 deletions onegov/form/widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-

from cgi import escape

from wtforms.widgets import FileInput
from wtforms.widgets.core import HTMLString


class UploadWidget(FileInput):
""" An upload widget for the :class:`onegov.form.fields.UploadField` class,
which supports keeping, removing and replacing already uploaded files.
This is necessary as file inputs are read-only on the client and it's
therefore rather easy for users to lose their input otherwise (e.g. a
form with a file is rejected because of some mistake - the file disappears
once the response is rendered on the client).
"""

def __call__(self, field, **kwargs):
input_html = super(UploadWidget, self).__call__(field, **kwargs)

if not field.data:
return HTMLString("""
<div class="upload-widget without-data">
{}
</div>
""".format(input_html))
else:
return HTMLString("""
<div class="upload-widget with-data">
<p>{existing_file_label}: {filename} ✓</p>
<ul>
<li>
<input type="radio" id="{name}-0" name="{name}"
value="keep" checked="">
<label for="{name}-0">{keep_label}</label
</li>
<li>
<input type="radio" id="{name}-1" name="{name}"
value="delete">
<label for="{name}-1">{delete_label}</label>
</li>
<li>
<input type="radio" id="{name}-2" name="{name}"
value="replace">
<label for="{name}-2">{replace_label}</label>
<div>
<label>
<div data-depends-on="{name}/replace"
data-hide-label="false">
{input_html}
</div>
</label>
</div>
</li>
</ul>
</div>
""".format(
# be careful, we do our own html generation here without any
# safety harness - we need to carefully escape values the user
# might supply
filename=escape(field.data['filename'], quote=True),
name=field.id,
input_html=input_html,
existing_file_label=field.gettext('Uploaded file'),
keep_label=field.gettext('Keep file'),
delete_label=field.gettext('Delete file'),
replace_label=field.gettext('Replace file')
))

0 comments on commit acd0a09

Please sign in to comment.