Requests index rendering

This commit is contained in:
richard-dds 2018-08-01 14:17:43 -04:00
parent 3a53fc122d
commit 4ee662665e
8 changed files with 164 additions and 42 deletions

View File

@ -16,6 +16,7 @@ alembic = "*"
flask = "*" flask = "*"
flask-sqlalchemy = "*" flask-sqlalchemy = "*"
flask-assets = "*" flask-assets = "*"
flask-session = "*"
[dev-packages] [dev-packages]
bandit = "*" bandit = "*"

10
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "2ee6dd90ff3784e7b1781c680d690ac59118b4e3d72e8da3adf9e93d6e512bc7" "sha256": "f097384512537988c799b892830b52e78bcc19133327213e9c6e2876210d62d3"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -46,6 +46,14 @@
"index": "pypi", "index": "pypi",
"version": "==0.12" "version": "==0.12"
}, },
"flask-session": {
"hashes": [
"sha256:a31c27e0c3287f00c825b3d9625aba585f4df4cccedb1e7dd5a69a215881a731",
"sha256:b9b32126bfc52c3169089f2ed9a40e34b589527bda48b633428e07d39d9c8792"
],
"index": "pypi",
"version": "==0.3.1"
},
"flask-sqlalchemy": { "flask-sqlalchemy": {
"hashes": [ "hashes": [
"sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b",

View File

@ -2,15 +2,18 @@ import os
import re import re
from configparser import ConfigParser from configparser import ConfigParser
from redis import StrictRedis from redis import StrictRedis
from flask import Flask, request, g from flask import Flask, request, g, session
from unipath import Path from unipath import Path
from atst.api_client import ApiClient from atst.api_client import ApiClient
from atst.sessions import RedisSessions from atst.sessions import RedisSessions
from atst.database import db from atst.database import db
from atst.assets import assets from atst.assets import assets
from atst.routes import bp from atst.routes import bp
from atst.routes.workspaces import bp as workspace_routes from atst.routes.workspaces import bp as workspace_routes
from atst.routes.requests import requests_bp
ENV = os.getenv("TORNADO_ENV", "dev") ENV = os.getenv("TORNADO_ENV", "dev")
@ -33,6 +36,7 @@ def make_app(config):
app.register_blueprint(bp) app.register_blueprint(bp)
app.register_blueprint(workspace_routes) app.register_blueprint(workspace_routes)
app.register_blueprint(requests_bp)
return app return app
@ -48,7 +52,8 @@ def make_flask_callbacks(app):
"id": "cce17030-4109-4719-b958-ed109dbb87c8", "id": "cce17030-4109-4719-b958-ed109dbb87c8",
"first_name": "Amanda", "first_name": "Amanda",
"last_name": "Adamson", "last_name": "Adamson",
"atat_role": "default" "atat_role": "default",
"atat_permissions": []
} }
# TODO: Make me a macro # TODO: Make me a macro

View File

@ -4,6 +4,8 @@ from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from atst.models import Request, RequestStatusEvent from atst.models import Request, RequestStatusEvent
from atst.database import db
from .exceptions import NotFoundError from .exceptions import NotFoundError
@ -28,67 +30,68 @@ def deep_merge(source, destination: dict):
class Requests(object): class Requests(object):
AUTO_APPROVE_THRESHOLD = 1000000 AUTO_APPROVE_THRESHOLD = 1000000
def __init__(self, db_session): @classmethod
self.db_session = db_session def create(cls, creator_id, body):
def create(self, creator_id, body):
request = Request(creator=creator_id, body=body) request = Request(creator=creator_id, body=body)
status_event = RequestStatusEvent(new_status="incomplete") status_event = RequestStatusEvent(new_status="incomplete")
request.status_events.append(status_event) request.status_events.append(status_event)
self.db_session.add(request) db.session.add(request)
self.db_session.commit() db.session.commit()
return request return request
def exists(self, request_id, creator_id): @classmethod
return self.db_session.query( def exists(cls, request_id, creator_id):
return db.session.query(
exists().where( exists().where(
and_(Request.id == request_id, Request.creator == creator_id) and_(Request.id == request_id, Request.creator == creator_id)
) )
).scalar() ).scalar()
def get(self, request_id): @classmethod
def get(cls, request_id):
try: try:
request = self.db_session.query(Request).filter_by(id=request_id).one() request = db.session.query(Request).filter_by(id=request_id).one()
except NoResultFound: except NoResultFound:
raise NotFoundError("request") raise NotFoundError("request")
return request return request
def get_many(self, creator_id=None): @classmethod
def get_many(cls, creator_id=None):
filters = [] filters = []
if creator_id: if creator_id:
filters.append(Request.creator == creator_id) filters.append(Request.creator == creator_id)
requests = ( requests = (
self.db_session.query(Request) db.session.query(Request)
.filter(*filters) .filter(*filters)
.order_by(Request.time_created.desc()) .order_by(Request.time_created.desc())
.all() .all()
) )
return requests return requests
@tornado.gen.coroutine @classmethod
def submit(self, request): def submit(cls, request):
request.status_events.append(RequestStatusEvent(new_status="submitted")) request.status_events.append(RequestStatusEvent(new_status="submitted"))
if Requests.should_auto_approve(request): if Requests.should_auto_approve(request):
request.status_events.append(RequestStatusEvent(new_status="approved")) request.status_events.append(RequestStatusEvent(new_status="approved"))
self.db_session.add(request) db.session.add(request)
self.db_session.commit() db.session.commit()
return request return request
@tornado.gen.coroutine @classmethod
def update(self, request_id, request_delta): def update(cls, request_id, request_delta):
try: try:
# Query for request matching id, acquiring a row-level write lock. # Query for request matching id, acquiring a row-level write lock.
# https://www.postgresql.org/docs/10/static/sql-select.html#SQL-FOR-UPDATE-SHARE # https://www.postgresql.org/docs/10/static/sql-select.html#SQL-FOR-UPDATE-SHARE
request = ( request = (
self.db_session.query(Request) db.session.query(Request)
.filter_by(id=request_id) .filter_by(id=request_id)
.with_for_update(of=Request) .with_for_update(of=Request)
.one() .one()
@ -105,8 +108,8 @@ class Requests(object):
# since it doesn't track dictionary mutations by default. # since it doesn't track dictionary mutations by default.
flag_modified(request, "body") flag_modified(request, "body")
self.db_session.add(request) db.session.add(request)
self.db_session.commit() db.session.commit()
@classmethod @classmethod
def should_auto_approve(cls, request): def should_auto_approve(cls, request):

View File

@ -1,4 +1,7 @@
from flask import Blueprint, render_template, g from flask import Blueprint, render_template, g
import pendulum
from atst.domain.requests import Requests
bp = Blueprint("atst", __name__) bp = Blueprint("atst", __name__)

52
atst/routes/requests.py Normal file
View File

@ -0,0 +1,52 @@
from flask import Blueprint, g, render_template
import pendulum
from atst.domain.requests import Requests
requests_bp = Blueprint("requests", __name__)
def map_request(user, request):
time_created = pendulum.instance(request.time_created)
is_new = time_created.add(days=1) > pendulum.now()
return {
"order_id": request.id,
"is_new": is_new,
"status": request.status,
"app_count": 1,
"date": time_created.format("M/DD/YYYY"),
"full_name": "{} {}".format(user["first_name"], user["last_name"]),
}
@requests_bp.route("/requests", methods=["GET"])
def requests_index():
requests = []
if "review_and_approve_jedi_workspace_request" in g.current_user["atat_permissions"]:
requests = Requests.get_many()
else:
requests = Requests.get_many(creator_id=g.current_user["id"])
mapped_requests = [map_request(g.current_user, r) for r in requests]
return render_template("requests.html", requests=mapped_requests)
@requests_bp.route("/requests/new/<int:screen>", methods=["GET"])
def requests_new():
pass
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["GET"])
def requests_form_update():
pass
@requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"])
def financial_verification():
pass
@requests_bp.route("/requests/verify/<string:request_id>", methods=["POST"])
def update_financial_verification():
pass

View File

@ -31,7 +31,17 @@
</li> </li>
{%- endmacro %} {%- endmacro %}
{% macro EmptyState(self, message, actionLabel, actionHref, icon=None) -%} {% macro Modal() -%}
<div class='modal'>
<div class='modal__dialog' role='dialog' aria-modal='true'>
<div class='modal__body'>
{{ caller() }}
</div>
</div>
</div>
{%- endmacro %}
{% macro EmptyState(message, actionLabel, actionHref, icon=None) -%}
<div class='empty-state'> <div class='empty-state'>
<p>{{ message }}</p> <p>{{ message }}</p>
@ -42,3 +52,41 @@
<a href='{{ actionHref }}' class='usa-button usa-button-big'>{{ actionLabel }}</a> <a href='{{ actionHref }}' class='usa-button usa-button-big'>{{ actionLabel }}</a>
</div> </div>
{%- endmacro %} {%- endmacro %}
{% macro Alert(title, message=None, actions=None, level='info') -%}
{% set role = 'alertdialog' if actions else 'alert' %}
{% set levels = {
'warning': {
'icon': 'alert',
'tone': 'assertive'
},
'error': {
'icon': 'alert',
'tone': 'assertive'
},
'info': {
'icon': 'info',
'tone': 'polite'
},
'success': {
'icon': 'ok',
'tone': 'polite'
}
} %}
<div class='alert alert--{{level}}' role='{{role}}' aria-live='{{levels.get(level).get('tone')}}'>
{{ Icon(levels.get(level).get('icon'), classes='alert__icon icon--large') }}
<div class='alert__content'>
<h2 class='alert__title'>{{title}}</h2>
{% if message %}
<div class='alert__message'>{{ message | safe }}</div>
{% endif %}
{% if actions %}
<div class='alert__actions'>{{ actions | safe }}</div>
{% endif %}
</div>
</div>
{%- endmacro %}

View File

@ -1,8 +1,10 @@
{% extends "base.html.to" %} {% extends "base.html" %}
{% from "components.html" import Modal, Alert, EmptyState %}
{% block modal %} {% block modal %}
{% if modalOpen() %} {% if g.modalOpen %}
{% apply modal %} {% call Modal() %}
<h1>Your request is now approved!</h1> <h1>Your request is now approved!</h1>
<p> <p>
@ -17,34 +19,34 @@
usage in sync with your budget. usage in sync with your budget.
</p> </p>
{% module Alert("You'll need these details: ", {{ Alert("You'll need these details: ",
message="<p>Task Order Number</p><p>Contracting Officer: Name, E-mail and Office</p>" message="<p>Task Order Number</p><p>Contracting Officer: Name, E-mail and Office</p>"
) %} ) }}
<div class='action-group'> <div class='action-group'>
<a href='/requests' class='action-group__action usa-button'>Close</a> <a href='/requests' class='action-group__action usa-button'>Close</a>
</div> </div>
{% end %} {% endcall %}
{% end %} {% endif %}
{% end %} {% endblock %}
{% block content %} {% block content %}
{% if not requests %} {% if not requests %}
{% module EmptyState( {{ EmptyState(
'There are currently no active requests for you to see.', 'There are currently no active requests for you to see.',
actionLabel='Create a new JEDI Cloud Request', actionLabel='Create a new JEDI Cloud Request',
actionHref='/requests/new', actionHref='/requests/new',
icon='document' icon='document'
)%} ) }}
{% else %} {% else %}
{% module Alert('Pending Financial Verification', {{ Alert('Pending Financial Verification',
message="<p>Your next step is to create a Task Order (T.O.) associated with JEDI Cloud. Please consult a Contracting Officer (KO) or Contracting Officer Representative (COR) to help with this step.</p>" message="<p>Your next step is to create a Task Order (T.O.) associated with JEDI Cloud. Please consult a Contracting Officer (KO) or Contracting Officer Representative (COR) to help with this step.</p>"
) %} ) }}
<div class="col col--grow"> <div class="col col--grow">
@ -84,10 +86,10 @@
{% for r in requests %} {% for r in requests %}
<tr> <tr>
<th scope="row"> <th scope="row">
<a class='icon-link icon-link--large' href="{{ reverse_url('request_form_update', 1, r['order_id']) if r["status"] != "approved" else reverse_url('financial_verification', r['order_id']) }}">{{ r['order_id'] }}</a> <a class='icon-link icon-link--large' href="{{ url_for('requests.requests_form_update', screen=1, request_id=r['order_id']) if r["status"] != "approved" else url_for('requests.financial_verification', request_id=r['order_id']) }}">{{ r['order_id'] }}</a>
{% if r['is_new'] %}<span class="usa-label">New</span> {% if r['is_new'] %}<span class="usa-label">New</span>
</th> </th>
{% end %} {% endif %}
<td>{{ r['date'] }}</td> <td>{{ r['date'] }}</td>
<td>{{ r['full_name'] }}</td> <td>{{ r['full_name'] }}</td>
<td>{{ r['app_count'] }}</td> <td>{{ r['app_count'] }}</td>
@ -97,13 +99,13 @@
<a href="/request/approval" class='icon-link'>Approval</a> <a href="/request/approval" class='icon-link'>Approval</a>
</td> </td>
</tr> </tr>
{% end %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
{% end %} {% endif %}
{% end %} {% endblock %}