Merge pull request #38 from dod-ccpo/requests-persistence

Hook ATST up to the requests-queue
This commit is contained in:
richard-dds 2018-06-19 16:14:31 -04:00 committed by GitHub
commit 390df83390
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 163 additions and 54 deletions

View File

@ -22,6 +22,10 @@ class ApiClient(object):
def post(self, path, **kwargs): def post(self, path, **kwargs):
return (yield self.make_request('POST', self.base_url + path, **kwargs)) return (yield self.make_request('POST', self.base_url + path, **kwargs))
@tornado.gen.coroutine
def patch(self, path, **kwargs):
return (yield self.make_request('PATCH', self.base_url + path, **kwargs))
@tornado.gen.coroutine @tornado.gen.coroutine
def delete(self, path, **kwargs): def delete(self, path, **kwargs):
return (yield self.make_request('DELETE', self.base_url + path, **kwargs)) return (yield self.make_request('DELETE', self.base_url + path, **kwargs))
@ -39,11 +43,13 @@ class ApiClient(object):
if not 'validate_cert' in kwargs: if not 'validate_cert' in kwargs:
kwargs['validate_cert'] = self.validate_cert kwargs['validate_cert'] = self.validate_cert
response = yield self.client.fetch(url, method=method, **kwargs) response = yield self.client.fetch(
url, method=method, **kwargs)
return self.adapt_response(response) return self.adapt_response(response)
def adapt_response(self, response): def adapt_response(self, response):
if response.headers['Content-Type'] == 'application/json': if 'application/json' in response.headers['Content-Type']:
json = loads(response.body) json = loads(response.body)
setattr(response, 'json', json) setattr(response, 'json', json)
setattr(response, 'ok', 200 <= response.code < 300)
return response return response

View File

@ -34,7 +34,11 @@ def make_app(config, deps, **kwargs):
{"page": "workspaces", "authz_client": deps["authz_client"]}, {"page": "workspaces", "authz_client": deps["authz_client"]},
name="workspaces", name="workspaces",
), ),
url(r"/requests", Request, {"page": "requests"}, name="requests"), url(
r"/requests",
Request,
{"page": "requests", 'requests_client': deps['requests_client']},
name="requests"),
url( url(
r"/requests/new", r"/requests/new",
RequestNew, RequestNew,
@ -44,7 +48,13 @@ def make_app(config, deps, **kwargs):
r"/requests/new/([0-9])", r"/requests/new/([0-9])",
RequestNew, RequestNew,
{"page": "requests_new", "requests_client": deps["requests_client"]}, {"page": "requests_new", "requests_client": deps["requests_client"]},
name="request_form", name="request_form_new",
),
url(
r"/requests/new/([0-9])/(\S+)",
RequestNew,
{"page": "requests_new", "requests_client": deps["requests_client"]},
name="request_form_update",
), ),
url(r"/users", MainHandler, {"page": "users"}, name="users"), url(r"/users", MainHandler, {"page": "users"}, name="users"),
url(r"/reports", MainHandler, {"page": "reports"}, name="reports"), url(r"/reports", MainHandler, {"page": "reports"}, name="reports"),

View File

