From bc42cca71a3e1edec8dfde6e7b01c77a84e637f3 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 13 Aug 2018 21:25:31 -0400 Subject: [PATCH 01/18] Simplify requests_update --- atst/routes/requests/requests_form.py | 43 ++++++++++++--------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index c027890c..4839b059 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -64,35 +64,30 @@ def requests_update(screen=1, request_id=None): existing_request=existing_request, ) - rerender_args = dict( - f=jedi_flow.form, - data=post_data, - screens=jedi_flow.screens, - current=screen, - next_screen=jedi_flow.next_screen, - request_id=jedi_flow.request_id, - ) + has_next_screen = jedi_flow.next_screen <= len(jedi_flow.screens) + valid = jedi_flow.validate() and jedi_flow.validate_warnings() - if jedi_flow.validate(): + if valid: jedi_flow.create_or_update_request() - valid = jedi_flow.validate_warnings() - if valid: - if jedi_flow.next_screen > len(jedi_flow.screens): - where = "/requests" - else: - where = url_for( - "requests.requests_form_update", - screen=jedi_flow.next_screen, - request_id=jedi_flow.request_id, - ) - return redirect(where) - else: - return render_template( - "requests/screen-%d.html" % int(screen), **rerender_args + if has_next_screen: + where = url_for( + "requests.requests_form_update", + screen=jedi_flow.next_screen, + request_id=jedi_flow.request_id, ) - + else: + where = "/requests" + return redirect(where) else: + rerender_args = dict( + f=jedi_flow.form, + data=post_data, + screens=jedi_flow.screens, + current=screen, + next_screen=jedi_flow.next_screen, + request_id=jedi_flow.request_id, + ) return render_template("requests/screen-%d.html" % int(screen), **rerender_args) From 9de9fb5c6a50675ab994711bd09f1abd1ef3f58e Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 13 Aug 2018 21:34:22 -0400 Subject: [PATCH 02/18] Remove unused fields from JEDIRequestFlow screens --- atst/routes/requests/jedi_request_flow.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py index 7c96d7d5..d69f1928 100644 --- a/atst/routes/requests/jedi_request_flow.py +++ b/atst/routes/requests/jedi_request_flow.py @@ -103,33 +103,21 @@ class JEDIRequestFlow(object): "title": "Details of Use", "section": "details_of_use", "form": RequestForm, - "subitems": [ - { - "title": "Overall request details", - "id": "overall-request-details", - }, - {"title": "Cloud Resources", "id": "cloud-resources"}, - {"title": "Support Staff", "id": "support-staff"}, - ], - "show": True, }, { "title": "Information About You", "section": "information_about_you", "form": OrgForm, - "show": True, }, { "title": "Workspace Owner", "section": "primary_poc", "form": POCForm, - "show": True, }, { "title": "Review & Submit", "section": "review_submit", "form": ReviewForm, - "show": True, }, ] From 5a2953ffc37fa63379cb556ed1477e75557577ee Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 14 Aug 2018 11:41:37 -0400 Subject: [PATCH 03/18] Split request update into new method for easier extension --- atst/routes/requests/jedi_request_flow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py index d69f1928..79506916 100644 --- a/atst/routes/requests/jedi_request_flow.py +++ b/atst/routes/requests/jedi_request_flow.py @@ -122,9 +122,13 @@ class JEDIRequestFlow(object): ] def create_or_update_request(self): - request_data = {self.form_section: self.form.data} if self.request_id: - Requests.update(self.request_id, request_data) + self.update_request(self.form_section, self.form.data) else: + request_data = {self.form_section: self.form.data} request = Requests.create(self.current_user, request_data) self.request_id = request.id + + def update_request(self, section, data): + request_data = {section: data} + Requests.update(self.request_id, request_data) From c6618c503b9a2320ccb101e7948e7d3646fe7eca Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 14 Aug 2018 12:54:48 -0400 Subject: [PATCH 04/18] Create new template and form field --- atst/forms/poc.py | 28 ++++++++++++--- atst/routes/requests/jedi_request_flow.py | 9 +++++ js/components/forms/poc.js | 44 +++++++++++++++++++++++ js/index.js | 2 ++ templates/requests/screen-3.html | 36 ++++++++++++------- tests/factories.py | 1 + tests/routes/test_request_new.py | 40 +++++++++++++++++++++ 7 files changed, 142 insertions(+), 18 deletions(-) create mode 100644 js/components/forms/poc.js diff --git a/atst/forms/poc.py b/atst/forms/poc.py index 3dcdc223..02ef76ab 100644 --- a/atst/forms/poc.py +++ b/atst/forms/poc.py @@ -1,16 +1,34 @@ -from wtforms.fields import StringField +from wtforms.fields import StringField, RadioField from wtforms.fields.html5 import EmailField -from wtforms.validators import Required, Email, Length +from wtforms.validators import Required, Email, Length, Optional from .forms import ValidatedForm from .validators import IsNumber class POCForm(ValidatedForm): - fname_poc = StringField("First Name", validators=[Required()]) - lname_poc = StringField("Last Name", validators=[Required()]) + def validate(self, *args, **kwargs): + if self.am_poc.data == "yes": + self.fname_poc.validators.insert(0, Optional()) + self.lname_poc.validators.insert(0, Optional()) + self.email_poc.validators.insert(0, Optional()) + self.dodid_poc.validators.insert(0, Optional()) - email_poc = EmailField("Email Address", validators=[Required(), Email()]) + return super(POCForm, self).validate(*args, **kwargs) + + + am_poc = RadioField( + "I am the technical POC.", + choices=[("yes", "Yes"), ("no", "No")], + default="no", + validators=[Required()], + ) + + fname_poc = StringField("POC First Name", validators=[Required()]) + + lname_poc = StringField("POC Last Name", validators=[Required()]) + + email_poc = EmailField("POC Email Address", validators=[Required(), Email()]) dodid_poc = StringField( "DOD ID", validators=[Required(), Length(min=10), IsNumber()] diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py index 79506916..ce9b3d01 100644 --- a/atst/routes/requests/jedi_request_flow.py +++ b/atst/routes/requests/jedi_request_flow.py @@ -130,5 +130,14 @@ class JEDIRequestFlow(object): self.request_id = request.id def update_request(self, section, data): + if section == "primary_poc": + if data.get("am_poc") == "yes": + data = { + "dodid_poc": self.existing_request.creator.dod_id, + "fname_poc": self.existing_request.creator.first_name, + "lname_poc": self.existing_request.creator.last_name, + "email_poc": self.existing_request.creator.email + } + request_data = {section: data} Requests.update(self.request_id, request_data) diff --git a/js/components/forms/poc.js b/js/components/forms/poc.js new file mode 100644 index 00000000..0be9fb1f --- /dev/null +++ b/js/components/forms/poc.js @@ -0,0 +1,44 @@ +import optionsinput from '../options_input' +import textinput from '../text_input' + +export default { + name: 'poc', + + components: { + optionsinput, + textinput, + }, + + props: { + initialData: { + type: Object, + default: () => ({}) + } + }, + + data: function () { + return { + am_poc: "no" + } + }, + + mounted: function () { + this.$root.$on('field-change', this.handleFieldChange) + }, + + computed: { + amPOC: function () { + return this.am_poc === 'yes' + }, + }, + + methods: { + handleFieldChange: function (event) { + const { value, name } = event + console.log(value, name) + if (typeof this[name] !== undefined) { + this[name] = value + } + }, + } +} diff --git a/js/index.js b/js/index.js index 5cf47a60..84b90c96 100644 --- a/js/index.js +++ b/js/index.js @@ -5,6 +5,7 @@ import VTooltip from 'v-tooltip' import optionsinput from './components/options_input' import textinput from './components/text_input' import DetailsOfUse from './components/forms/details_of_use' +import poc from './components/forms/poc' Vue.use(VTooltip) @@ -15,6 +16,7 @@ const app = new Vue({ optionsinput, textinput, DetailsOfUse, + poc, }, methods: { closeModal: function(name) { diff --git a/templates/requests/screen-3.html b/templates/requests/screen-3.html index 2f03c8bf..41c632dc 100644 --- a/templates/requests/screen-3.html +++ b/templates/requests/screen-3.html @@ -2,6 +2,7 @@ {% from "components/alert.html" import Alert %} {% from "components/text_input.html" import TextInput %} +{% from "components/options_input.html" import OptionsInput %} {% block subtitle %}

Designate a Workspace Owner

@@ -16,20 +17,29 @@ ) }} {% endif %} -

The Workspace Owner is the primary point of contact and technical administrator of the JEDI Workspace and will have the following responsibilities:

-
    -
  • Organize your cloud-hosted systems into projects and environments
  • -
  • Add users to this workspace and manage members
  • -
  • Manage access to the JEDI Cloud service provider’s portal
  • -
-

+ +
+

Please designate a Primary Point of Contact that will be responsible for owning the workspace in the JEDI Cloud.

+

The Point of Contact will become the primary owner of the workspace created to use the JEDI Cloud. As a workspace owner, this person will have the ability to: +

    +
  • Create multiple application stacks and environments in the workspace to access the commercial cloud service provider portal
  • +
  • Add and manage users in the workspace
  • +
  • View the budget and billing history related to this workspace
  • +
  • Manage access to the Cloud Service Provider's Console
  • +
  • Transfer Workspace ownership to another person
  • +
+ This POC may be you. +

-

This person must be a DoD employee (not a contractor).

-

The Workspace Owner may be you. You will be able to add other administrators later. This person will be invited via email once your request is approved.

+ {{ OptionsInput(f.am_poc) }} -{{ TextInput(f.fname_poc,placeholder='First Name') }} -{{ TextInput(f.lname_poc,placeholder='Last Name') }} -{{ TextInput(f.email_poc,placeholder='jane@mail.mil', validation='email') }} -{{ TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC', validation='dodId') }} + +
+
{% endblock %} diff --git a/tests/factories.py b/tests/factories.py index 74124f95..41996d47 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -56,6 +56,7 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): def build_request_body(cls, user, dollar_value=1000000): return { "primary_poc": { + "am_poc": "no", "dodid_poc": user.dod_id, "email_poc": user.email, "fname_poc": user.first_name, diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py index fadf0ce9..217882c4 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -4,6 +4,7 @@ import urllib from tests.mocks import MOCK_USER, MOCK_REQUEST from tests.factories import RequestFactory, UserFactory from atst.domain.roles import Roles +from atst.domain.requests import Requests ERROR_CLASS = "alert--error" @@ -103,6 +104,45 @@ def test_non_creator_info_is_not_autopopulated(monkeypatch, client, user_session assert not user.last_name in body assert not user.email in body +def test_am_poc_causes_poc_to_be_autopopulated(client, user_session): + creator = UserFactory.create() + user_session(creator) + request = RequestFactory.create(creator=creator, body={}) + client.post( + "/requests/new/3/{}".format(request.id), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="am_poc=yes", + ) + request = Requests.get(request.id) + assert request.body["primary_poc"]["dodid_poc"] == creator.dod_id + + +def test_not_am_poc_requires_poc_info_to_be_completed(client, user_session): + creator = UserFactory.create() + user_session(creator) + request = RequestFactory.create(creator=creator, body={}) + response = client.post( + "/requests/new/3/{}".format(request.id), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="am_poc=no", + ) + assert ERROR_CLASS in response.data.decode() + + +# def test_not_am_poc_allows_user_to_fill_in_poc_info(client, user_session): +# creator = UserFactory.create() +# user_session(creator) +# request = RequestFactory.create(creator=creator, body={}) +# client.post( +# "/requests/new/3/{}".format(request.id), +# headers={"Content-Type": "application/x-www-form-urlencoded"}, +# data="am_poc=yes", +# ) + +# assert "Location" not in response.headers +# request = Requests.get(request.id) + + def test_can_review_data(user_session, client): creator = UserFactory.create() user_session(creator) From ee207f163c23f74eaa7a28da2c33f886eb773cf7 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 14 Aug 2018 15:29:55 -0400 Subject: [PATCH 05/18] Fix tests --- atst/forms/poc.py | 2 ++ tests/routes/test_request_new.py | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/atst/forms/poc.py b/atst/forms/poc.py index 02ef76ab..d55fce7b 100644 --- a/atst/forms/poc.py +++ b/atst/forms/poc.py @@ -9,6 +9,8 @@ class POCForm(ValidatedForm): def validate(self, *args, **kwargs): if self.am_poc.data == "yes": + # Prepend Optional validators so that the validation chain + # halts if no data exists. self.fname_poc.validators.insert(0, Optional()) self.lname_poc.validators.insert(0, Optional()) self.email_poc.validators.insert(0, Optional()) diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py index 217882c4..3efc4360 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -125,22 +125,21 @@ def test_not_am_poc_requires_poc_info_to_be_completed(client, user_session): "/requests/new/3/{}".format(request.id), headers={"Content-Type": "application/x-www-form-urlencoded"}, data="am_poc=no", + follow_redirects=True ) assert ERROR_CLASS in response.data.decode() -# def test_not_am_poc_allows_user_to_fill_in_poc_info(client, user_session): -# creator = UserFactory.create() -# user_session(creator) -# request = RequestFactory.create(creator=creator, body={}) -# client.post( -# "/requests/new/3/{}".format(request.id), -# headers={"Content-Type": "application/x-www-form-urlencoded"}, -# data="am_poc=yes", -# ) - -# assert "Location" not in response.headers -# request = Requests.get(request.id) +def test_not_am_poc_allows_user_to_fill_in_poc_info(client, user_session): + creator = UserFactory.create() + user_session(creator) + request = RequestFactory.create(creator=creator, body={}) + response = client.post( + "/requests/new/3/{}".format(request.id), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="am_poc=no&fname_poc=test&lname_poc=user&email_poc=test.user@mail.com&dodid_poc=1234567890", + ) + assert ERROR_CLASS not in response.data.decode() def test_can_review_data(user_session, client): From d5d1265cd740c6d2d836378cdf508689b6ddc57d Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 14 Aug 2018 15:32:30 -0400 Subject: [PATCH 06/18] Fix linting errors --- tests/routes/test_request_new.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py index 3efc4360..e7a58da1 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -1,7 +1,4 @@ import re -import pytest -import urllib -from tests.mocks import MOCK_USER, MOCK_REQUEST from tests.factories import RequestFactory, UserFactory from atst.domain.roles import Roles from atst.domain.requests import Requests From c639a82b82e8e71c18d3605074e4a974fe151ecc Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 14 Aug 2018 15:57:38 -0400 Subject: [PATCH 07/18] Hide fields on page load if necessary --- js/components/forms/poc.js | 7 +++++-- templates/requests/screen-3.html | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/js/components/forms/poc.js b/js/components/forms/poc.js index 0be9fb1f..da9b9c59 100644 --- a/js/components/forms/poc.js +++ b/js/components/forms/poc.js @@ -17,8 +17,12 @@ export default { }, data: function () { + const { + am_poc = 'no' + } = this.initialData + return { - am_poc: "no" + am_poc } }, @@ -35,7 +39,6 @@ export default { methods: { handleFieldChange: function (event) { const { value, name } = event - console.log(value, name) if (typeof this[name] !== undefined) { this[name] = value } diff --git a/templates/requests/screen-3.html b/templates/requests/screen-3.html index 41c632dc..f54460e2 100644 --- a/templates/requests/screen-3.html +++ b/templates/requests/screen-3.html @@ -33,7 +33,7 @@ {{ OptionsInput(f.am_poc) }} -