forms: add tracking code UI elements

This commit is contained in:
Frédéric Péters 2015-01-02 13:31:16 +01:00
parent 402e6f65fb
commit ebe4bc6137
5 changed files with 353 additions and 39 deletions

View File

@ -71,11 +71,47 @@ a:visited {
padding-top: 70px;
}
#steps {
#side {
float: right;
width: 204px;
padding: 0;
margin: 0 -20px 0 20px;
}
#side #tracking-code {
margin-bottom: 1em;
border: 1px solid #bfbfbf;
color: #333333;
background: #e6e6e6;
padding: 1ex;
}
#side #tracking-code h3 {
margin: 0;
}
#side #tracking-code button,
#side #tracking-code a {
margin: 1ex auto;
display: block;
text-align: center;
font-size: 120%;
background: white;
border: 1px solid black;
padding: 0.5ex 0;
width: 10em;
}
#side #tracking-code button {
background: #0273B9;
color: white;
}
input[name=savedraft] {
display: none;
}
#steps {
background: white;
border: 1px solid #bfbfbf;
color: #333333;

View File

@ -4,6 +4,7 @@ from quixote import cleanup
from wcs.qommon.ident.password_accounts import PasswordAccount
from wcs.formdef import FormDef
from wcs.categories import Category
from wcs.tracking_code import TrackingCode
from wcs import fields
from utilities import get_app, login, create_temporary_pub, emails
@ -232,3 +233,111 @@ def test_form_tryauth():
pub.cfg['identification'] = {'methods': ['password']}
pub.write_cfg()
def test_form_tracking_code():
formdef = create_formdef()
formdef.data_class().wipe()
formdef.fields = [fields.StringField(id='0', label='string')]
formdef.allow_drafts = True
formdef.store()
resp = get_app(pub).get('/test/')
assert '<h3>Tracking code</h3>' in resp.body
resp.forms[0]['f0'] = 'foobar'
resp = resp.forms[0].submit('submit')
tracking_code = None
for a_tag in resp.html.findAll('a'):
if 'code/' in a_tag['href']:
tracking_code = a_tag.text
break
assert tracking_code is not None
assert formdef.data_class().count() == 1
assert formdef.data_class().select()[0].is_draft()
assert formdef.data_class().select()[0].tracking_code == tracking_code
assert formdef.data_class().select()[0].data['0'] == 'foobar'
# check we can load the formdata as a draft
resp = get_app(pub).get('/')
resp.forms[0]['code'] = tracking_code
resp = resp.forms[0].submit()
assert resp.location == 'http://example.net/code/%s/load' % tracking_code
resp = resp.follow()
assert resp.location == 'http://example.net/test/1'
resp = resp.follow()
assert resp.location.startswith('http://example.net/test/?mt=')
resp = resp.follow()
resp = resp.forms[0].submit('previous')
assert resp.forms[0]['f0'].value == 'foobar'
# check submitted form keeps the tracking code
resp.forms[0]['f0'] = 'barfoo'
resp = resp.forms[0].submit('submit') # -> confirmation page
resp = resp.forms[0].submit('submit') # -> done
resp = resp.follow()
assert 'barfoo' in resp.body
assert formdef.data_class().count() == 1 # check the draft one has been removed
assert formdef.data_class().select()[0].tracking_code == tracking_code
assert formdef.data_class().select()[0].status == 'wf-new'
assert formdef.data_class().select()[0].data['0'] == 'barfoo'
formdata_id = formdef.data_class().select()[0].id
# check we can still go back to it
resp = get_app(pub).get('/')
resp.forms[0]['code'] = tracking_code
resp = resp.forms[0].submit()
assert resp.location == 'http://example.net/code/%s/load' % tracking_code
resp = resp.follow()
assert resp.location == 'http://example.net/test/%s' % formdata_id
resp = resp.follow()
def test_form_tracking_code_email():
formdef = create_formdef()
formdef.data_class().wipe()
formdef.fields = [fields.StringField(id='0', label='string')]
formdef.allow_drafts = True
formdef.store()
formdata = formdef.data_class()()
formdata.data = {'0': 'foobar'}
formdata.tracking_code = 'ABCDEF'
formdata.store()
resp = get_app(pub).get('/test/code/ABCDEF/')
assert '<h2>Keep your tracking code</h2>' in resp.body
resp.forms[0]['email'] = 'foo@localhost'
resp = resp.forms[0].submit()
assert emails.emails.get('Tracking Code reminder')
assert 'ABCDEF' in emails.emails.values()[0]['args'][0]
assert resp.location == 'http://example.net/test/code/ABCDEF/load'
def test_form_invalid_tracking_code():
formdef = create_formdef()
formdef.data_class().wipe()
formdef.fields = [fields.StringField(id='0', label='string')]
formdef.allow_drafts = True
formdef.store()
formdata = formdef.data_class()()
formdata.data = {'0': 'foobar'}
formdata.store()
code = TrackingCode()
code.formdata = formdata
code.store()
# check we can go back to it
resp = get_app(pub).get('/')
resp.forms[0]['code'] = code.id
resp = resp.forms[0].submit()
assert resp.location == 'http://example.net/code/%s/load' % code.id
resp = resp.follow()
assert resp.location == 'http://example.net/test/%s' % formdata.id
resp = resp.follow()
# check we get a not found error message on non-existent code
fake_code = TrackingCode().get_new_id()
resp = get_app(pub).get('/')
resp.forms[0]['code'] = fake_code
resp = resp.forms[0].submit()
assert resp.location == 'http://example.net/code/%s/load' % fake_code
resp = resp.follow(status=404)