@ -1,5 +1,5 @@
from wtforms.fields.html5 import IntegerField from wtforms.fields.html5 import IntegerField
from wtforms.fields import RadioField, StringField, SelectField, TextAreaField from wtforms.fields import RadioField, StringField, SelectField, TextAreaField, FormField
from wtforms.validators import Required, ValidationError from wtforms.validators import Required, ValidationError
from wtforms_tornado import Form from wtforms_tornado import Form
from .date import DateForm from .date import DateForm
@ -14,7 +14,7 @@ class RequestForm(Form):
('B','Option B'), ('B','Option B'),
('C','Option C') ]) ('C','Option C') ])
# no way to apply a label to a whole nested form like this # no way to apply a label to a whole nested form like this
date_start = DateForm() date_start = FormField(DateForm)
period_of_performance = SelectField('Desired period of performance', validators=[Required()], period_of_performance = SelectField('Desired period of performance', validators=[Required()],
choices=[('','- Select -'), choices=[('','- Select -'),
('value1','30 days'), ('value1','30 days'),
@ -31,6 +31,12 @@ class RequestForm(Form):
('both', 'Both') ]) ('both', 'Both') ])
number_of_cores = IntegerField('Number of cores', validators=[Required()]) number_of_cores = IntegerField('Number of cores', validators=[Required()])
total_ram = IntegerField('Total RAM', validators=[Required()]) total_ram = IntegerField('Total RAM', validators=[Required()])
object_storage = IntegerField('Total object storage', validators=[Required()])
server_storage = IntegerField('Total server storage', validators=[Required()])
total_active_users = IntegerField('Total active users', validators=[Required()])
total_peak_users = IntegerField('Total peak users', validators=[Required()])
total_requests = IntegerField('Total requests', validators=[Required()])
total_environments = IntegerField('Total environments', validators=[Required()])
# this is just an example validation; obviously this is wrong. # this is just an example validation; obviously this is wrong.
def validate_total_ram(self,field): def validate_total_ram(self,field):

View File

@ -26,7 +26,7 @@ class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self): def get_current_user(self):
if self.get_secure_cookie('atst'): if self.get_secure_cookie('atst'):
return True return '9cb348f0-8102-4962-88c4-dac8180c904c'
else: else:
return False return False

View File

@ -28,10 +28,27 @@ mock_requests = [
}, },
] ]
def map_request(request):
return {
'order_id': request['id'],
'is_new': False,
'status': 'Pending',
'app_count': 1,
'is_new': False,
'date': '',
'full_name': 'Richard Howard'
}
class Request(BaseHandler): class Request(BaseHandler):
def initialize(self, page): def initialize(self, page, requests_client):
self.page = page self.page = page
self.requests_client = requests_client
@tornado.web.authenticated @tornado.web.authenticated
@tornado.gen.coroutine
def get(self): def get(self):
self.render('requests.html.to', page = self.page, requests = mock_requests ) response = yield self.requests_client.get(
'/users/{}/requests'.format(self.get_current_user()))
requests = response.json['requests']
mapped_requests = [map_request(request) for request in requests]
self.render('requests.html.to', page=self.page, requests=mapped_requests)

View File

@ -6,6 +6,7 @@ from atst.forms.funding import FundingForm
from atst.forms.readiness import ReadinessForm from atst.forms.readiness import ReadinessForm
from atst.forms.review import ReviewForm from atst.forms.review import ReviewForm
import tornado.httputil import tornado.httputil
from tornado.httpclient import HTTPError
class RequestNew(BaseHandler): class RequestNew(BaseHandler):
@ -45,26 +46,62 @@ class RequestNew(BaseHandler):
self.requests_client = requests_client self.requests_client = requests_client
@tornado.web.authenticated @tornado.web.authenticated
def post(self, screen = 1): @tornado.gen.coroutine
def post(self, screen=1, request_id=None):
self.check_xsrf_cookie() self.check_xsrf_cookie()
screen = int(screen) screen = int(screen)
form = self.screens[ screen - 1 ]['form'](self.request.arguments) form = self.screens[ screen - 1 ]['form'](self.request.arguments)
if form.validate(): if form.validate():
where = self.application.default_router.reverse_url('request_form', str(screen + 1)) response = yield self.create_or_update_request(form.data, request_id)
self.redirect(where) if response.ok:
where = self.application.default_router.reverse_url(
'request_form_update', str(screen + 1), request_id or response.json['id'])
self.redirect(where)
else:
self.set_status(response.code)
else: else:
self.show_form(screen, form) self.show_form(screen, form)
@tornado.web.authenticated @tornado.web.authenticated
def get(self, screen = 1): @tornado.gen.coroutine
self.show_form(screen=screen) def get(self, screen=1, request_id=None):
form = None
if request_id:
request = yield self.get_request(request_id)
if request.ok:
form_data = request.json['body'] if request else {}
form = self.screens[ int(screen) - 1 ]['form'](data=form_data)
def show_form(self, screen = 1, form = None): self.show_form(screen=screen, form=form, request_id=request_id)
def show_form(self, screen=1, form=None, request_id=None):
if not form: if not form:
form = self.screens[ int(screen) - 1 ]['form'](self.request.arguments) form = self.screens[ int(screen) - 1 ]['form'](self.request.arguments)
self.render( 'requests/screen-%d.html.to' % int(screen), self.render('requests/screen-%d.html.to' % int(screen),
f = form, f=form,
page = self.page, page=self.page,
screens = self.screens, screens=self.screens,
current = int(screen), current=int(screen),
next_screen = int(screen) + 1 ) next_screen=int(screen) + 1,
request_id=request_id)
@tornado.gen.coroutine
def get_request(self, request_id):
request = yield self.requests_client.get(
'/users/{}/requests/{}'.format(self.get_current_user(), request_id),
raise_error=False)
return request
@tornado.gen.coroutine
def create_or_update_request(self, form_data, request_id=None):
request_data = {
'creator_id': self.get_current_user(),
'request': form_data
}
if request_id:
response = yield self.requests_client.patch(
'/requests/{}'.format(request_id), json=request_data)
else:
response = yield self.requests_client.post(
'/requests', json=request_data)
return response

