Merge pull request #448 from dod-ccpo/save-form-state

Save user's form state for later when their session expires before submission
This commit is contained in:
richard-dds 2018-11-20 11:34:24 -05:00 committed by GitHub
commit b8abeeb1cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 103 additions and 19 deletions

View File

@ -24,6 +24,7 @@ from atst.models.permissions import Permissions
from atst.eda_client import MockEDAClient from atst.eda_client import MockEDAClient
from atst.uploader import Uploader from atst.uploader import Uploader
from atst.utils import mailer from atst.utils import mailer
from atst.utils.form_cache import FormCache
from atst.queue import queue from atst.queue import queue
@ -67,6 +68,8 @@ def make_app(config):
if ENV != "prod": if ENV != "prod":
app.register_blueprint(dev_routes) app.register_blueprint(dev_routes)
app.form_cache = FormCache(app.redis)
apply_authentication(app) apply_authentication(app)
return app return app

View File

@ -2,11 +2,11 @@ from wtforms.fields.html5 import EmailField, TelField
from wtforms.fields import StringField, TextAreaField from wtforms.fields import StringField, TextAreaField
from wtforms.validators import Email, Optional from wtforms.validators import Email, Optional
from .forms import ValidatedForm from .forms import CacheableForm
from .validators import Name, PhoneNumber from .validators import Name, PhoneNumber
class CCPOReviewForm(ValidatedForm): class CCPOReviewForm(CacheableForm):
comment = TextAreaField( comment = TextAreaField(
"Instructions or comments", "Instructions or comments",
description="Provide instructions or notes for additional information that is necessary to approve the request here. The requestor may then re-submit the updated request or initiate contact outside of AT-AT if further discussion is required. <strong>This message will be shared with the person making the JEDI request.</strong>.", description="Provide instructions or notes for additional information that is necessary to approve the request here. The requestor may then re-submit the updated request or initiate contact outside of AT-AT if further discussion is required. <strong>This message will be shared with the person making the JEDI request.</strong>.",

View File

@ -5,7 +5,7 @@ from wtforms.fields import RadioField, StringField
from wtforms.validators import Email, DataRequired, Optional from wtforms.validators import Email, DataRequired, Optional
from .fields import SelectField from .fields import SelectField
from .forms import ValidatedForm from .forms import CacheableForm
from .data import SERVICE_BRANCHES from .data import SERVICE_BRANCHES
from atst.models.user import User from atst.models.user import User
@ -77,7 +77,7 @@ def inherit_user_field(field_name):
return inherit_field(USER_FIELDS[field_name], required=required) return inherit_field(USER_FIELDS[field_name], required=required)
class EditUserForm(ValidatedForm): class EditUserForm(CacheableForm):
first_name = inherit_user_field("first_name") first_name = inherit_user_field("first_name")
last_name = inherit_user_field("last_name") last_name = inherit_user_field("last_name")

View File

@ -7,7 +7,7 @@ from flask_wtf.file import FileAllowed
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from .fields import NewlineListField, SelectField, NumberStringField from .fields import NewlineListField, SelectField, NumberStringField
from atst.forms.forms import ValidatedForm from atst.forms.forms import CacheableForm
from .data import FUNDING_TYPES from .data import FUNDING_TYPES
from .validators import DateRange from .validators import DateRange
@ -31,7 +31,7 @@ def coerce_choice(val):
return val.value return val.value
class TaskOrderForm(ValidatedForm): class TaskOrderForm(CacheableForm):
def do_validate_number(self): def do_validate_number(self):
for field in self: for field in self:
if field.name != "task_order-number": if field.name != "task_order-number":
@ -127,7 +127,7 @@ class TaskOrderForm(ValidatedForm):
) )
class RequestFinancialVerificationForm(ValidatedForm): class RequestFinancialVerificationForm(CacheableForm):
uii_ids = NewlineListField( uii_ids = NewlineListField(
"Unique Item Identifier (UII)s related to your application(s) if you already have them.", "Unique Item Identifier (UII)s related to your application(s) if you already have them.",
description="If you have more than one UII, place each one on a new line.", description="If you have more than one UII, place each one on a new line.",
@ -174,7 +174,7 @@ class RequestFinancialVerificationForm(ValidatedForm):
self.uii_ids.process_data(self.uii_ids.data) self.uii_ids.process_data(self.uii_ids.data)
class FinancialVerificationForm(ValidatedForm): class FinancialVerificationForm(CacheableForm):
task_order = FormField(TaskOrderForm) task_order = FormField(TaskOrderForm)
request = FormField(RequestFinancialVerificationForm) request = FormField(RequestFinancialVerificationForm)

View File

@ -1,4 +1,5 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask import current_app, request as http_request
class ValidatedForm(FlaskForm): class ValidatedForm(FlaskForm):
@ -12,3 +13,11 @@ class ValidatedForm(FlaskForm):
_data = super().data _data = super().data
_data.pop("csrf_token", None) _data.pop("csrf_token", None)
return _data return _data
class CacheableForm(ValidatedForm):
def __init__(self, formdata=None, **kwargs):
formdata = formdata or {}
cached_data = current_app.form_cache.from_request(http_request)
cached_data.update(formdata)
super().__init__(cached_data, **kwargs)

View File

@ -1,10 +1,10 @@
from wtforms.fields import TextAreaField from wtforms.fields import TextAreaField
from wtforms.validators import InputRequired from wtforms.validators import InputRequired
from .forms import ValidatedForm from .forms import CacheableForm
class InternalCommentForm(ValidatedForm): class InternalCommentForm(CacheableForm):
text = TextAreaField( text = TextAreaField(
"CCPO Internal Notes", "CCPO Internal Notes",
default="", default="",

View File

@ -4,7 +4,7 @@ from wtforms.fields import BooleanField, RadioField, StringField, TextAreaField
from wtforms.validators import Email, Length, Optional, InputRequired, DataRequired from wtforms.validators import Email, Length, Optional, InputRequired, DataRequired
from .fields import SelectField from .fields import SelectField
from .forms import ValidatedForm from .forms import CacheableForm
from .edit_user import USER_FIELDS, inherit_field from .edit_user import USER_FIELDS, inherit_field
from .data import ( from .data import (
SERVICE_BRANCHES, SERVICE_BRANCHES,
@ -16,7 +16,7 @@ from .validators import DateRange, IsNumber
from atst.domain.requests import Requests from atst.domain.requests import Requests
class DetailsOfUseForm(ValidatedForm): class DetailsOfUseForm(CacheableForm):
def validate(self, *args, **kwargs): def validate(self, *args, **kwargs):
if self.jedi_migration.data == "no": if self.jedi_migration.data == "no":
self.rationalization_software_systems.validators.append(Optional()) self.rationalization_software_systems.validators.append(Optional())
@ -162,7 +162,7 @@ class DetailsOfUseForm(ValidatedForm):
) )
class InformationAboutYouForm(ValidatedForm): class InformationAboutYouForm(CacheableForm):
fname_request = inherit_field(USER_FIELDS["first_name"]) fname_request = inherit_field(USER_FIELDS["first_name"])
lname_request = inherit_field(USER_FIELDS["last_name"]) lname_request = inherit_field(USER_FIELDS["last_name"])
email_request = inherit_field(USER_FIELDS["email"]) email_request = inherit_field(USER_FIELDS["email"])
@ -174,7 +174,7 @@ class InformationAboutYouForm(ValidatedForm):
date_latest_training = inherit_field(USER_FIELDS["date_latest_training"]) date_latest_training = inherit_field(USER_FIELDS["date_latest_training"])
class WorkspaceOwnerForm(ValidatedForm): class WorkspaceOwnerForm(CacheableForm):
def validate(self, *args, **kwargs): def validate(self, *args, **kwargs):
if self.am_poc.data: if self.am_poc.data:
# Prepend Optional validators so that the validation chain # Prepend Optional validators so that the validation chain
@ -203,5 +203,5 @@ class WorkspaceOwnerForm(ValidatedForm):
) )
class ReviewAndSubmitForm(ValidatedForm): class ReviewAndSubmitForm(CacheableForm):
reviewed = BooleanField("I have reviewed this data and it is correct.") reviewed = BooleanField("I have reviewed this data and it is correct.")

View File

@ -1,10 +1,10 @@
from wtforms.fields import StringField from wtforms.fields import StringField
from wtforms.validators import Length from wtforms.validators import Length
from .forms import ValidatedForm from .forms import CacheableForm
class WorkspaceForm(ValidatedForm): class WorkspaceForm(CacheableForm):
name = StringField( name = StringField(
"Workspace Name", "Workspace Name",
validators=[ validators=[

View File

@ -96,7 +96,12 @@ def _make_authentication_context():
def redirect_after_login_url(): def redirect_after_login_url():
if request.args.get("next"): if request.args.get("next"):
return request.args.get("next") returl = request.args.get("next")
if request.args.get(app.form_cache.PARAM_NAME):
returl += "?" + url.urlencode(
{app.form_cache.PARAM_NAME: request.args.get(app.form_cache.PARAM_NAME)}
)
return returl
else: else:
return url_for("atst.home") return url_for("atst.home")

View File

@ -37,7 +37,10 @@ def make_error_pages(app):
# pylint: disable=unused-variable # pylint: disable=unused-variable
def session_expired(e): def session_expired(e):
log_error(e) log_error(e)
return redirect(url_for("atst.root", sessionExpired=True, next=request.path)) url_args = {"sessionExpired": True, "next": request.path}
if request.method == "POST":
url_args[app.form_cache.PARAM_NAME] = app.form_cache.write(request.form)
return redirect(url_for("atst.root", **url_args))
@app.errorhandler(Exception) @app.errorhandler(Exception)
# pylint: disable=unused-variable # pylint: disable=unused-variable

View File

@ -99,6 +99,7 @@ class GetFinancialVerificationForm(FinancialVerificationBase):
def execute(self): def execute(self):
form = self._get_form(self.request, self.is_extended) form = self._get_form(self.request, self.is_extended)
form.reset()
return form return form
@ -178,6 +179,7 @@ class SaveFinancialVerificationDraft(FinancialVerificationBase):
return updated_request return updated_request
@requests_bp.route("/requests/verify/<string:request_id>/draft", methods=["GET"])
@requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"]) @requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"])
def financial_verification(request_id): def financial_verification(request_id):
request = Requests.get(g.current_user, request_id) request = Requests.get(g.current_user, request_id)

40
atst/utils/form_cache.py Normal file
View File

@ -0,0 +1,40 @@
from hashlib import sha256
import json
from werkzeug.datastructures import MultiDict
DEFAULT_CACHE_NAME = "formcache"
class FormCache(object):
PARAM_NAME = "formCache"
def __init__(self, redis):
self.redis = redis
def from_request(self, http_request):
cache_key = http_request.args.get(self.PARAM_NAME)
if cache_key:
return self.read(cache_key)
return MultiDict()
def write(self, formdata, expiry_seconds=3600, key_prefix=DEFAULT_CACHE_NAME):
value = json.dumps(formdata)
hash_ = self._hash()
self.redis.setex(
name=self._key(key_prefix, hash_), value=value, time=expiry_seconds
)
return hash_
def read(self, formdata_key, key_prefix=DEFAULT_CACHE_NAME):
data = self.redis.get(self._key(key_prefix, formdata_key))
dict_data = json.loads(data) if data is not None else {}
return MultiDict(dict_data)
@staticmethod
def _key(prefix, hash_):
return "{}:{}".format(prefix, hash_)
@staticmethod
def _hash():
return sha256().hexdigest()

View File

@ -0,0 +1,22 @@
import pytest
from werkzeug.datastructures import ImmutableMultiDict
from atst.utils.form_cache import DEFAULT_CACHE_NAME, FormCache
@pytest.fixture
def form_cache(app):
return FormCache(app.redis)
def test_cache_form_data(app, form_cache):
data = ImmutableMultiDict({"kessel_run": "12 parsecs"})
key = form_cache.write(data)
assert app.redis.get("{}:{}".format(DEFAULT_CACHE_NAME, key))
def test_retrieve_form_data(form_cache):
data = ImmutableMultiDict({"class": "corellian"})
key = form_cache.write(data)
retrieved = form_cache.read(key)
assert retrieved == data