Requests index rendering
This commit is contained in:
parent
3a53fc122d
commit
4ee662665e
1
Pipfile
1
Pipfile
@ -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
10
Pipfile.lock
generated
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
52
atst/routes/requests.py
Normal 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
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user