View File

@ -20,7 +20,7 @@
<tbody> <tbody>
{% for r in requests %} {% for r in requests %}
<tr> <tr>
<th scope="row"><a href="#">#{{ r['order_id'] }}</a> <th scope="row"><a href="{{ reverse_url('request_form_update', 1, r['order_id']) }}">{{ r['order_id'] }}</a>
{% if r['is_new'] %}<span class="usa-label">New</span></th>{% end %} {% if r['is_new'] %}<span class="usa-label">New</span></th>{% end %}
<td>{{ r['date'] }}</td> <td>{{ r['date'] }}</td>
<td>{{ r['full_name'] }}</td> <td>{{ r['full_name'] }}</td>

View File

@ -5,8 +5,8 @@
<p class="usa-font-lead">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Natus error omnis a, tenetur similique quo officiis voluptates eum recusandae dolorem minus dignissimos, magni consequatur, maxime debitis reprehenderit sint non iusto?</p> <p class="usa-font-lead">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Natus error omnis a, tenetur similique quo officiis voluptates eum recusandae dolorem minus dignissimos, magni consequatur, maxime debitis reprehenderit sint non iusto?</p>
<a class='usa-button usa-button-secondary' href='{{ reverse_url('request_form',next_screen) }}'>New Application</a> <a class='usa-button usa-button-secondary' href='{{ reverse_url('request_form_new',next_screen) }}'>New Application</a>
<a class='usa-button usa-button-secondary' href='{{ reverse_url('request_form',next_screen) }}'>Existing Application</a> <a class='usa-button usa-button-secondary' href='{{ reverse_url('request_form_new',next_screen) }}'>Existing Application</a>
<a class='usa-button usa-button-secondary' href='{{ reverse_url('request_form',next_screen) }}'>Sandbox Environment</a> <a class='usa-button usa-button-secondary' href='{{ reverse_url('request_form_new',next_screen) }}'>Sandbox Environment</a>
{% end %} {% end %}

View File

