diff --git a/atst/forms/forms.py b/atst/forms/forms.py index 2aaa4973..ce0ff791 100644 --- a/atst/forms/forms.py +++ b/atst/forms/forms.py @@ -6,3 +6,9 @@ class ValidatedForm(FlaskForm): """Performs any applicable extra validation. Must return True if the form is valid or False otherwise.""" return True + + @property + def data(self): + _data = super().data + _data.pop("csrf_token", None) + return _data diff --git a/atst/forms/poc.py b/atst/forms/poc.py index 3dcdc223..827bf1ae 100644 --- a/atst/forms/poc.py +++ b/atst/forms/poc.py @@ -1,11 +1,30 @@ -from wtforms.fields import StringField +from wtforms.fields import StringField, BooleanField 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): + + def validate(self, *args, **kwargs): + if self.am_poc.data: + # 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()) + self.dodid_poc.validators.insert(0, Optional()) + + return super().validate(*args, **kwargs) + + + am_poc = BooleanField( + "I am the Workspace Owner.", + default=False, + false_values=(False, "false", "False", "no", "") + ) + fname_poc = StringField("First Name", validators=[Required()]) lname_poc = StringField("Last Name", validators=[Required()]) diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py index 7c96d7d5..0b56743b 100644 --- a/atst/routes/requests/jedi_request_flow.py +++ b/atst/routes/requests/jedi_request_flow.py @@ -65,7 +65,7 @@ class JEDIRequestFlow(object): return { "fname_request": user.first_name, "lname_request": user.last_name, - "email_request": user.email + "email_request": user.email, } @property @@ -80,7 +80,7 @@ class JEDIRequestFlow(object): data = self.request.body elif self.form_section == "information_about_you": form_data = self.request.body.get(self.form_section, {}) - data = { **self.map_user_data(self.request.creator), **form_data } + data = {**self.map_user_data(self.request.creator), **form_data} else: data = self.request.body.get(self.form_section, {}) elif self.form_section == "information_about_you": @@ -103,40 +103,36 @@ 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": "Workspace Owner", "section": "primary_poc", "form": POCForm}, { "title": "Review & Submit", "section": "review_submit", "form": ReviewForm, - "show": True, }, ] def create_or_update_request(self): - request_data = {self.form_section: self.form.data} + request_data = self.map_request_data(self.form_section, self.form.data) if self.request_id: Requests.update(self.request_id, request_data) else: request = Requests.create(self.current_user, request_data) self.request_id = request.id + + def map_request_data(self, section, data): + if section == "primary_poc": + if data.get("am_poc", False): + data = { + **data, + "dodid_poc": self.current_user.dod_id, + "fname_poc": self.current_user.first_name, + "lname_poc": self.current_user.last_name, + "email_poc": self.current_user.email, + } + return {section: data} 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) diff --git a/js/components/checkbox_input.js b/js/components/checkbox_input.js new file mode 100644 index 00000000..6ed5e821 --- /dev/null +++ b/js/components/checkbox_input.js @@ -0,0 +1,16 @@ +export default { + name: 'checkboxinput', + + props: { + name: String, + }, + + methods: { + onInput: function (e) { + this.$root.$emit('field-change', { + value: e.target.checked, + name: this.name + }) + } + } +} diff --git a/js/components/forms/poc.js b/js/components/forms/poc.js new file mode 100644 index 00000000..255c1b04 --- /dev/null +++ b/js/components/forms/poc.js @@ -0,0 +1,43 @@ +import optionsinput from '../options_input' +import textinput from '../text_input' +import checkboxinput from '../checkbox_input' + +export default { + name: 'poc', + + components: { + optionsinput, + textinput, + checkboxinput, + }, + + props: { + initialData: { + type: Object, + default: () => ({}) + } + }, + + data: function () { + const { + am_poc = false + } = this.initialData + + return { + am_poc + } + }, + + mounted: function () { + this.$root.$on('field-change', this.handleFieldChange) + }, + + methods: { + handleFieldChange: function (event) { + const { value, name } = event + if (typeof this[name] !== undefined) { + this[name] = value + } + }, + } +} diff --git a/js/index.js b/js/index.js index 5cf47a60..afe6961d 100644 --- a/js/index.js +++ b/js/index.js @@ -4,7 +4,9 @@ import VTooltip from 'v-tooltip' import optionsinput from './components/options_input' import textinput from './components/text_input' +import checkboxinput from './components/checkbox_input' import DetailsOfUse from './components/forms/details_of_use' +import poc from './components/forms/poc' Vue.use(VTooltip) @@ -14,7 +16,9 @@ const app = new Vue({ components: { optionsinput, textinput, + checkboxinput, DetailsOfUse, + poc, }, methods: { closeModal: function(name) { diff --git a/templates/components/checkbox_input.html b/templates/components/checkbox_input.html new file mode 100644 index 00000000..92538843 --- /dev/null +++ b/templates/components/checkbox_input.html @@ -0,0 +1,18 @@ + +{% macro CheckboxInput(field, inline=False) -%} + +
+ +
+ + {{ field() }} + {{ field.label }} + + {% if field.description %} + {{ field.description | safe }} + {% endif %} +
+
+
+ +{%- endmacro %} diff --git a/templates/requests/screen-3.html b/templates/requests/screen-3.html index 2f03c8bf..79f4b3a0 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/checkbox_input.html" import CheckboxInput %} {% block subtitle %}

Designate a Workspace Owner

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

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

- -

+ +
-

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.

+

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

+ +

-{{ 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') }} +

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.

+ {{ CheckboxInput(f.am_poc) }} + + + +
+
{% endblock %} diff --git a/tests/factories.py b/tests/factories.py index 74124f95..3073c4fb 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": False, "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..dd4f926c 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -1,9 +1,8 @@ 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 urllib.parse import urlencode ERROR_CLASS = "alert--error" @@ -103,6 +102,65 @@ 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", + 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={}) + poc_input = { + "am_poc": "no", + "fname_poc": "test", + "lname_poc": "user", + "email_poc": "test.user@mail.com", + "dodid_poc": "1234567890", + } + response = client.post( + "/requests/new/3/{}".format(request.id), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data=urlencode(poc_input), + ) + assert ERROR_CLASS not in response.data.decode() + + +def test_poc_details_can_be_autopopulated_on_new_request(client, user_session): + creator = UserFactory.create() + user_session(creator) + response = client.post( + "/requests/new/3", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="am_poc=yes", + ) + request_id = response.headers["Location"].split('/')[-1] + request = Requests.get(request_id) + + assert request.body["primary_poc"]["dodid_poc"] == creator.dod_id + + def test_can_review_data(user_session, client): creator = UserFactory.create() user_session(creator)