diff --git a/Pipfile b/Pipfile index 4110ccbf..620553f1 100644 --- a/Pipfile +++ b/Pipfile @@ -16,6 +16,7 @@ alembic = "*" flask = "*" flask-sqlalchemy = "*" flask-assets = "*" +flask-session = "*" [dev-packages] bandit = "*" diff --git a/Pipfile.lock b/Pipfile.lock index d6e5cf64..d55c6fcf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2ee6dd90ff3784e7b1781c680d690ac59118b4e3d72e8da3adf9e93d6e512bc7" + "sha256": "f097384512537988c799b892830b52e78bcc19133327213e9c6e2876210d62d3" }, "pipfile-spec": 6, "requires": { @@ -46,6 +46,14 @@ "index": "pypi", "version": "==0.12" }, + "flask-session": { + "hashes": [ + "sha256:a31c27e0c3287f00c825b3d9625aba585f4df4cccedb1e7dd5a69a215881a731", + "sha256:b9b32126bfc52c3169089f2ed9a40e34b589527bda48b633428e07d39d9c8792" + ], + "index": "pypi", + "version": "==0.3.1" + }, "flask-sqlalchemy": { "hashes": [ "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", diff --git a/atst/app.py b/atst/app.py index 087277fc..7dac78b7 100644 --- a/atst/app.py +++ b/atst/app.py @@ -2,15 +2,18 @@ import os import re from configparser import ConfigParser from redis import StrictRedis -from flask import Flask, request, g +from flask import Flask, request, g, session from unipath import Path from atst.api_client import ApiClient from atst.sessions import RedisSessions from atst.database import db from atst.assets import assets + from atst.routes import bp from atst.routes.workspaces import bp as workspace_routes +from atst.routes.requests import requests_bp + ENV = os.getenv("TORNADO_ENV", "dev") @@ -33,6 +36,7 @@ def make_app(config): app.register_blueprint(bp) app.register_blueprint(workspace_routes) + app.register_blueprint(requests_bp) return app @@ -48,7 +52,8 @@ def make_flask_callbacks(app): "id": "cce17030-4109-4719-b958-ed109dbb87c8", "first_name": "Amanda", "last_name": "Adamson", - "atat_role": "default" + "atat_role": "default", + "atat_permissions": [] } # TODO: Make me a macro diff --git a/atst/domain/requests.py b/atst/domain/requests.py index aa37b932..a513d660 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -4,6 +4,8 @@ from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.attributes import flag_modified from atst.models import Request, RequestStatusEvent +from atst.database import db + from .exceptions import NotFoundError @@ -28,67 +30,68 @@ def deep_merge(source, destination: dict): class Requests(object): AUTO_APPROVE_THRESHOLD = 1000000 - def __init__(self, db_session): - self.db_session = db_session - - def create(self, creator_id, body): + @classmethod + def create(cls, creator_id, body): request = Request(creator=creator_id, body=body) status_event = RequestStatusEvent(new_status="incomplete") request.status_events.append(status_event) - self.db_session.add(request) - self.db_session.commit() + db.session.add(request) + db.session.commit() return request - def exists(self, request_id, creator_id): - return self.db_session.query( + @classmethod + def exists(cls, request_id, creator_id): + return db.session.query( exists().where( and_(Request.id == request_id, Request.creator == creator_id) ) ).scalar() - def get(self, request_id): + @classmethod + def get(cls, request_id): 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: raise NotFoundError("request") return request - def get_many(self, creator_id=None): + @classmethod + def get_many(cls, creator_id=None): filters = [] if creator_id: filters.append(Request.creator == creator_id) requests = ( - self.db_session.query(Request) + db.session.query(Request) .filter(*filters) .order_by(Request.time_created.desc()) .all() ) return requests - @tornado.gen.coroutine - def submit(self, request): + @classmethod + def submit(cls, request): request.status_events.append(RequestStatusEvent(new_status="submitted")) if Requests.should_auto_approve(request): request.status_events.append(RequestStatusEvent(new_status="approved")) - self.db_session.add(request) - self.db_session.commit() + db.session.add(request) + db.session.commit() return request - @tornado.gen.coroutine - def update(self, request_id, request_delta): + @classmethod + def update(cls, request_id, request_delta): try: # 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 request = ( - self.db_session.query(Request) + db.session.query(Request) .filter_by(id=request_id) .with_for_update(of=Request) .one() @@ -105,8 +108,8 @@ class Requests(object): # since it doesn't track dictionary mutations by default. flag_modified(request, "body") - self.db_session.add(request) - self.db_session.commit() + db.session.add(request) + db.session.commit() @classmethod def should_auto_approve(cls, request): diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 93fde8e2..de181ce2 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -1,4 +1,7 @@ from flask import Blueprint, render_template, g +import pendulum + +from atst.domain.requests import Requests bp = Blueprint("atst", __name__) diff --git a/atst/routes/requests.py b/atst/routes/requests.py new file mode 100644 index 00000000..6c37344d --- /dev/null +++ b/atst/routes/requests.py @@ -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/", methods=["GET"]) +def requests_new(): + pass + + +@requests_bp.route("/requests/new//", methods=["GET"]) +def requests_form_update(): + pass + + +@requests_bp.route("/requests/verify/", methods=["GET"]) +def financial_verification(): + pass + + +@requests_bp.route("/requests/verify/", methods=["POST"]) +def update_financial_verification(): + pass diff --git a/templates/components.html b/templates/components.html index 0bc83991..04f6be36 100644 --- a/templates/components.html +++ b/templates/components.html @@ -31,7 +31,17 @@ {%- endmacro %} -{% macro EmptyState(self, message, actionLabel, actionHref, icon=None) -%} +{% macro Modal() -%} + +{%- endmacro %} + +{% macro EmptyState(message, actionLabel, actionHref, icon=None) -%}