@ -92,29 +92,25 @@
<h4 id="application-storage">Storage</h4> <h4 id="application-storage">Storage</h4>
<p>The particulars of your body copy will be determined by the topic of your page. Regardless of topic, its a good practice to follow the inverted pyramid structure when writing copy: Begin with the information thats most important to your users and then present information of less importance.</p> <p>The particulars of your body copy will be determined by the topic of your page. Regardless of topic, its a good practice to follow the inverted pyramid structure when writing copy: Begin with the information thats most important to your users and then present information of less importance.</p>
<label for="">Object storage</label> {{ f.object_storage.label }}
<input id="" name="" type="text" placeholder="Total object storage"> {{ f.object_storage(placeholder="Total object storage") }}
<label for="">Server storage</label>
<input id="" name="" type="text" placeholder="Total server storage">
{{ f.server_storage.label }}
{{ f.server_storage(placeholder="Total server storage") }}
<!-- Application Usage --> <!-- Application Usage -->
<h4 id="application-usage">Estimated Application Storage</h4> <h4 id="application-usage">Estimated Application Storage</h4>
<p>The particulars of your body copy will be determined by the topic of your page. Regardless of topic, its a good practice to follow the inverted pyramid structure when writing copy: Begin with the information thats most important to your users and then present information of less importance.</p> <p>The particulars of your body copy will be determined by the topic of your page. Regardless of topic, its a good practice to follow the inverted pyramid structure when writing copy: Begin with the information thats most important to your users and then present information of less importance.</p>
<label for="">Expected active users</label> {{ f.total_active_users.label }}
<input id="" name="" type="text" placeholder="Total active users"> {{ f.total_active_users(placeholder="Total active users") }}
<label for="">Expected peak concurrent users</label> {{ f.total_peak_users.label }}
<input id="" name="" type="text" placeholder="Total peak users"> {{ f.total_peak_users(placeholder="Total peak users") }}
<label for="">Expected requests per minute</label>
<input id="" name="" type="text" placeholder="Total requests">
<label for="">Number of application environments</label>
<input id="" name="" type="text" placeholder="Total number of environments">
{{ f.total_requests.label }}
{{ f.total_requests(placeholder="Total requests") }}
{{ f.total_environments.label }}
{{ f.total_environments(placeholder="Total number of environments") }}
{% end %} {% end %}

View File

@ -2,7 +2,7 @@
{% for i,s in enumerate(screens) %} {% for i,s in enumerate(screens) %}
<li> <li>
{% if i+1==current %} {% if i+1==current %}
<a class="usa-current" href="{{ reverse_url('request_form',i+1) }}"> <a class="usa-current" href="{{ reverse_url('request_form_update', i+1, request_id) if request_id else reverse_url('request_form_new',i+1) }}">
{{ i+1 }}. {{ s['title'] }} {{ i+1 }}. {{ s['title'] }}
</a> </a>
{% if s.get('subitems') %} {% if s.get('subitems') %}
@ -14,10 +14,10 @@
{% end %} {% end %}
{% else %} {% else %}
<a href='{{ reverse_url('request_form',i+1) }}'> <a href="{{ reverse_url('request_form_update', i+1, request_id) if request_id else reverse_url('request_form_new',i+1) }}">
{{ i+1 }}. {{ s['title'] }} {{ i+1 }}. {{ s['title'] }}
</a> </a>
{% end %} {% end %}
</li> </li>
{% end %} {% end %}
</ul> </ul>

View File

@ -14,7 +14,12 @@
<main class="main-content usa-width-two-thirds"> <main class="main-content usa-width-two-thirds">
<form method='POST' action='{{ reverse_url('request_form', current) }}'> {% if request_id %}
<form method='POST' action="{{ reverse_url('request_form_update', current, request_id) }}">
{% else %}
<form method='POST' action="{{ reverse_url('request_form_new', current) }}">
{% end %}
{% module xsrf_form_html() %} {% module xsrf_form_html() %}
{% block form %} {% block form %}
form goes here form goes here

View File

@ -1,14 +1,14 @@
import pytest import pytest
from atst.app import make_app, make_deps, make_config from atst.app import make_app, make_deps, make_config
from tests.mocks import MockApiClient from tests.mocks import MockApiClient, MockRequestsClient
@pytest.fixture @pytest.fixture
def app(): def app():
TEST_DEPS = { TEST_DEPS = {
'authz_client': MockApiClient('authz'), 'authz_client': MockApiClient('authz'),
'requests_client': MockApiClient('requests'), 'requests_client': MockRequestsClient('requests'),
'authnid_client': MockApiClient('authnid'), 'authnid_client': MockApiClient('authnid'),
} }

View File