View File

@ -31,17 +31,20 @@ from quixote.util import randbytes
from quixote.form.widget import *
from quixote.html import TemplateIO, htmltext
from qommon.admin.emails import EmailsDirectory
from qommon import errors, get_cfg
from qommon import misc, get_logger
from qommon import template
from qommon.form import *
from qommon import tokens
from qommon import emails
from wcs.anonylink import AnonymityLink
from wcs.categories import Category
from wcs.formdef import FormDef
from wcs.formdata import FormData
from wcs.roles import logged_users_role
from wcs.tracking_code import TrackingCode
from wcs.workflows import Workflow, EditableWorkflowStatusItem
from wcs.api import get_user_from_api_query_string
@ -118,9 +121,73 @@ class TokensDirectory(Directory):
return TokenDirectory(self.formdef, token)
class TrackingCodeDirectory(Directory):
_q_exports = ['', 'load']
def __init__(self, code, formdef):
self.code = code
self.formdef = formdef
def _q_index(self):
if self.formdef is None:
raise errors.TraversalError()
form = Form()
if get_request().user and get_request().user.email:
email = get_request().user.email
else:
email = None
form.add(EmailWidget, 'email', value=email, title=_('Email'), size=25, required=True)
form.add_submit('submit', _('Send email'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('./load')
if form.is_submitted() and not form.has_errors():
email = form.get_widget('email').parse()
data = {
'tracking_code': self.code,
'email': email
}
data.update(self.formdef.get_substitution_variables(minimal=True))
emails.custom_ezt_email('tracking-code-reminder', data,
email, fire_and_forget=True)
return redirect('./load')
html_top()
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Keep your tracking code')
r += TextsDirectory.get_html_text('tracking-code-email-dialog')
r += form.render()
return r.getvalue()
def load(self):
try:
tracking_code = TrackingCode.get(self.code)
except KeyError:
raise errors.TraversalError()
formdata = tracking_code.formdata
if not get_session().user:
get_session().mark_anonymous_formdata(formdata)
return redirect(formdata.get_url().rstrip('/'))
class TrackingCodesDirectory(Directory):
_q_exports = ['load']
def __init__(self, formdef=None):
self.formdef = formdef
def load(self):
code = get_request().form.get('code')
return redirect('./%s/load' % code)
def _q_lookup(self, component):
return TrackingCodeDirectory(component, self.formdef)
class FormPage(Directory):
_q_exports = ['', 'listing', 'tempfile', 'tokens', 'schema', 'tryauth',
'auth', 'qrcode', 'autosave']
'auth', 'qrcode', 'autosave', 'code']
steps = [N_("Filling"), N_("Validating"), N_("Receipt")]
@ -135,6 +202,7 @@ class FormPage(Directory):
get_publisher().substitutions.feed(self.formdef)
self.tokens = TokensDirectory(self.formdef)
self.code = TrackingCodesDirectory(self.formdef)
self.page_number = len([
x for x in self.formdef.fields[1:] if x.type == 'page']) + 1
@ -223,6 +291,7 @@ class FormPage(Directory):
self.feed_current_data(magictoken)
form = self.formdef.create_form(page_no, displayed_fields)
form.action = '.'
# include a data-has-draft attribute on the <form> element when a draft
# already exists for the form; this will activate the autosave.
magictoken = get_request().form.get('magictoken')
@ -230,7 +299,8 @@ class FormPage(Directory):
form_data = session.get_by_magictoken(magictoken, {})
if form_data.get('draft_formdata_id'):
form.attrs['data-has-draft'] = 'yes'
get_response().add_javascript(['jquery.js', 'qommon.forms.js'])
else:
form_data = {}
if page_no == 0 and not get_request().form.has_key('magictoken'):
magictoken = randbytes(8)
@ -289,19 +359,50 @@ class FormPage(Directory):
req.form = {}
html_top(self.formdef.name)
r += self.step(0, page_no, log_detail, data = data, editing = editing)
r += self.form_side(0, page_no, log_detail=log_detail, data=data, editing=editing)
form.add_hidden('step', '0')
form.add_hidden('page', page_no)
form.add_submit('cancel', _('Cancel'), css_class = 'cancel')
if session.user and self.formdef.allow_drafts and not editing:
if get_request().form.has_key('mt'):
form.add_submit('removedraft', _('Remove Draft'), css_class = 'remove-draft')
form.add_submit('savedraft', _('Save As Draft'), css_class = 'save-draft')
if self.formdef.allow_drafts and not editing:
form.add_submit('savedraft', _('Save As Draft'), css_class = 'save-draft',
attrs={'style': 'display: none'})
r += form.render()
return r.getvalue()
def form_side(self, step_no, page_no=0, log_detail=None, data=None, editing=None):
'''Create the elements that typically appear aside the main form
(tracking code and steps).'''
r = TemplateIO(html=True)
r += htmltext('<div id="side">')
if self.formdef.allow_drafts:
r += self.tracking_code_box(data)
r += self.step(step_no, page_no, log_detail, data=data, editing=editing)
r += htmltext('</div> <!-- #side -->')
return r.getvalue()
def tracking_code_box(self, data):
'''Create the tracking code box, it displays the current tracking code
or a 'save' button if it has not yet been created.'''
r = TemplateIO(html=True)
draft_formdata_id = data.get('draft_formdata_id')
r += htmltext('<div id="tracking-code">')
r += htmltext('<h3>%s</h3>') % _('Tracking code')
if draft_formdata_id:
formdata = self.formdef.data_class().get(draft_formdata_id)
if formdata.tracking_code:
get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'popup.js'])
r += htmltext('<a rel="popup" href="%s">%s</a>') % (
'code/%s/' % formdata.tracking_code,
formdata.tracking_code)
else:
r += htmltext('<button>%s</button>') % _('Save')
r += TextsDirectory.get_html_text('tracking-code-short-text')
r += htmltext('</div>') # <!-- #tracking-code -->
return r.getvalue()
def feed_current_data(self, magictoken):
# create a fake FormData to feed variables
formdata = FormData()
@ -339,6 +440,7 @@ class FormPage(Directory):
if [x for x in user_forms if not x.is_draft()]:
return redirect('%s/' % user_forms[0].id)
get_response().add_javascript(['jquery.js', 'qommon.forms.js'])
form = Form()
form.add_hidden('step', '-1')
form.add_hidden('page', '-1')
@ -353,6 +455,7 @@ class FormPage(Directory):
if get_request().form.has_key('mt'):
magictoken = get_request().form['mt']
data = session.get_by_magictoken(magictoken, {})
session.remove_magictoken(magictoken)
if data:
# create a new one since the other has been exposed in a url
magictoken = randbytes(8)
@ -418,8 +521,8 @@ class FormPage(Directory):
form_data = session.get_by_magictoken(magictoken, {})
data = self.formdef.get_data(form)
form_data.update(data)
self.save_draft(form_data, page_no)
return redirect(get_publisher().get_root_url())
filled = self.save_draft(form_data, page_no)
return redirect(filled.get_url().rstrip('/'))
# form.get_submit() returns the name of the clicked button, and
# it will return True if the form has been submitted, but not
@ -448,10 +551,14 @@ class FormPage(Directory):
if next_page.is_visible(form_data, self.formdef):
break
# if there's a draft, update it with current data
draft_id = session.get_by_magictoken(magictoken, {}).get('draft_formdata_id')
if draft_id:
self.autosave_draft(draft_id, page_no, form_data)
if self.formdef.allow_drafts:
draft_id = session.get_by_magictoken(magictoken, {}).get('draft_formdata_id')
if draft_id:
# if there's a draft, update it with current data
self.autosave_draft(draft_id, page_no, form_data)
else:
# if there's no draft yet, create one
filled = self.save_draft(form_data, page_no)
if page_no == self.page_number:
# last page has been submitted
@ -516,8 +623,8 @@ class FormPage(Directory):
return redirect(get_publisher().get_root_url())
if self.formdef.allow_drafts and form.get_submit() == 'savedraft':
self.save_draft(form_data, page_no = -1)
return redirect(get_publisher().get_root_url())
filled = self.save_draft(form_data, page_no = -1)
return redirect(filled.get_url().rstrip('/'))
# so it gets FakeFileWidget in preview mode
form = self.formdef.create_view_form(form_data,
@ -625,14 +732,14 @@ class FormPage(Directory):
filled.user_id = session.user
filled.store()
magictoken = get_request().form.get('magictoken')
if magictoken:
form_data = session.get_by_magictoken(magictoken, {})
if form_data.get('draft_formdata_id'):
filled.remove_object(form_data.get('draft_formdata_id'))
if not filled.user_id:
get_session().mark_anonymous_formdata(filled)
self.keep_tracking_code(filled)
get_logger().info('form %s - saving draft (id: %s)' % (self.formdef.name, filled.id))
return filled
def submitted(self, form, existing_formdata = None):
if existing_formdata: # modifying
@ -657,13 +764,10 @@ class FormPage(Directory):
user_forms = get_user_forms(self.formdef)
if [x for x in user_forms if not x.is_draft()]:
return redirect('%s/' % user_forms[0].id)
filled.store()
magictoken = get_request().form.get('magictoken')
if magictoken:
form_data = session.get_by_magictoken(magictoken, {})
if form_data.get('draft_formdata_id'):
filled.remove_object(form_data.get('draft_formdata_id'))
self.keep_tracking_code(filled)
session.remove_magictoken(get_request().form.get('magictoken'))
filled.store()
if not filled.user_id and existing_formdata is None:
a = AnonymityLink()
@ -686,6 +790,26 @@ class FormPage(Directory):
url = filled.get_url()
return redirect(url)
def keep_tracking_code(self, formdata):
'''Remove current draft in favour of formdata, conserving the same
tracking code.'''
code = None
magictoken = get_request().form.get('magictoken')
if magictoken:
session = get_session()
form_data = session.get_by_magictoken(magictoken, {})
draft_formdata_id = form_data.get('draft_formdata_id')
if draft_formdata_id:
old_draft_formdata = self.formdef.data_class().get(draft_formdata_id)
old_draft_formdata.remove_self()
if old_draft_formdata.tracking_code:
code = TrackingCode.get(old_draft_formdata.tracking_code)
form_data['draft_formdata_id'] = formdata.id
if code is None:
code = TrackingCode()
code.formdata = formdata # this will .store() the code
def submitted_existing(self, form, editing):
old_data = editing.data
editing.data = self.formdef.get_data(form)
@ -716,7 +840,7 @@ class FormPage(Directory):
html_top(self.formdef.name)
r = TemplateIO(html=True)
r += htmltext('<div class="form-validation">')
r += self.step(1, data = data)
r += self.form_side(step_no=1, data=data)
r += TextsDirectory.get_html_text('check-before-submit')
form = self.formdef.create_view_form(data)
token_widget = form.get_widget(form.TOKEN_NAME)
@ -725,10 +849,9 @@ class FormPage(Directory):
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'), css_class = 'cancel')
session = get_session()
if session.user and self.formdef.allow_drafts:
if get_request().form.has_key('mt'):
form.add_submit('removedraft', _('Remove Draft'), css_class = 'remove-draft')
form.add_submit('savedraft', _('Save As Draft'), css_class = 'save-draft')
if self.formdef.allow_drafts:
form.add_submit('savedraft', _('Save As Draft'), css_class = 'save-draft',
attrs={'style': 'display: none'})
form.add_hidden('step', '2')
magictoken = get_request().form['magictoken']
form.add_hidden('magictoken', magictoken)
@ -803,12 +926,17 @@ class FormPage(Directory):
if not filled.is_draft():
return PublicFormStatusPage(self.formdef, filled)
if get_request().user is None:
raise errors.AccessUnauthorizedError()
session = get_session()
if str(session.user) != str(filled.user_id):
raise errors.AccessForbiddenError()
if session.user:
if str(session.user) != str(filled.user_id):
raise errors.AccessForbiddenError()
else:
if not session.is_anonymous_submitter(filled):
raise errors.AccessUnauthorizedError()
if get_request().get_query() == 'remove-draft':
filled.remove_self()
return redirect(get_publisher().get_root_url())
magictoken = randbytes(8)
filled.feed_session()
@ -817,13 +945,14 @@ class FormPage(Directory):
form_data['page_no'] = filled.page_no
session.add_magictoken(magictoken, form_data)
return redirect('./?mt=%s' % magictoken)
return redirect('%s?mt=%s' % (filled.formdef.get_url(), magictoken))
class RootDirectory(AccessControlled, Directory):
_q_exports = ['', 'json', 'categories']
_q_exports = ['', 'json', 'categories', 'code']
category = None
code = TrackingCodesDirectory()
def __init__(self, category = None):
self.category = category
@ -879,6 +1008,17 @@ class RootDirectory(AccessControlled, Directory):
r += message
r += htmltext('</div>')
if FormDef.count():
r += htmltext('<div id="side">')
r += htmltext('<div id="tracking-code">')
r += htmltext('<h3>%s</h3>') % _('Tracking code')
r += htmltext('<form action="/code/load">')
r += htmltext('<input size="12" name="code" placeholder="%s"/>') % _('ex: RPQDFVCD')
r += htmltext('<input type="submit" value="load"/>')
r += htmltext('</form>')
r += htmltext('</div>')
r += htmltext('</div> <!-- #side -->')
if self.category:
formdefs = FormDef.select(lambda x: (
x.category_id == self.category.id and (not x.is_disabled() or x.disabled_redirection)),
@ -1211,3 +1351,23 @@ TextsDirectory.register('check-before-submit',
category = N_('Forms'),
default = N_('Check values then click submit.'))
TextsDirectory.register('tracking-code-email-dialog',
N_('Message in tracking code popup dialog'),
category = N_('Forms'),
default = N_('You can get a reminder of the tracking code by email.'))
TextsDirectory.register('tracking-code-short-text',
N_('Short text in the tracking code box'),
category=N_('Forms'))
EmailsDirectory.register('tracking-code-reminder',
N_('Tracking Code'),
N_('Available variables: email, form, tracking_code'),
category = N_('Miscellaneous'),
default_subject = N_('Tracking Code reminder'),
default_body = N_('''\
Hello,
As a reminder your tracking code for [form_name] is [tracking_code].
'''))

View File

@ -21,4 +21,7 @@ $(function() {
});
}, 5000);
}
$('#tracking-code button').click(function() {
$('input[name=savedraft]').click();
});
});

View File

@ -46,6 +46,12 @@ class BasicSession(Session):
return default
return self.magictokens.get(token, default)
def remove_magictoken(self, token):
if not self.magictokens:
return
if token in self.magictokens:
del self.magictokens[token]
def mark_anonymous_formdata(self, formdata):
if not self.anonymous_formdata_keys:
self.anonymous_formdata_keys = {}