{{ message }}

@@ -42,3 +52,41 @@ {{ actionLabel }}
{%- 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' + } +} %} + +
+ {{ Icon(levels.get(level).get('icon'), classes='alert__icon icon--large') }} + +
+

{{title}}

+ + {% if message %} +
{{ message | safe }}
+ {% endif %} + + {% if actions %} +
{{ actions | safe }}
+ {% endif %} +
+
+{%- endmacro %} diff --git a/templates/requests.html.to b/templates/requests.html similarity index 81% rename from templates/requests.html.to rename to templates/requests.html index bd0c9e8c..5d27ff1e 100644 --- a/templates/requests.html.to +++ b/templates/requests.html @@ -1,8 +1,10 @@ -{% extends "base.html.to" %} +{% extends "base.html" %} + +{% from "components.html" import Modal, Alert, EmptyState %} {% block modal %} - {% if modalOpen() %} - {% apply modal %} + {% if g.modalOpen %} + {% call Modal() %}

Your request is now approved!

@@ -17,34 +19,34 @@ usage in sync with your budget.

- {% module Alert("You'll need these details: ", + {{ Alert("You'll need these details: ", message="

Task Order Number

Contracting Officer: Name, E-mail and Office

" - ) %} + ) }} - {% end %} - {% end %} -{% end %} + {% endcall %} + {% endif %} +{% endblock %} {% block content %} {% if not requests %} - {% module EmptyState( + {{ EmptyState( 'There are currently no active requests for you to see.', actionLabel='Create a new JEDI Cloud Request', actionHref='/requests/new', icon='document' - )%} + ) }} {% else %} - {% module Alert('Pending Financial Verification', + {{ Alert('Pending Financial Verification', message="

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.

" - ) %} + ) }}
@@ -84,10 +86,10 @@ {% for r in requests %} - {{ r['order_id'] }} + {{ r['order_id'] }} {% if r['is_new'] %}New - {% end %} + {% endif %} {{ r['date'] }} {{ r['full_name'] }} {{ r['app_count'] }} @@ -97,13 +99,13 @@ Approval - {% end %} + {% endfor %}
-{% end %} +{% endif %} -{% end %} +{% endblock %}