@ -1,9 +1,9 @@
import re import re
import pytest import pytest
ERROR_CLASS = 'usa-input-error-message' ERROR_CLASS = 'usa-input-error-message'
@pytest.mark.gen_test @pytest.mark.gen_test
def test_submit_invalid_request_form(monkeypatch, http_client, base_url): def test_submit_invalid_request_form(monkeypatch, http_client, base_url):
monkeypatch.setattr('atst.handlers.request_new.RequestNew.get_current_user', lambda s: True) monkeypatch.setattr('atst.handlers.request_new.RequestNew.get_current_user', lambda s: True)
@ -18,11 +18,13 @@ def test_submit_invalid_request_form(monkeypatch, http_client, base_url):
assert response.effective_url == base_url + '/requests/new' assert response.effective_url == base_url + '/requests/new'
assert re.search(ERROR_CLASS, response.body.decode()) assert re.search(ERROR_CLASS, response.body.decode())
@pytest.mark.gen_test @pytest.mark.gen_test
def test_submit_valid_request_form(monkeypatch, http_client, base_url): def test_submit_valid_request_form(monkeypatch, http_client, base_url):
monkeypatch.setattr('atst.handlers.request_new.RequestNew.get_current_user', lambda s: True) monkeypatch.setattr('atst.handlers.request_new.RequestNew.get_current_user', lambda s: True)
monkeypatch.setattr('atst.handlers.request_new.RequestNew.check_xsrf_cookie', lambda s: True) monkeypatch.setattr('atst.handlers.request_new.RequestNew.check_xsrf_cookie', lambda s: True)
monkeypatch.setattr('atst.forms.request.RequestForm.validate', lambda s: True) monkeypatch.setattr('atst.forms.request.RequestForm.validate', lambda s: True)
# this just needs to send a known invalid form value # this just needs to send a known invalid form value
response = yield http_client.fetch( response = yield http_client.fetch(
base_url + "/requests/new", base_url + "/requests/new",
@ -30,4 +32,4 @@ def test_submit_valid_request_form(monkeypatch, http_client, base_url):
headers={"Content-Type": "application/x-www-form-urlencoded"}, headers={"Content-Type": "application/x-www-form-urlencoded"},
body="meaning=42", body="meaning=42",
) )
assert response.effective_url == base_url + '/requests/new/2' assert '/requests/new/2' in response.effective_url

View File

@ -1,8 +1,10 @@
import tornado.gen import tornado.gen
from tornado.httpclient import HTTPRequest, HTTPResponse from tornado.httpclient import HTTPRequest, HTTPResponse
from atst.api_client import ApiClient
class MockApiClient(object):
class MockApiClient(ApiClient):
def __init__(self, service): def __init__(self, service):
self.service = service self.service = service
@ -26,7 +28,35 @@ class MockApiClient(object):
def delete(self, path, **kwargs): def delete(self, path, **kwargs):
return self._get_response('DELETE', path) return self._get_response('DELETE', path)
def _get_response(self, verb, path): def _get_response(self, verb, path, code=200, json=None):
response = HTTPResponse(request=HTTPRequest(path, verb), code=200) response = HTTPResponse(
setattr(response, 'json', {}) request=HTTPRequest(path, verb),
code=code,
headers={'Content-Type': 'application/json'})
setattr(response, 'ok', 200 <= code < 300)
if json:
setattr(response, 'json', json)
return response return response
class MockRequestsClient(MockApiClient):
@tornado.gen.coroutine
def get(self, path, **kwargs):
json = {
'id': '66b8ef71-86d3-48ef-abc2-51bfa1732b6b',
'creator': '49903ae7-da4a-49bf-a6dc-9dff5d004238',
'body': {}
}
return self._get_response('GET', path, 200, json=json)
@tornado.gen.coroutine
def post(self, path, **kwargs):
json = {
'id': '66b8ef71-86d3-48ef-abc2-51bfa1732b6b',
'creator': '49903ae7-da4a-49bf-a6dc-9dff5d004238',
'body': {}
}
return self._get_response('POST', path, 202, json=json)