From c3b79103a4e00248b236cf911e0c124a717d6cef Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 19 Feb 2019 10:51:07 -0500 Subject: [PATCH 01/25] Remove Requests routes --- atst/app.py | 2 - atst/routes/requests/__init__.py | 15 - atst/routes/requests/approval.py | 97 ---- .../routes/requests/financial_verification.py | 291 ---------- atst/routes/requests/index.py | 107 ---- atst/routes/requests/jedi_request_flow.py | 162 ------ atst/routes/requests/requests_form.py | 187 ------ .../requests/requests_form/test_details.py | 39 -- .../requests/requests_form/test_edit.py | 52 -- .../routes/requests/requests_form/test_new.py | 249 -------- .../requests/requests_form/test_submit.py | 42 -- tests/routes/requests/test_approval.py | 201 ------- .../requests/test_financial_verification.py | 543 ------------------ tests/routes/requests/test_requests_index.py | 28 - tests/test_integration.py | 84 --- 15 files changed, 2099 deletions(-) delete mode 100644 atst/routes/requests/__init__.py delete mode 100644 atst/routes/requests/approval.py delete mode 100644 atst/routes/requests/financial_verification.py delete mode 100644 atst/routes/requests/index.py delete mode 100644 atst/routes/requests/jedi_request_flow.py delete mode 100644 atst/routes/requests/requests_form.py delete mode 100644 tests/routes/requests/requests_form/test_details.py delete mode 100644 tests/routes/requests/requests_form/test_edit.py delete mode 100644 tests/routes/requests/requests_form/test_new.py delete mode 100644 tests/routes/requests/requests_form/test_submit.py delete mode 100644 tests/routes/requests/test_approval.py delete mode 100644 tests/routes/requests/test_financial_verification.py delete mode 100644 tests/routes/requests/test_requests_index.py delete mode 100644 tests/test_integration.py diff --git a/atst/app.py b/atst/app.py index 2f27b661..19e09ca2 100644 --- a/atst/app.py +++ b/atst/app.py @@ -13,7 +13,6 @@ from atst.assets import environment as assets_environment from atst.filters import register_filters from atst.routes import bp from atst.routes.portfolios import portfolios_bp as portfolio_routes -from atst.routes.requests import requests_bp from atst.routes.task_orders import task_orders_bp from atst.routes.dev import bp as dev_routes from atst.routes.users import bp as user_routes @@ -68,7 +67,6 @@ def make_app(config): app.register_blueprint(portfolio_routes) app.register_blueprint(task_orders_bp) app.register_blueprint(user_routes) - app.register_blueprint(requests_bp) if ENV != "prod": app.register_blueprint(dev_routes) diff --git a/atst/routes/requests/__init__.py b/atst/routes/requests/__init__.py deleted file mode 100644 index d4214f56..00000000 --- a/atst/routes/requests/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Blueprint - -from atst.domain.requests import Requests - -requests_bp = Blueprint("requests", __name__) - -from . import index -from . import requests_form -from . import financial_verification -from . import approval - - -@requests_bp.context_processor -def annual_spend_threshold(): - return {"annual_spend_threshold": Requests.ANNUAL_SPEND_THRESHOLD} diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py deleted file mode 100644 index 9ec31bc9..00000000 --- a/atst/routes/requests/approval.py +++ /dev/null @@ -1,97 +0,0 @@ -from flask import ( - render_template, - g, - Response, - request as http_request, - redirect, - url_for, -) -from flask import current_app as app - -from . import requests_bp -from atst.domain.requests import Requests -from atst.domain.exceptions import NotFoundError -from atst.forms.ccpo_review import CCPOReviewForm -from atst.forms.internal_comment import InternalCommentForm - - -def map_ccpo_authorizing(user): - return {"fname_ccpo": user.first_name, "lname_ccpo": user.last_name} - - -def render_approval(request, form=None, internal_comment_form=None): - data = request.body - if request.has_financial_data: - data["legacy_task_order"] = request.legacy_task_order.to_dictionary() - - if not form: - mo_data = map_ccpo_authorizing(g.current_user) - form = CCPOReviewForm(data=mo_data) - - if not internal_comment_form: - internal_comment_form = InternalCommentForm() - - return render_template( - "requests/approval.html", - data=data, - reviews=list(reversed(request.reviews)), - jedi_request=request, - current_status=request.status.value, - review_form=form or CCPOReviewForm(), - internal_comment_form=internal_comment_form, - comments=request.internal_comments, - ) - - -@requests_bp.route("/requests/approval/", methods=["GET"]) -def approval(request_id): - request = Requests.get_for_approval(g.current_user, request_id) - - return render_approval(request) - - -@requests_bp.route("/requests/submit_approval/", methods=["POST"]) -def submit_approval(request_id): - request = Requests.get_for_approval(g.current_user, request_id) - - form = CCPOReviewForm(http_request.form) - if form.validate(): - if http_request.form.get("review") == "approving": - Requests.advance(g.current_user, request, form.data) - else: - Requests.request_changes(g.current_user, request, form.data) - - return redirect(url_for("requests.requests_index")) - else: - return render_approval(request, form) - - -@requests_bp.route("/requests/task_order_download/", methods=["GET"]) -def task_order_pdf_download(request_id): - request = Requests.get(g.current_user, request_id) - if request.legacy_task_order and request.legacy_task_order.pdf: - pdf = request.legacy_task_order.pdf - generator = app.csp.files.download(pdf.object_name) - return Response( - generator, - headers={ - "Content-Disposition": "attachment; filename={}".format(pdf.filename) - }, - mimetype="application/pdf", - ) - - else: - raise NotFoundError("legacy_task_order pdf") - - -@requests_bp.route("/requests/internal_comments/", methods=["POST"]) -def create_internal_comment(request_id): - form = InternalCommentForm(http_request.form) - request = Requests.get(g.current_user, request_id) - if form.validate(): - Requests.add_internal_comment(g.current_user, request, form.data.get("text")) - return redirect( - url_for("requests.approval", request_id=request_id, _anchor="ccpo-notes") - ) - else: - return render_approval(request, internal_comment_form=form) diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py deleted file mode 100644 index 970d85a4..00000000 --- a/atst/routes/requests/financial_verification.py +++ /dev/null @@ -1,291 +0,0 @@ -from flask import g, render_template, redirect, url_for -from flask import request as http_request -from werkzeug.datastructures import ImmutableMultiDict, FileStorage - -from . import requests_bp -from atst.domain.requests import Requests -from atst.forms.financial import FinancialVerificationForm -from atst.forms.exceptions import FormValidationError -from atst.domain.exceptions import NotFoundError -from atst.domain.requests.financial_verification import ( - PENumberValidator, - TaskOrderNumberValidator, -) -from atst.models.attachment import Attachment -from atst.domain.legacy_task_orders import LegacyTaskOrders -from atst.utils.flash import formatted_flash as flash - - -def fv_extended(_http_request): - return _http_request.args.get("extended", "false").lower() in ["true", "t"] - - -class FinancialVerification(object): - def __init__(self, request): - self.request = request.latest_revision - self.legacy_task_order = request.legacy_task_order - - -class FinancialVerificationBase(object): - def _get_form(self, request, is_extended, formdata=None): - _formdata = ImmutableMultiDict(formdata) if formdata is not None else None - fv = FinancialVerification(request) - form = FinancialVerificationForm(obj=fv, formdata=_formdata) - - if not form.has_pdf_upload: - if isinstance(form.legacy_task_order.pdf.data, Attachment): - form.legacy_task_order.pdf.data = ( - form.legacy_task_order.pdf.data.filename - ) - else: - try: - attachment = Attachment.get_for_resource( - "legacy_task_order", self.request.id - ) - form.legacy_task_order.pdf.data = attachment.filename - except NotFoundError: - pass - - return form - - def _process_attachment(self, is_extended, form): - attachment = None - if is_extended: - attachment = None - if isinstance(form.legacy_task_order.pdf.data, FileStorage): - Attachment.delete_for_resource("legacy_task_order", self.request.id) - attachment = Attachment.attach( - form.legacy_task_order.pdf.data, - "legacy_task_order", - self.request.id, - ) - elif isinstance(form.legacy_task_order.pdf.data, str): - try: - attachment = Attachment.get_for_resource( - "legacy_task_order", self.request.id - ) - except NotFoundError: - pass - - if attachment: - form.legacy_task_order.pdf.data = attachment.filename - - return attachment - - def _try_create_task_order(self, form, attachment, is_extended): - task_order_number = form.legacy_task_order.number.data - if not task_order_number: - return None - - task_order_data = form.legacy_task_order.data - - if attachment: - task_order_data["pdf"] = attachment - - try: - legacy_task_order = LegacyTaskOrders.get(task_order_number) - legacy_task_order = LegacyTaskOrders.update( - legacy_task_order, task_order_data - ) - return legacy_task_order - except NotFoundError: - pass - - try: - return LegacyTaskOrders.get_from_eda(task_order_number) - except NotFoundError: - pass - - return LegacyTaskOrders.create(**task_order_data) - - def _raise(self, form): - form.reset() - raise FormValidationError(form) - - -class GetFinancialVerificationForm(FinancialVerificationBase): - def __init__(self, user, request, is_extended=False): - self.user = user - self.request = request - self.is_extended = is_extended - - def execute(self): - form = self._get_form(self.request, self.is_extended) - form.reset() - return form - - -class UpdateFinancialVerification(FinancialVerificationBase): - def __init__( - self, - pe_validator, - task_order_validator, - user, - request, - fv_data, - is_extended=False, - ): - self.pe_validator = pe_validator - self.task_order_validator = task_order_validator - self.user = user - self.request = request - self.fv_data = fv_data - self.is_extended = is_extended - - def execute(self): - form = self._get_form(self.request, self.is_extended, self.fv_data) - - should_update = True - should_submit = True - updated_request = None - - attachment = self._process_attachment(self.is_extended, form) - - if not form.validate(is_extended=self.is_extended, has_attachment=attachment): - should_update = False - - if not self.pe_validator.validate(self.request, form.pe_id): - should_submit = False - - if not self.is_extended and not self.task_order_validator.validate( - form.legacy_task_order.number - ): - should_submit = False - - if should_update: - legacy_task_order = self._try_create_task_order( - form, attachment, self.is_extended - ) - updated_request = Requests.update_financial_verification( - self.request.id, form.request.data, legacy_task_order=legacy_task_order - ) - if should_submit: - return Requests.submit_financial_verification(updated_request) - - self._raise(form) - - -class SaveFinancialVerificationDraft(FinancialVerificationBase): - def __init__( - self, - pe_validator, - task_order_validator, - user, - request, - fv_data, - is_extended=False, - ): - self.pe_validator = pe_validator - self.task_order_validator = task_order_validator - self.user = user - self.request = request - self.fv_data = fv_data - self.is_extended = is_extended - - def execute(self): - form = self._get_form(self.request, self.is_extended, self.fv_data) - attachment = self._process_attachment(self.is_extended, form) - legacy_task_order = self._try_create_task_order( - form, attachment, self.is_extended - ) - updated_request = Requests.update_financial_verification( - self.request.id, form.request.data, legacy_task_order=legacy_task_order - ) - - return updated_request - - -@requests_bp.route("/requests/verify//draft", methods=["GET"]) -@requests_bp.route("/requests/verify/", methods=["GET"]) -def financial_verification(request_id): - request = Requests.get(g.current_user, request_id) - is_extended = fv_extended(http_request) - saved_draft = http_request.args.get("saved_draft", False) - - should_be_extended = not is_extended and request.has_manual_task_order - if should_be_extended: - return redirect( - url_for(".financial_verification", request_id=request_id, extended=True) - ) - - form = GetFinancialVerificationForm( - g.current_user, request, is_extended=is_extended - ).execute() - - if request.review_comment: - flash("request_review_comment", comment=request.review_comment) - - return render_template( - "requests/financial_verification.html", - f=form, - jedi_request=request, - extended=is_extended, - saved_draft=saved_draft, - ) - - -@requests_bp.route("/requests/verify/", methods=["POST"]) -def update_financial_verification(request_id): - request = Requests.get(g.current_user, request_id) - fv_data = {**http_request.form, **http_request.files} - is_extended = fv_extended(http_request) - - try: - updated_request = UpdateFinancialVerification( - PENumberValidator(), - TaskOrderNumberValidator(), - g.current_user, - request, - fv_data, - is_extended=is_extended, - ).execute() - except FormValidationError as e: - return render_template( - "requests/financial_verification.html", - jedi_request=request, - f=e.form, - extended=is_extended, - ) - - if updated_request.legacy_task_order.verified: - portfolio = Requests.auto_approve_and_create_portfolio(updated_request) - flash("new_portfolio") - return redirect( - url_for("portfolios.new_application", portfolio_id=portfolio.id) - ) - else: - return redirect(url_for("requests.requests_index", modal="pendingCCPOApproval")) - - -@requests_bp.route("/requests/verify//draft", methods=["POST"]) -def save_financial_verification_draft(request_id): - user = g.current_user - request = Requests.get(user, request_id) - fv_data = {**http_request.form, **http_request.files} - is_extended = fv_extended(http_request) - - try: - updated_request = SaveFinancialVerificationDraft( - PENumberValidator(), - TaskOrderNumberValidator(), - user, - request, - fv_data, - is_extended=is_extended, - ).execute() - except FormValidationError as e: - return render_template( - "requests/financial_verification.html", - jedi_request=request, - f=e.form, - extended=is_extended, - ) - - return redirect( - url_for( - "requests.financial_verification", - request_id=updated_request.id, - is_extended=is_extended, - saved_draft=True, - ) - ) diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py deleted file mode 100644 index eb0c7204..00000000 --- a/atst/routes/requests/index.py +++ /dev/null @@ -1,107 +0,0 @@ -import pendulum -from flask import render_template, g, url_for - -from . import requests_bp -from atst.domain.requests import Requests -from atst.models.permissions import Permissions -from atst.forms.data import SERVICE_BRANCHES -from atst.utils.flash import formatted_flash as flash - - -class RequestsIndex(object): - def __init__(self, user): - self.user = user - - def execute(self): - if ( - Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST - in self.user.atat_permissions - ): - context = self._ccpo_view(self.user) - - else: - context = self._non_ccpo_view(self.user) - - return { - **context, - "possible_statuses": Requests.possible_statuses(), - "possible_dod_components": [b[0] for b in SERVICE_BRANCHES[1:]], - } - - def _ccpo_view(self, user): - requests = Requests.get_many() - mapped_requests = [self._map_request(r, "ccpo") for r in requests] - num_action_required = len( - [r for r in mapped_requests if r.get("action_required")] - ) - - return { - "requests": mapped_requests, - "pending_financial_verification": False, - "pending_ccpo_acceptance": False, - "extended_view": True, - "kpi_inprogress": Requests.in_progress_count(), - "kpi_pending": Requests.pending_ccpo_count(), - "kpi_completed": Requests.completed_count(), - "num_action_required": num_action_required, - } - - def _non_ccpo_view(self, user): - requests = Requests.get_many(creator=user) - mapped_requests = [self._map_request(r, "mission_owner") for r in requests] - num_action_required = len( - [r for r in mapped_requests if r.get("action_required")] - ) - pending_fv = any(r.is_pending_financial_verification for r in requests) - pending_ccpo = any(r.is_pending_ccpo_acceptance for r in requests) - - return { - "requests": mapped_requests, - "pending_financial_verification": pending_fv, - "pending_ccpo_acceptance": pending_ccpo, - "num_action_required": num_action_required, - "extended_view": False, - } - - def _portfolio_link_for_request(self, request): - if request.is_approved: - return url_for( - "portfolios.portfolio_applications", portfolio_id=request.portfolio.id - ) - else: - return None - - def _map_request(self, request, viewing_role): - time_created = pendulum.instance(request.time_created) - is_new = time_created.add(days=1) > pendulum.now() - app_count = request.body.get("details_of_use", {}).get( - "num_software_systems", 0 - ) - annual_usage = request.annual_spend - - return { - "portfolio_id": request.portfolio.id if request.portfolio else None, - "name": request.displayname, - "is_new": is_new, - "is_approved": request.is_approved, - "status": request.status_displayname, - "app_count": app_count, - "last_submission_timestamp": request.last_submission_timestamp, - "last_edited_timestamp": request.latest_revision.time_updated, - "full_name": request.creator.full_name, - "annual_usage": annual_usage, - "edit_link": url_for("requests.edit", request_id=request.id), - "action_required": request.action_required_by == viewing_role, - "dod_component": request.latest_revision.dod_component, - "portfolio_link": self._portfolio_link_for_request(request), - } - - -@requests_bp.route("/requests", methods=["GET"]) -def requests_index(): - context = RequestsIndex(g.current_user).execute() - - if context.get("num_action_required"): - flash("requests_action_required", count=context.get("num_action_required")) - - return render_template("requests/index.html", **context) diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py deleted file mode 100644 index 269ff632..00000000 --- a/atst/routes/requests/jedi_request_flow.py +++ /dev/null @@ -1,162 +0,0 @@ -from collections import defaultdict - -from atst.domain.requests import Requests -import atst.forms.new_request as request_forms - - -class JEDIRequestFlow(object): - def __init__( - self, - current_step, - current_user=None, - request=None, - post_data=None, - request_id=None, - existing_request=None, - ): - self.current_step = current_step - - self.current_user = current_user - self.request = request - - self.post_data = post_data - self.is_post = self.post_data is not None - - self.request_id = request_id - self.form = self._form() - - self.existing_request = existing_request - - def _form(self): - if self.is_post: - return self.form_class()(self.post_data) - else: - return self.form_class()(data=self.current_step_data) - - def validate(self): - return self.form.validate() - - def validate_warnings(self): - existing_request_data = ( - self.existing_request and self.existing_request.body.get(self.form_section) - ) or None - - valid = self.form.perform_extra_validation(existing_request_data) - return valid - - @property - def current_screen(self): - return self.screens[self.current_step - 1] - - @property - def form_section(self): - return self.current_screen["section"] - - def form_class(self): - return self.current_screen["form"] - - # maps user data to fields in InformationAboutYouForm; this should be moved - # into the request initialization process when we have a request schema, or - # we just shouldn't record this data on the request - def map_user_data(self, user): - return { - "fname_request": user.first_name, - "lname_request": user.last_name, - "email_request": user.email, - "phone_number": user.phone_number, - "phone_ext": user.phone_ext, - "service_branch": user.service_branch, - "designation": user.designation, - "citizenship": user.citizenship, - "date_latest_training": user.date_latest_training, - } - - @property - def current_step_data(self): - data = {} - - if self.is_post: - data = self.post_data - - if self.request: - if self.form_section == "review_submit": - 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} - else: - data = self.request.body.get(self.form_section, {}) - elif self.form_section == "information_about_you": - data = self.map_user_data(self.current_user) - - return defaultdict(lambda: defaultdict(lambda: None), data) - - @property - def can_submit(self): - return self.request and Requests.should_allow_submission(self.request) - - @property - def next_screen(self): - return self.current_step + 1 - - @property - def screens(self): - return [ - { - "title": "Details of Use", - "section": "details_of_use", - "form": request_forms.DetailsOfUseForm, - }, - { - "title": "Information About You", - "section": "information_about_you", - "form": request_forms.InformationAboutYouForm, - }, - { - "title": "Portfolio Owner", - "section": "primary_poc", - "form": request_forms.PortfolioOwnerForm, - }, - { - "title": "Review & Submit", - "section": "review_submit", - "form": request_forms.ReviewAndSubmitForm, - }, - ] - - @property - def is_review_screen(self): - return self.screens[-1] == self.current_screen - - def create_or_update_request(self): - 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): - try: - request_user_info = self.existing_request.body.get( - "information_about_you", {} - ) - except AttributeError: - request_user_info = {} - - data = { - **data, - "dodid_poc": self.current_user.dod_id, - "fname_poc": request_user_info.get( - "fname_request", self.current_user.first_name - ), - "lname_poc": request_user_info.get( - "lname_request", self.current_user.last_name - ), - "email_poc": request_user_info.get( - "email_request", self.current_user.email - ), - } - return {section: data} diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py deleted file mode 100644 index 8d8f17a4..00000000 --- a/atst/routes/requests/requests_form.py +++ /dev/null @@ -1,187 +0,0 @@ -from flask import g, redirect, render_template, url_for, request as http_request - -from . import requests_bp -from atst.domain.requests import Requests -from atst.domain.authz import Authorization -from atst.routes.requests.jedi_request_flow import JEDIRequestFlow -from atst.models.request_status_event import RequestStatus -from atst.forms.data import ( - SERVICE_BRANCHES, - ASSISTANCE_ORG_TYPES, - DATA_TRANSFER_AMOUNTS, - COMPLETION_DATE_RANGES, - FUNDING_TYPES, - TASK_ORDER_SOURCES, -) -from atst.utils.flash import formatted_flash as flash - - -@requests_bp.context_processor -def option_data(): - return { - "service_branches": SERVICE_BRANCHES, - "assistance_org_types": ASSISTANCE_ORG_TYPES, - "data_transfer_amounts": DATA_TRANSFER_AMOUNTS, - "completion_date_ranges": COMPLETION_DATE_RANGES, - "funding_types": FUNDING_TYPES, - "task_order_sources": TASK_ORDER_SOURCES, - } - - -@requests_bp.route("/requests/new/", methods=["GET"]) -def requests_form_new(screen): - jedi_flow = JEDIRequestFlow(screen, request=None, current_user=g.current_user) - - if jedi_flow.is_review_screen and not jedi_flow.can_submit: - flash("request_incomplete") - - return render_template( - "requests/screen-%d.html" % int(screen), - f=jedi_flow.form, - data=jedi_flow.current_step_data, - screens=jedi_flow.screens, - current=screen, - next_screen=screen + 1, - can_submit=jedi_flow.can_submit, - ) - - -@requests_bp.route( - "/requests/new/", methods=["GET"], defaults={"request_id": None} -) -@requests_bp.route("/requests/new//", methods=["GET"]) -def requests_form_update(screen=1, request_id=None): - request = ( - Requests.get(g.current_user, request_id) if request_id is not None else None - ) - jedi_flow = JEDIRequestFlow( - screen, request=request, request_id=request_id, current_user=g.current_user - ) - - if jedi_flow.is_review_screen and not jedi_flow.can_submit: - flash("request_incomplete") - - if request.review_comment: - flash("request_review_comment", comment=request.review_comment) - - return render_template( - "requests/screen-%d.html" % int(screen), - f=jedi_flow.form, - data=jedi_flow.current_step_data, - screens=jedi_flow.screens, - current=screen, - next_screen=screen + 1, - request_id=request_id, - jedi_request=jedi_flow.request, - can_submit=jedi_flow.can_submit, - ) - - -@requests_bp.route( - "/requests/new/", methods=["POST"], defaults={"request_id": None} -) -@requests_bp.route("/requests/new//", methods=["POST"]) -def requests_update(screen=1, request_id=None): - screen = int(screen) - post_data = http_request.form - current_user = g.current_user - existing_request = ( - Requests.get(g.current_user, request_id) if request_id is not None else None - ) - jedi_flow = JEDIRequestFlow( - screen, - post_data=post_data, - request_id=request_id, - current_user=current_user, - existing_request=existing_request, - ) - - has_next_screen = jedi_flow.next_screen <= len(jedi_flow.screens) - valid = jedi_flow.validate() and jedi_flow.validate_warnings() - - if valid: - jedi_flow.create_or_update_request() - - 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) - - -@requests_bp.route("/requests/submit/", methods=["POST"]) -def requests_submit(request_id=None): - request = Requests.get(g.current_user, request_id) - Requests.submit(request) - - if request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION: - modal = "pendingFinancialVerification" - else: - modal = "pendingCCPOAcceptance" - - return redirect(url_for("requests.requests_index", modal=modal)) - - -@requests_bp.route("/requests/details/", methods=["GET"]) -def view_request_details(request_id=None): - request = Requests.get(g.current_user, request_id) - requires_fv_action = ( - request.is_pending_financial_verification - or request.is_pending_financial_verification_changes - ) - - data = request.body - if request.has_financial_data: - data["legacy_task_order"] = request.legacy_task_order.to_dictionary() - - return render_template( - "requests/details.html", - data=data, - jedi_request=request, - requires_fv_action=requires_fv_action, - ) - - -@requests_bp.route("/requests/edit/") -def edit(request_id): - user = g.current_user - request = Requests.get(user, request_id) - is_ccpo = Authorization.is_ccpo(user) - - redirect_url = "" - - if request.creator == user: - if request.is_pending_financial_verification: - redirect_url = url_for( - "requests.financial_verification", request_id=request.id - ) - elif request.is_pending_financial_verification_changes: - redirect_url = url_for( - "requests.financial_verification", request_id=request.id, extended=True - ) - elif request.is_approved: - redirect_url = url_for( - "requests.view_request_details", request_id=request.id - ) - else: - redirect_url = url_for( - "requests.requests_form_update", screen=1, request_id=request.id - ) - elif is_ccpo: - redirect_url = url_for("requests.approval", request_id=request.id) - - return redirect(redirect_url) diff --git a/tests/routes/requests/requests_form/test_details.py b/tests/routes/requests/requests_form/test_details.py deleted file mode 100644 index ddeade54..00000000 --- a/tests/routes/requests/requests_form/test_details.py +++ /dev/null @@ -1,39 +0,0 @@ -import re -from flask import url_for - -from atst.models.request_status_event import RequestStatus - -from tests.factories import RequestFactory, LegacyTaskOrderFactory, UserFactory - - -def test_can_show_financial_data(client, user_session): - user = UserFactory.create() - user_session(user) - - legacy_task_order = LegacyTaskOrderFactory.create() - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_APPROVAL, - legacy_task_order=legacy_task_order, - creator=user, - ) - response = client.get( - url_for("requests.view_request_details", request_id=request.id) - ) - - body = response.data.decode() - assert re.search(r">\s+Financial Verification\s+<", body) - - -def test_can_not_show_financial_data(client, user_session): - user = UserFactory.create() - user_session(user) - - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_ACCEPTANCE, creator=user - ) - response = client.get( - url_for("requests.view_request_details", request_id=request.id) - ) - - body = response.data.decode() - assert not re.search(r">\s+Financial Verification\s+<", body) diff --git a/tests/routes/requests/requests_form/test_edit.py b/tests/routes/requests/requests_form/test_edit.py deleted file mode 100644 index 37e7b243..00000000 --- a/tests/routes/requests/requests_form/test_edit.py +++ /dev/null @@ -1,52 +0,0 @@ -from tests.factories import UserFactory, RequestFactory -from atst.models.request_status_event import RequestStatus - - -def test_creator_pending_finver(client, user_session): - request = RequestFactory.create_with_status( - RequestStatus.PENDING_FINANCIAL_VERIFICATION - ) - user_session(request.creator) - response = client.get( - "/requests/edit/{}".format(request.id), follow_redirects=False - ) - assert "verify" in response.location - - -def test_creator_pending_finver_changes(client, user_session): - request = RequestFactory.create_with_status( - RequestStatus.CHANGES_REQUESTED_TO_FINVER - ) - user_session(request.creator) - response = client.get( - "/requests/edit/{}".format(request.id), follow_redirects=False - ) - assert "verify" in response.location - - -def test_creator_approved(client, user_session): - request = RequestFactory.create_with_status(RequestStatus.APPROVED) - user_session(request.creator) - response = client.get( - "/requests/edit/{}".format(request.id), follow_redirects=False - ) - assert "details" in response.location - - -def test_creator_approved(client, user_session): - request = RequestFactory.create_with_status(RequestStatus.STARTED) - user_session(request.creator) - response = client.get( - "/requests/edit/{}".format(request.id), follow_redirects=False - ) - assert "new" in response.location - - -def test_ccpo(client, user_session): - ccpo = UserFactory.from_atat_role("ccpo") - request = RequestFactory.create_with_status(RequestStatus.STARTED) - user_session(ccpo) - response = client.get( - "/requests/edit/{}".format(request.id), follow_redirects=False - ) - assert "approval" in response.location diff --git a/tests/routes/requests/requests_form/test_new.py b/tests/routes/requests/requests_form/test_new.py deleted file mode 100644 index bc3c1705..00000000 --- a/tests/routes/requests/requests_form/test_new.py +++ /dev/null @@ -1,249 +0,0 @@ -import datetime -import re -import pytest -from tests.factories import ( - RequestFactory, - UserFactory, - RequestRevisionFactory, - RequestStatusEventFactory, - RequestReviewFactory, -) -from atst.models.request_status_event import RequestStatus -from atst.domain.roles import Roles -from atst.domain.requests import Requests -from urllib.parse import urlencode - -from tests.assert_util import dict_contains - -ERROR_CLASS = "alert--error" - - -def test_submit_invalid_request_form(monkeypatch, client, user_session): - user_session() - response = client.post( - "/requests/new/1", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data="total_ram=5", - ) - assert re.search(ERROR_CLASS, response.data.decode()) - - -def test_submit_valid_request_form(monkeypatch, client, user_session): - user_session() - monkeypatch.setattr( - "atst.forms.new_request.DetailsOfUseForm.validate", lambda s: True - ) - - response = client.post( - "/requests/new/1", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data="meaning=42", - ) - assert "/requests/new/2" in response.headers.get("Location") - - -def test_owner_can_view_request(client, user_session): - user = UserFactory.create() - user_session(user) - request = RequestFactory.create(creator=user) - - response = client.get( - "/requests/new/1/{}".format(request.id), follow_redirects=True - ) - - assert response.status_code == 200 - - -def test_non_owner_cannot_view_request(client, user_session): - user = UserFactory.create() - user_session(user) - request = RequestFactory.create() - - response = client.get( - "/requests/new/1/{}".format(request.id), follow_redirects=True - ) - - assert response.status_code == 404 - - -def test_ccpo_can_view_request(client, user_session): - ccpo = Roles.get("ccpo") - user = UserFactory.create(atat_role=ccpo) - user_session(user) - request = RequestFactory.create() - - response = client.get( - "/requests/new/1/{}".format(request.id), follow_redirects=True - ) - - assert response.status_code == 200 - - -@pytest.mark.skip(reason="create request flow no longer active") -def test_nonexistent_request(client, user_session): - user_session() - response = client.get("/requests/new/1/foo", follow_redirects=True) - - assert response.status_code == 404 - - -def test_creator_info_is_autopopulated_for_existing_request( - monkeypatch, client, user_session -): - user = UserFactory.create() - user_session(user) - request = RequestFactory.create(creator=user, initial_revision={}) - - response = client.get("/requests/new/2/{}".format(request.id)) - body = response.data.decode() - prepopulated_values = [ - "first_name", - "last_name", - "email", - "phone_number", - "date_latest_training", - ] - for attr in prepopulated_values: - value = getattr(user, attr) - if isinstance(value, datetime.date): - value = value.strftime("%m/%d/%Y") - assert "initial-value='{}'".format(value) in body - - -def test_creator_info_is_autopopulated_for_new_request( - monkeypatch, client, user_session -): - user = UserFactory.create() - user_session(user) - - response = client.get("/requests/new/2") - body = response.data.decode() - assert "initial-value='{}'".format(user.first_name) in body - assert "initial-value='{}'".format(user.last_name) in body - assert "initial-value='{}'".format(user.email) in body - - -def test_non_creator_info_is_not_autopopulated(monkeypatch, client, user_session): - user = UserFactory.create() - creator = UserFactory.create() - user_session(user) - request = RequestFactory.create(creator=creator, initial_revision={}) - - response = client.get("/requests/new/2/{}".format(request.id)) - body = response.data.decode() - assert not user.first_name in body - 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, initial_revision={}) - client.post( - "/requests/new/3/{}".format(request.id), - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data="am_poc=yes", - ) - request = Requests.get(creator, 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, initial_revision={}) - 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, initial_revision={}) - 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(creator, request_id) - - assert request.body["primary_poc"]["dodid_poc"] == creator.dod_id - - -def test_poc_autofill_checks_information_about_you_form_first(client, user_session): - creator = UserFactory.create() - user_session(creator) - request = RequestFactory.create( - creator=creator, - initial_revision=dict( - fname_request="Alice", - lname_request="Adams", - email_request="alice.adams@mail.mil", - ), - ) - poc_input = {"am_poc": "yes"} - client.post( - "/requests/new/3/{}".format(request.id), - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data=urlencode(poc_input), - ) - request = Requests.get(creator, request.id) - assert dict_contains( - request.body["primary_poc"], - { - "fname_poc": "Alice", - "lname_poc": "Adams", - "email_poc": "alice.adams@mail.mil", - }, - ) - - -def test_can_review_data(user_session, client): - creator = UserFactory.create() - user_session(creator) - request = RequestFactory.create(creator=creator) - response = client.get("/requests/new/4/{}".format(request.id)) - body = response.data.decode() - # assert a sampling of the request data is on the review page - assert request.body["primary_poc"]["fname_poc"] in body - assert request.body["information_about_you"]["email_request"] in body - - -def test_displays_ccpo_review_comment(user_session, client): - creator = UserFactory.create() - ccpo = UserFactory.from_atat_role("ccpo") - user_session(creator) - request = RequestFactory.create(creator=creator) - request = Requests.set_status(request, RequestStatus.CHANGES_REQUESTED) - review_comment = "add all of the correct info, instead of the incorrect info" - RequestReviewFactory.create( - reviewer=ccpo, comment=review_comment, status=request.status_events[-1] - ) - response = client.get("/requests/new/1/{}".format(request.id)) - body = response.data.decode() - assert review_comment in body diff --git a/tests/routes/requests/requests_form/test_submit.py b/tests/routes/requests/requests_form/test_submit.py deleted file mode 100644 index c824015d..00000000 --- a/tests/routes/requests/requests_form/test_submit.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest -from tests.factories import RequestFactory -from atst.models.request_status_event import RequestStatus - - -def _mock_func(*args, **kwargs): - return RequestFactory.create() - - -def test_submit_reviewed_request(monkeypatch, client, user_session): - user_session() - monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func) - monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func) - monkeypatch.setattr("atst.models.request.Request.status", "pending") - # this just needs to send a known invalid form value - response = client.post( - "/requests/submit/1", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data="", - follow_redirects=False, - ) - assert "/requests" in response.headers["Location"] - assert "modal=pendingCCPOAcceptance" in response.headers["Location"] - - -def test_submit_autoapproved_reviewed_request(monkeypatch, client, user_session): - user_session() - monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func) - monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func) - monkeypatch.setattr( - "atst.models.request.Request.status", - RequestStatus.PENDING_FINANCIAL_VERIFICATION, - ) - response = client.post( - "/requests/submit/1", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data="", - follow_redirects=False, - ) - assert ( - "/requests?modal=pendingFinancialVerification" in response.headers["Location"] - ) diff --git a/tests/routes/requests/test_approval.py b/tests/routes/requests/test_approval.py deleted file mode 100644 index 9d507789..00000000 --- a/tests/routes/requests/test_approval.py +++ /dev/null @@ -1,201 +0,0 @@ -import os -from flask import url_for - -from atst.models.attachment import Attachment -from atst.models.request_status_event import RequestStatus -from atst.domain.roles import Roles - -from tests.factories import ( - RequestFactory, - LegacyTaskOrderFactory, - UserFactory, - RequestReviewFactory, - RequestStatusEventFactory, -) - - -def test_ccpo_can_view_approval(user_session, client): - ccpo = Roles.get("ccpo") - user = UserFactory.create(atat_role=ccpo) - user_session(user) - - request = RequestFactory.create() - response = client.get(url_for("requests.approval", request_id=request.id)) - assert response.status_code == 200 - - -def test_ccpo_prepopulated_as_mission_owner(user_session, client): - user = UserFactory.from_atat_role("ccpo") - user_session(user) - - request = RequestFactory.create_with_status(RequestStatus.PENDING_CCPO_ACCEPTANCE) - response = client.get(url_for("requests.approval", request_id=request.id)) - - body = response.data.decode() - assert user.first_name in body - assert user.last_name in body - - -def test_non_ccpo_cannot_view_approval(user_session, client): - user = UserFactory.create() - user_session(user) - - request = RequestFactory.create(creator=user) - response = client.get(url_for("requests.approval", request_id=request.id)) - assert response.status_code == 404 - - -def prepare_request_pending_approval(creator, pdf_attachment=None): - legacy_task_order = LegacyTaskOrderFactory.create( - number="abc123", pdf=pdf_attachment - ) - return RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_APPROVAL, - legacy_task_order=legacy_task_order, - creator=creator, - ) - - -def test_ccpo_sees_pdf_link(user_session, client, pdf_upload): - ccpo = UserFactory.from_atat_role("ccpo") - user_session(ccpo) - - attachment = Attachment.attach(pdf_upload) - request = prepare_request_pending_approval(ccpo, pdf_attachment=attachment) - - response = client.get(url_for("requests.approval", request_id=request.id)) - download_url = url_for("requests.task_order_pdf_download", request_id=request.id) - - body = response.data.decode() - assert download_url in body - - -def test_ccpo_does_not_see_pdf_link_if_no_pdf(user_session, client, pdf_upload): - ccpo = UserFactory.from_atat_role("ccpo") - user_session(ccpo) - - request = prepare_request_pending_approval(ccpo) - - response = client.get(url_for("requests.approval", request_id=request.id)) - download_url = url_for("requests.task_order_pdf_download", request_id=request.id) - - body = response.data.decode() - assert download_url not in body - - -def test_task_order_download(app, client, user_session, pdf_upload): - user = UserFactory.create() - user_session(user) - - attachment = Attachment.attach(pdf_upload) - legacy_task_order = LegacyTaskOrderFactory.create(number="abc123", pdf=attachment) - request = RequestFactory.create(legacy_task_order=legacy_task_order, creator=user) - - # ensure that real data for pdf upload has been flushed to disk - pdf_upload.seek(0) - pdf_content = pdf_upload.read() - pdf_upload.close() - full_path = os.path.join( - app.config.get("STORAGE_CONTAINER"), attachment.object_name - ) - with open(full_path, "wb") as output_file: - output_file.write(pdf_content) - output_file.flush() - - response = client.get( - url_for("requests.task_order_pdf_download", request_id=request.id) - ) - assert response.data == pdf_content - - -def test_task_order_download_does_not_exist(client, user_session): - user = UserFactory.create() - user_session(user) - request = RequestFactory.create(creator=user) - response = client.get( - url_for("requests.task_order_pdf_download", request_id=request.id) - ) - assert response.status_code == 404 - - -def test_can_submit_request_approval(client, user_session): - user = UserFactory.from_atat_role("ccpo") - user_session(user) - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_ACCEPTANCE - ) - review_data = RequestReviewFactory.dictionary() - review_data["review"] = "approving" - response = client.post( - url_for("requests.submit_approval", request_id=request.id), data=review_data - ) - assert response.status_code == 302 - assert request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION - - -def test_can_submit_request_denial(client, user_session): - user = UserFactory.from_atat_role("ccpo") - user_session(user) - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_ACCEPTANCE - ) - review_data = RequestReviewFactory.dictionary() - review_data["review"] = "denying" - response = client.post( - url_for("requests.submit_approval", request_id=request.id), data=review_data - ) - assert response.status_code == 302 - assert request.status == RequestStatus.CHANGES_REQUESTED - - -def test_ccpo_user_can_comment_on_request(client, user_session): - user = UserFactory.from_atat_role("ccpo") - user_session(user) - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_ACCEPTANCE - ) - assert len(request.internal_comments) == 0 - - comment_text = "This is the greatest request in the history of requests" - comment_form_data = {"text": comment_text} - response = client.post( - url_for("requests.create_internal_comment", request_id=request.id), - data=comment_form_data, - ) - assert response.status_code == 302 - assert len(request.internal_comments) == 1 - assert request.internal_comments[0].text == comment_text - - -def test_comment_text_is_required(client, user_session): - user = UserFactory.from_atat_role("ccpo") - user_session(user) - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_ACCEPTANCE - ) - assert len(request.internal_comments) == 0 - - comment_form_data = {"text": ""} - response = client.post( - url_for("requests.create_internal_comment", request_id=request.id), - data=comment_form_data, - ) - assert response.status_code == 200 - assert len(request.internal_comments) == 0 - - -def test_other_user_cannot_comment_on_request(client, user_session): - user = UserFactory.create() - user_session(user) - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_ACCEPTANCE - ) - - comment_text = "What is this even" - comment_form_data = {"text": comment_text} - response = client.post( - url_for("requests.create_internal_comment", request_id=request.id), - data=comment_form_data, - ) - - assert response.status_code == 404 diff --git a/tests/routes/requests/test_financial_verification.py b/tests/routes/requests/test_financial_verification.py deleted file mode 100644 index 734d0864..00000000 --- a/tests/routes/requests/test_financial_verification.py +++ /dev/null @@ -1,543 +0,0 @@ -import pytest -from unittest.mock import MagicMock -from flask import url_for -import datetime - -from atst.eda_client import MockEDAClient -from atst.routes.requests.financial_verification import ( - GetFinancialVerificationForm, - UpdateFinancialVerification, - SaveFinancialVerificationDraft, -) - -from tests.mocks import MOCK_VALID_PE_ID -from tests.factories import RequestFactory, UserFactory, LegacyTaskOrderFactory -from atst.forms.exceptions import FormValidationError -from atst.domain.requests.financial_verification import ( - PENumberValidator, - TaskOrderNumberValidator, -) -from atst.models.request_status_event import RequestStatus -from atst.models.attachment import Attachment -from atst.domain.requests.query import RequestsQuery - - -@pytest.fixture -def fv_data(): - return { - "request-pe_id": "123", - "legacy_task_order-number": MockEDAClient.MOCK_CONTRACT_NUMBER, - "request-fname_co": "Contracting", - "request-lname_co": "Officer", - "request-email_co": "jane@mail.mil", - "request-office_co": "WHS", - "request-fname_cor": "Officer", - "request-lname_cor": "Representative", - "request-email_cor": "jane@mail.mil", - "request-office_cor": "WHS", - "request-uii_ids": "1234", - "request-treasury_code": "00123456", - "request-ba_code": "02A", - } - - -@pytest.fixture -def e_fv_data(pdf_upload): - return { - "legacy_task_order-funding_type": "RDTE", - "legacy_task_order-funding_type_other": "other", - "legacy_task_order-expiration_date": "1/1/{}".format( - datetime.date.today().year + 1 - ), - "legacy_task_order-clin_0001": "50000", - "legacy_task_order-clin_0003": "13000", - "legacy_task_order-clin_1001": "30000", - "legacy_task_order-clin_1003": "7000", - "legacy_task_order-clin_2001": "30000", - "legacy_task_order-clin_2003": "7000", - "legacy_task_order-pdf": pdf_upload, - } - - -MANUAL_TO_NUMBER = "DCA10096D0051" - - -TrueValidator = MagicMock() -TrueValidator.validate = MagicMock(return_value=True) - -FalseValidator = MagicMock() -FalseValidator.validate = MagicMock(return_value=False) - - -def test_update_fv(fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = {**fv_data, "pe_id": MOCK_VALID_PE_ID} - - updated_request = UpdateFinancialVerification( - TrueValidator, TrueValidator, user, request, data, is_extended=False - ).execute() - - assert updated_request.is_pending_ccpo_approval - - -def test_update_fv_re_enter_pe_number(fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = {**fv_data, "pe_id": "0101228M"} - update_fv = UpdateFinancialVerification( - PENumberValidator(), TrueValidator, user, request, data, is_extended=False - ) - - with pytest.raises(FormValidationError): - update_fv.execute() - updated_request = update_fv.execute() - - assert updated_request.is_pending_ccpo_approval - - -def test_update_fv_invalid_task_order_number(fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = {**fv_data, "legacy_task_order-number": MANUAL_TO_NUMBER} - update_fv = UpdateFinancialVerification( - TrueValidator, - TaskOrderNumberValidator(), - user, - request, - data, - is_extended=False, - ) - - with pytest.raises(FormValidationError): - update_fv.execute() - - -def test_draft_without_pe_id(fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = {"request-uii_ids": "1234"} - assert SaveFinancialVerificationDraft( - PENumberValidator(), - TaskOrderNumberValidator(), - user, - request, - data, - is_extended=False, - ).execute() - - -def test_update_fv_extended(fv_data, e_fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = {**fv_data, **e_fv_data} - update_fv = UpdateFinancialVerification( - TrueValidator, TaskOrderNumberValidator(), user, request, data, is_extended=True - ) - - assert update_fv.execute() - - -def test_update_fv_extended_does_not_validate_task_order(fv_data, e_fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = {**fv_data, **e_fv_data, "legacy_task_order-number": "abc123"} - update_fv = UpdateFinancialVerification( - TrueValidator, TaskOrderNumberValidator(), user, request, data, is_extended=True - ) - - assert update_fv.execute() - - -def test_update_fv_missing_extended_data(fv_data): - request = RequestFactory.create() - user = UserFactory.create() - update_fv = UpdateFinancialVerification( - TrueValidator, - TaskOrderNumberValidator(), - user, - request, - fv_data, - is_extended=True, - ) - - with pytest.raises(FormValidationError): - update_fv.execute() - - -def test_update_fv_submission(fv_data): - request = RequestFactory.create() - user = UserFactory.create() - updated_request = UpdateFinancialVerification( - TrueValidator, TrueValidator, user, request, fv_data - ).execute() - assert updated_request - - -def test_save_empty_draft(): - request = RequestFactory.create() - user = UserFactory.create() - save_draft = SaveFinancialVerificationDraft( - TrueValidator, TrueValidator, user, request, {}, is_extended=False - ) - - assert save_draft.execute() - - -def test_save_draft_with_ba_code(): - request = RequestFactory.create() - user = UserFactory.create() - data = {"ba_code": "02A"} - save_draft = SaveFinancialVerificationDraft( - TrueValidator, TrueValidator, user, request, data, is_extended=False - ) - - assert save_draft.execute() - - -def test_save_draft_allows_invalid_data(): - request = RequestFactory.create() - user = UserFactory.create() - data = { - "legacy_task_order-number": MANUAL_TO_NUMBER, - "request-pe_id": "123", - "request-ba_code": "a", - } - - assert SaveFinancialVerificationDraft( - PENumberValidator(), - TaskOrderNumberValidator(), - user, - request, - data, - is_extended=True, - ).execute() - - -def test_save_draft_and_then_submit(): - request = RequestFactory.create() - user = UserFactory.create() - data = {"ba_code": "02A"} - updated_request = SaveFinancialVerificationDraft( - TrueValidator, TrueValidator, user, request, data, is_extended=False - ).execute() - - with pytest.raises(FormValidationError): - UpdateFinancialVerification( - TrueValidator, TrueValidator, user, updated_request, data - ).execute() - - -def test_updated_request_has_pdf(fv_data, e_fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = {**fv_data, **e_fv_data, "legacy_task_order-number": MANUAL_TO_NUMBER} - updated_request = UpdateFinancialVerification( - TrueValidator, TrueValidator, user, request, data, is_extended=True - ).execute() - assert updated_request.legacy_task_order.pdf - - -def test_can_save_draft_with_just_pdf(e_fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = {"legacy_task_order-pdf": e_fv_data["legacy_task_order-pdf"]} - SaveFinancialVerificationDraft( - TrueValidator, TrueValidator, user, request, data, is_extended=True - ).execute() - - form = GetFinancialVerificationForm(user, request, is_extended=True).execute() - assert form.legacy_task_order.pdf - - -def test_task_order_info_present_in_extended_form(fv_data, e_fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = { - "legacy_task_order-clin_0001": "1", - "legacy_task_order-number": fv_data["legacy_task_order-number"], - } - SaveFinancialVerificationDraft( - TrueValidator, TrueValidator, user, request, data, is_extended=True - ).execute() - - form = GetFinancialVerificationForm(user, request, is_extended=True).execute() - assert form.legacy_task_order.clin_0001.data - - -def test_update_ignores_empty_values(fv_data, e_fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = {**fv_data, **e_fv_data, "legacy_task_order-funding_type": ""} - SaveFinancialVerificationDraft( - TrueValidator, TrueValidator, user, request, data, is_extended=True - ).execute() - - -def test_can_save_draft_with_funding_type(fv_data, e_fv_data): - request = RequestFactory.create() - user = UserFactory.create() - data = { - "legacy_task_order-number": fv_data["legacy_task_order-number"], - "legacy_task_order-funding_type": e_fv_data["legacy_task_order-funding_type"], - } - updated_request = SaveFinancialVerificationDraft( - TrueValidator, TrueValidator, user, request, data, is_extended=False - ).execute() - - assert updated_request.legacy_task_order.funding_type - - -def test_update_fv_route(client, user_session, fv_data): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - user_session(user) - response = client.post( - url_for("requests.financial_verification", request_id=request.id), - data=fv_data, - follow_redirects=False, - ) - - assert response.status_code == 200 - - -def test_save_fv_draft_route(client, user_session, fv_data): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - user_session(user) - response = client.post( - url_for("requests.save_financial_verification_draft", request_id=request.id), - data=fv_data, - follow_redirects=True, - ) - - assert response.status_code == 200 - - -def test_get_fv_form_route(client, user_session, fv_data): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - user_session(user) - response = client.get( - url_for("requests.financial_verification", request_id=request.id), - data=fv_data, - follow_redirects=False, - ) - - assert response.status_code == 200 - - -def test_manual_task_order_triggers_extended_form( - client, user_session, fv_data, e_fv_data -): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - - data = {**fv_data, **e_fv_data, "legacy_task_order-number": MANUAL_TO_NUMBER} - - UpdateFinancialVerification( - TrueValidator, TrueValidator, user, request, data, is_extended=True - ).execute() - - user_session(user) - response = client.get( - url_for("requests.financial_verification", request_id=request.id), - data=fv_data, - follow_redirects=False, - ) - assert "extended" in response.headers["Location"] - - -def test_manual_to_does_not_trigger_approval(client, user_session, fv_data, e_fv_data): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - data = { - **fv_data, - **e_fv_data, - "legacy_task_order-number": MANUAL_TO_NUMBER, - "request-pe_id": "0101228N", - } - user_session(user) - client.post( - url_for( - "requests.financial_verification", request_id=request.id, extended=True - ), - data=data, - follow_redirects=True, - ) - - updated_request = RequestsQuery.get(request.id) - assert updated_request.status != RequestStatus.APPROVED - - -def test_eda_task_order_does_trigger_approval(client, user_session, fv_data, e_fv_data): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - data = { - **fv_data, - **e_fv_data, - "legacy_task_order-number": MockEDAClient.MOCK_CONTRACT_NUMBER, - "request-pe_id": "0101228N", - } - user_session(user) - client.post( - url_for( - "requests.financial_verification", request_id=request.id, extended=True - ), - data=data, - follow_redirects=True, - ) - - updated_request = RequestsQuery.get(request.id) - assert updated_request.status == RequestStatus.APPROVED - - -def test_attachment_on_non_extended_form(client, user_session, fv_data, e_fv_data): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - data = { - **fv_data, - **e_fv_data, - "legacy_task_order-number": MockEDAClient.MOCK_CONTRACT_NUMBER, - "request-pe_id": "0101228N", - } - user_session(user) - client.post( - url_for( - "requests.financial_verification", request_id=request.id, extended=True - ), - data=data, - follow_redirects=True, - ) - - response = client.get( - url_for("requests.financial_verification", request_id=request.id) - ) - - assert response.status_code == 200 - - -def test_task_order_number_persists_in_form(fv_data, e_fv_data): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - data = { - **fv_data, - "legacy_task_order-number": MANUAL_TO_NUMBER, - "request-pe_id": "0101228N", - } - - try: - UpdateFinancialVerification( - TrueValidator, FalseValidator, user, request, data, is_extended=False - ).execute() - except FormValidationError: - pass - - form = GetFinancialVerificationForm(user, request, is_extended=True).execute() - assert form.legacy_task_order.number.data == MANUAL_TO_NUMBER - - -def test_can_submit_once_to_details_are_entered(fv_data, e_fv_data): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - data = { - **fv_data, - "legacy_task_order-number": MANUAL_TO_NUMBER, - "request-pe_id": "0101228N", - } - - try: - UpdateFinancialVerification( - TrueValidator, FalseValidator, user, request, data, is_extended=False - ).execute() - except FormValidationError: - pass - - data = { - **fv_data, - **e_fv_data, - "legacy_task_order-number": MANUAL_TO_NUMBER, - "request-pe_id": "0101228N", - } - assert UpdateFinancialVerification( - TrueValidator, TrueValidator, user, request, data, is_extended=True - ).execute() - - -def test_existing_task_order_with_pdf(fv_data, e_fv_data, client, user_session): - # Use finver route to create initial TO #1, complete with PDF - user = UserFactory.create() - request = RequestFactory.create(creator=user) - data = {**fv_data, **e_fv_data, "legacy_task_order-number": MANUAL_TO_NUMBER} - UpdateFinancialVerification( - TrueValidator, TaskOrderNumberValidator(), user, request, data, is_extended=True - ).execute() - - # Save draft on a new finver form, but with same number as TO #1 - user = UserFactory.create() - request = RequestFactory.create(creator=user) - data = {"legacy_task_order-number": MANUAL_TO_NUMBER} - SaveFinancialVerificationDraft( - TrueValidator, - TaskOrderNumberValidator(), - user, - request, - data, - is_extended=False, - ).execute() - - # Get finver form - user_session(user) - response = client.get( - url_for("requests.financial_verification", request_id=request.id), - follow_redirects=True, - ) - - assert response.status_code == 200 - - -def test_pdf_clearing(fv_data, e_fv_data, pdf_upload, pdf_upload2): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - data = {**fv_data, **e_fv_data, "legacy_task_order-pdf": pdf_upload} - - SaveFinancialVerificationDraft( - TrueValidator, TrueValidator, user, request, data, is_extended=True - ).execute() - - data = {**data, "legacy_task_order-pdf": pdf_upload2} - UpdateFinancialVerification( - TrueValidator, TrueValidator, user, request, data, is_extended=True - ).execute() - - form = GetFinancialVerificationForm(user, request, is_extended=True).execute() - assert form.legacy_task_order.pdf.data == pdf_upload2.filename - - -# TODO: This test manages an edge case for our current non-unique handling of -# task orders. Because two requests can reference the same task order but we -# only record one request ID on the PDF attachment, multiple task -# orders/requests reference the same task order but only one of them is noted -# in the related attachment entity. I have changed the handling in -# FinancialVerificationBase#_get_form to be more generous in how it finds the -# PDF filename and prepopulates the form data with that name. -def test_always_derives_pdf_filename(fv_data, e_fv_data, pdf_upload): - user = UserFactory.create() - request_one = RequestFactory.create(creator=user) - attachment = Attachment.attach( - pdf_upload, resource="legacy_task_order", resource_id=request_one.id - ) - legacy_task_order = LegacyTaskOrderFactory.create(pdf=attachment) - request_two = RequestFactory.create( - creator=user, legacy_task_order=legacy_task_order - ) - - form_one = GetFinancialVerificationForm( - user, request_one, is_extended=True - ).execute() - form_two = GetFinancialVerificationForm( - user, request_two, is_extended=True - ).execute() - - assert form_one.legacy_task_order.pdf.data == attachment.filename - assert form_two.legacy_task_order.pdf.data == attachment.filename diff --git a/tests/routes/requests/test_requests_index.py b/tests/routes/requests/test_requests_index.py deleted file mode 100644 index 6243bd00..00000000 --- a/tests/routes/requests/test_requests_index.py +++ /dev/null @@ -1,28 +0,0 @@ -from flask import url_for - -from atst.routes.requests.index import RequestsIndex -from tests.factories import RequestFactory, UserFactory -from atst.domain.requests import Requests - - -def test_action_required_mission_owner(): - creator = UserFactory.create() - requests = RequestFactory.create_batch(5, creator=creator) - Requests.submit(requests[0]) - Requests.approve_and_create_portfolio(requests[1]) - - context = RequestsIndex(creator).execute() - - assert context["requests"][0]["action_required"] == False - - -def test_action_required_ccpo(): - creator = UserFactory.create() - requests = RequestFactory.create_batch(5, creator=creator) - Requests.submit(requests[0]) - Requests.approve_and_create_portfolio(requests[1]) - - ccpo = UserFactory.from_atat_role("ccpo") - context = RequestsIndex(ccpo).execute() - - assert context["num_action_required"] == 1 diff --git a/tests/test_integration.py b/tests/test_integration.py deleted file mode 100644 index 152e960b..00000000 --- a/tests/test_integration.py +++ /dev/null @@ -1,84 +0,0 @@ -import pytest -from urllib.parse import urlencode -from .factories import UserFactory, RequestFactory - -from atst.routes.requests.jedi_request_flow import JEDIRequestFlow -from atst.models.request_status_event import RequestStatus -from atst.domain.requests import Requests - - -@pytest.fixture -def screens(app): - return JEDIRequestFlow(3).screens - - -def serialize_dates(data): - if not data: - return data - - dates = { - k: v.strftime("%m/%d/%Y") for k, v in data.items() if hasattr(v, "strftime") - } - - new_data = data.copy() - new_data.update(dates) - - return new_data - - -def test_stepthrough_request_form(user_session, screens, client): - user = UserFactory.create() - user_session(user) - mock_request = RequestFactory.create() - mock_body = mock_request.body - - def post_form(url, redirects=False, data=""): - return client.post( - url, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data=data, - follow_redirects=redirects, - ) - - def take_a_step(inc, req=None, data=None): - req_url = "/requests/new/{}".format(inc) - if req: - req_url += "/" + req - # we do it twice, with and without redirect, in order to get the - # destination url - prelim_resp = post_form(req_url, data=data) - response = post_form(req_url, True, data=data) - assert prelim_resp.status_code == 302 - return (prelim_resp.headers.get("Location"), response) - - # GET the initial form - response = client.get("/requests/new/1") - assert screens[0]["title"] in response.data.decode() - - # POST to each of the form pages up until review and submit - req_id = None - for i in range(1, len(screens)): - # get appropriate form data to POST for this section - section = screens[i - 1]["section"] - massaged = serialize_dates(mock_body[section]) - post_data = urlencode(massaged) - - effective_url, resp = take_a_step(i, req=req_id, data=post_data) - req_id = effective_url.split("/")[-1] - screen_title = screens[i]["title"].replace("&", "&") - - assert "/requests/new/{}/{}".format(i + 1, req_id) in effective_url - assert screen_title in resp.data.decode() - - # at this point, the real request we made and the mock_request bodies - # should be equivalent - assert Requests.get(user, req_id).body == mock_body - - # finish the review and submit step - client.post( - "/requests/submit/{}".format(req_id), - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - - finished_request = Requests.get(user, req_id) - assert finished_request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE From c8a139a9418dca667408c910cbf1cc367322ffc5 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 20 Feb 2019 15:32:08 -0500 Subject: [PATCH 02/25] Remove unused request form --- atst/forms/financial.py | 248 -------------------------------- atst/forms/new_request.py | 222 ---------------------------- tests/forms/test_financial.py | 92 ------------ tests/forms/test_new_request.py | 103 ------------- 4 files changed, 665 deletions(-) delete mode 100644 atst/forms/financial.py delete mode 100644 atst/forms/new_request.py delete mode 100644 tests/forms/test_financial.py delete mode 100644 tests/forms/test_new_request.py diff --git a/atst/forms/financial.py b/atst/forms/financial.py deleted file mode 100644 index 0d614997..00000000 --- a/atst/forms/financial.py +++ /dev/null @@ -1,248 +0,0 @@ -import re -import pendulum -from wtforms.fields.html5 import DateField, EmailField -from wtforms.fields import StringField, FileField, FormField -from wtforms.validators import InputRequired, Email, Regexp, Optional -from flask_wtf.file import FileAllowed -from werkzeug.datastructures import FileStorage - -from .fields import NewlineListField, SelectField, NumberStringField -from atst.forms.forms import CacheableForm -from atst.utils.localization import translate -from .data import FUNDING_TYPES -from .validators import DateRange - - -TREASURY_CODE_REGEX = re.compile(r"^0*([1-9]{4}|[1-9]{6})$") - -BA_CODE_REGEX = re.compile(r"[0-9]{2}\w?$") - - -def number_to_int(num): - if num: - return int(num) - - -def coerce_choice(val): - if val is None: - return None - elif isinstance(val, str): - return val - else: - return val.value - - -class TaskOrderForm(CacheableForm): - def do_validate_number(self): - for field in self: - if field.name != "legacy_task_order-number": - field.validators.insert(0, Optional()) - - valid = super().validate() - - for field in self: - if field.name != "legacy_task_order-number": - field.validators.pop(0) - - return valid - - number = StringField( - translate("forms.financial.number_label"), - description=translate("forms.financial.number_description"), - validators=[InputRequired()], - ) - - funding_type = SelectField( - description=translate("forms.financial.funding_type_description"), - choices=FUNDING_TYPES, - validators=[InputRequired()], - coerce=coerce_choice, - render_kw={"required": False}, - ) - - funding_type_other = StringField( - translate("forms.financial.funding_type_other_label") - ) - - expiration_date = DateField( - translate("forms.financial.expiration_date_label"), - description=translate("forms.financial.expiration_date_description"), - validators=[ - InputRequired(), - DateRange( - lower_bound=pendulum.duration(days=0), - upper_bound=pendulum.duration(years=100), - message="Must be a date in the future.", - ), - ], - format="%m/%d/%Y", - ) - - clin_0001 = NumberStringField( - translate("forms.financial.clin_0001_label"), - validators=[InputRequired()], - description=translate("forms.financial.clin_0001_description"), - filters=[number_to_int], - ) - - clin_0003 = NumberStringField( - translate("forms.financial.clin_0003_label"), - validators=[InputRequired()], - description=translate("forms.financial.clin_0003_description"), - filters=[number_to_int], - ) - - clin_1001 = NumberStringField( - translate("forms.financial.clin_1001_label"), - validators=[InputRequired()], - description=translate("forms.financial.clin_1001_description"), - filters=[number_to_int], - ) - - clin_1003 = NumberStringField( - translate("forms.financial.clin_1003_label"), - validators=[InputRequired()], - description=translate("forms.financial.clin_1003_description"), - filters=[number_to_int], - ) - - clin_2001 = NumberStringField( - translate("forms.financial.clin_2001_label"), - validators=[InputRequired()], - description=translate("forms.financial.clin_2001_description"), - filters=[number_to_int], - ) - - clin_2003 = NumberStringField( - translate("forms.financial.clin_2003_label"), - validators=[InputRequired()], - description=translate("forms.financial.clin_2003_description"), - filters=[number_to_int], - ) - - pdf = FileField( - translate("forms.financial.pdf_label"), - validators=[ - FileAllowed(["pdf"], translate("forms.financial.pdf_allowed_description")), - InputRequired(), - ], - render_kw={"required": False}, - ) - - -class RequestFinancialVerificationForm(CacheableForm): - uii_ids = NewlineListField( - translate("forms.financial.uii_ids_label"), - description=translate("forms.financial.uii_ids_description"), - ) - - pe_id = StringField( - translate("forms.financial.pe_id_label"), - description=translate("forms.financial.pe_id_description"), - validators=[InputRequired()], - ) - - treasury_code = StringField( - translate("forms.financial.treasury_code_label"), - description=translate("forms.financial.treasury_code_description"), - validators=[InputRequired(), Regexp(TREASURY_CODE_REGEX)], - ) - - ba_code = StringField( - translate("forms.financial.ba_code_label"), - description=translate("forms.financial.ba_code_description"), - validators=[InputRequired(), Regexp(BA_CODE_REGEX)], - ) - - fname_co = StringField( - translate("forms.financial.fname_co_label"), validators=[InputRequired()] - ) - lname_co = StringField( - translate("forms.financial.lname_co_label"), validators=[InputRequired()] - ) - - email_co = EmailField( - translate("forms.financial.email_co_label"), - validators=[InputRequired(), Email()], - ) - - office_co = StringField( - translate("forms.financial.office_co_label"), validators=[InputRequired()] - ) - - fname_cor = StringField( - translate("forms.financial.fname_cor_label"), validators=[InputRequired()] - ) - - lname_cor = StringField( - translate("forms.financial.lname_cor_label"), validators=[InputRequired()] - ) - - email_cor = EmailField( - translate("forms.financial.email_cor_label"), - validators=[InputRequired(), Email()], - ) - - office_cor = StringField( - translate("forms.financial.office_cor_label"), validators=[InputRequired()] - ) - - def reset(self): - """ - Reset UII info so that it can be de-parsed rendered properly. - This is a stupid workaround, and there's probably a better way. - """ - self.uii_ids.process_data(self.uii_ids.data) - - -class FinancialVerificationForm(CacheableForm): - - legacy_task_order = FormField(TaskOrderForm) - request = FormField(RequestFinancialVerificationForm) - - def validate(self, *args, **kwargs): - if not kwargs.get("is_extended", True): - return self.do_validate_request() - - if self.legacy_task_order.funding_type.data == "OTHER": - self.legacy_task_order.funding_type_other.validators.append(InputRequired()) - - to_pdf_validators = None - if kwargs.get("has_attachment"): - to_pdf_validators = list(self.legacy_task_order.pdf.validators) - self.legacy_task_order.pdf.validators = [] - - valid = super().validate() - - if to_pdf_validators: - self.legacy_task_order.pdf.validators = to_pdf_validators - - return valid - - def do_validate_request(self): - """ - Called do_validate_request to avoid being considered an inline - validator by wtforms. - """ - request_valid = self.request.validate(self) - task_order_valid = self.legacy_task_order.do_validate_number() - return request_valid and task_order_valid - - def reset(self): - self.request.reset() - - @property - def pe_id(self): - return self.request.pe_id - - @property - def has_pdf_upload(self): - return isinstance(self.legacy_task_order.pdf.data, FileStorage) - - @property - def is_missing_task_order_number(self): - return "number" in self.errors.get("legacy_task_order", {}) - - @property - def is_only_missing_task_order_number(self): - return "task_order_number" in self.errors and len(self.errors) == 1 diff --git a/atst/forms/new_request.py b/atst/forms/new_request.py deleted file mode 100644 index e160070c..00000000 --- a/atst/forms/new_request.py +++ /dev/null @@ -1,222 +0,0 @@ -import pendulum -from wtforms.fields.html5 import DateField, EmailField, IntegerField -from wtforms.fields import BooleanField, RadioField, StringField, TextAreaField -from wtforms.validators import Email, Length, Optional, InputRequired, DataRequired - -from .fields import SelectField -from .forms import CacheableForm -from .edit_user import USER_FIELDS, inherit_field -from .data import ( - SERVICE_BRANCHES, - ASSISTANCE_ORG_TYPES, - DATA_TRANSFER_AMOUNTS, - COMPLETION_DATE_RANGES, -) -from .validators import DateRange, IsNumber -from atst.domain.requests import Requests -from atst.utils.localization import translate - - -class DetailsOfUseForm(CacheableForm): - def validate(self, *args, **kwargs): - if self.jedi_migration.data == "no": - self.rationalization_software_systems.validators.append(Optional()) - self.technical_support_team.validators.append(Optional()) - self.organization_providing_assistance.validators.append(Optional()) - self.engineering_assessment.validators.append(Optional()) - self.data_transfers.validators.append(Optional()) - self.expected_completion_date.validators.append(Optional()) - elif self.jedi_migration.data == "yes": - if self.technical_support_team.data == "no": - self.organization_providing_assistance.validators.append(Optional()) - self.cloud_native.validators.append(Optional()) - - try: - annual_spend = int(self.estimated_monthly_spend.data or 0) * 12 - except ValueError: - annual_spend = 0 - - if annual_spend > Requests.ANNUAL_SPEND_THRESHOLD: - self.number_user_sessions.validators.append(InputRequired()) - self.average_daily_traffic.validators.append(InputRequired()) - - return super(DetailsOfUseForm, self).validate(*args, **kwargs) - - # Details of Use: General - dod_component = SelectField( - translate("forms.new_request.dod_component_label"), - description=translate("forms.new_request.dod_component_description"), - choices=SERVICE_BRANCHES, - validators=[InputRequired()], - ) - - jedi_usage = TextAreaField( - translate("forms.new_request.jedi_usage_label"), - description=translate("forms.new_request.jedi_usage_description"), - validators=[InputRequired()], - ) - - # Details of Use: Cloud Readiness - num_software_systems = IntegerField( - translate("forms.new_request.num_software_systems_label"), - description=translate("forms.new_request.num_software_systems_description"), - ) - - jedi_migration = RadioField( - translate("forms.new_request.jedi_migration_label"), - description=translate("forms.new_request.jedi_migration_description"), - choices=[("yes", "Yes"), ("no", "No")], - default="", - ) - - rationalization_software_systems = RadioField( - description=translate( - "forms.new_request.rationalization_software_systems_description" - ), - choices=[("yes", "Yes"), ("no", "No"), ("In Progress", "In Progress")], - default="", - ) - - technical_support_team = RadioField( - description=translate("forms.new_request.technical_support_team_description"), - choices=[("yes", "Yes"), ("no", "No")], - default="", - ) - - organization_providing_assistance = RadioField( # this needs to be updated to use checkboxes instead of radio - description=translate( - "forms.new_request.organization_providing_assistance_description" - ), - choices=ASSISTANCE_ORG_TYPES, - default="", - ) - - engineering_assessment = RadioField( - description=translate("forms.new_request.engineering_assessment_description"), - choices=[("yes", "Yes"), ("no", "No"), ("In Progress", "In Progress")], - default="", - ) - - data_transfers = SelectField( - description=translate("forms.new_request.data_transfers_description"), - choices=DATA_TRANSFER_AMOUNTS, - validators=[DataRequired()], - ) - - expected_completion_date = SelectField( - description=translate("forms.new_request.expected_completion_date_description"), - choices=COMPLETION_DATE_RANGES, - validators=[DataRequired()], - ) - - cloud_native = RadioField( - description=translate("forms.new_request.cloud_native_description"), - choices=[("yes", "Yes"), ("no", "No")], - default="", - ) - - # Details of Use: Financial Usage - estimated_monthly_spend = IntegerField( - translate("forms.new_request.estimated_monthly_spend_label"), - description=translate("forms.new_request.estimated_monthly_spend_description"), - ) - - dollar_value = IntegerField( - translate("forms.new_request.dollar_value_label"), - description=translate("forms.new_request.dollar_value_description"), - ) - - number_user_sessions = IntegerField( - description=translate("forms.new_request.number_user_sessions_description") - ) - - average_daily_traffic = IntegerField( - translate("forms.new_request.average_daily_traffic_label"), - description=translate("forms.new_request.average_daily_traffic_description"), - ) - - average_daily_traffic_gb = IntegerField( - translate("forms.new_request.average_daily_traffic_gb_label"), - description=translate("forms.new_request.average_daily_traffic_gb_description"), - ) - - start_date = DateField( - description=translate("forms.new_request.start_date_label"), - validators=[ - InputRequired(), - DateRange( - lower_bound=pendulum.duration(days=1), - upper_bound=None, - message=translate( - "forms.new_request.start_date_date_range_validation_message" - ), - ), - ], - format="%m/%d/%Y", - ) - - name = StringField( - translate("forms.new_request.name_label"), - description=translate("forms.new_request.name_description"), - validators=[ - InputRequired(), - Length( - min=4, - max=100, - message=translate("forms.new_request.name_length_validation_message"), - ), - ], - ) - - -class InformationAboutYouForm(CacheableForm): - fname_request = inherit_field(USER_FIELDS["first_name"]) - lname_request = inherit_field(USER_FIELDS["last_name"]) - email_request = inherit_field(USER_FIELDS["email"]) - phone_number = inherit_field(USER_FIELDS["phone_number"]) - phone_ext = inherit_field(USER_FIELDS["phone_ext"], required=False) - service_branch = inherit_field(USER_FIELDS["service_branch"]) - citizenship = inherit_field(USER_FIELDS["citizenship"]) - designation = inherit_field(USER_FIELDS["designation"]) - date_latest_training = inherit_field(USER_FIELDS["date_latest_training"]) - - -class PortfolioOwnerForm(CacheableForm): - 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( - translate("forms.new_request.am_poc_label"), - default=False, - false_values=(False, "false", "False", "no", ""), - ) - - fname_poc = StringField( - translate("forms.new_request.fname_poc_label"), validators=[InputRequired()] - ) - - lname_poc = StringField( - translate("forms.new_request.lname_poc_label"), validators=[InputRequired()] - ) - - email_poc = EmailField( - translate("forms.new_request.email_poc_label"), - validators=[InputRequired(), Email()], - ) - - dodid_poc = StringField( - translate("forms.new_request.dodid_poc_label"), - validators=[InputRequired(), Length(min=10), IsNumber()], - ) - - -class ReviewAndSubmitForm(CacheableForm): - reviewed = BooleanField(translate("forms.new_request.reviewed_label")) diff --git a/tests/forms/test_financial.py b/tests/forms/test_financial.py deleted file mode 100644 index 39a02a87..00000000 --- a/tests/forms/test_financial.py +++ /dev/null @@ -1,92 +0,0 @@ -import pytest -from werkzeug.datastructures import ImmutableMultiDict - -from atst.forms.financial import FinancialVerificationForm -from atst.domain.requests.financial_verification import PENumberValidator - - -@pytest.mark.parametrize( - "input_,expected", - [ - ("0603502N", None), - ("0603502NZ", None), - ("603502N", "0603502N"), - ("063502N", "0603502N"), - ("63502N", "0603502N"), - ], -) -def test_suggest_pe_id(input_, expected): - assert PENumberValidator().suggest_pe_id(input_) == expected - - -def test_funding_type_other_not_required_if_funding_type_is_not_other(): - form_data = ImmutableMultiDict({"legacy_task_order-funding_type": "PROC"}) - form = FinancialVerificationForm(form_data) - form.validate() - assert "funding_type_other" not in form.errors - - -def test_funding_type_other_required_if_funding_type_is_other(): - form_data = ImmutableMultiDict({"legacy_task_order-funding_type": "OTHER"}) - form = FinancialVerificationForm(form_data) - form.validate() - assert "funding_type_other" in form.errors["legacy_task_order"] - - -@pytest.mark.parametrize( - "input_,expected", - [ - ("1234", True), - ("123456", True), - ("0001234", True), - ("000123456", True), - ("12345", False), - ("00012345", False), - ("0001234567", False), - ("000000", False), - ], -) -def test_treasury_code_validation(input_, expected): - form_data = ImmutableMultiDict([("request-treasury_code", input_)]) - form = FinancialVerificationForm(form_data) - form.validate() - is_valid = "treasury_code" not in form.errors["request"] - - assert is_valid == expected - - -@pytest.mark.parametrize( - "input_,expected", - [ - ("1", False), - ("12", True), - ("01", True), - ("0A", False), - ("A", False), - ("AB", False), - ("123", True), - ("012", True), - ("12A", True), - ("02A", True), - ("0012", False), - ("012A", False), - ("2AB", False), - ], -) -def test_ba_code_validation(input_, expected): - form_data = ImmutableMultiDict([("request-ba_code", input_)]) - form = FinancialVerificationForm(form_data) - form.validate() - is_valid = "ba_code" not in form.errors["request"] - - assert is_valid == expected - - -def test_can_submit_zero_for_clin(): - form_first = FinancialVerificationForm() - form_first.validate() - assert "clin_0001" in form_first.errors["legacy_task_order"] - form_data = ImmutableMultiDict([("legacy_task_order-clin_0001", "0")]) - form_second = FinancialVerificationForm(form_data) - form_second.validate() - assert "clin_0001" not in form_second.errors["legacy_task_order"] diff --git a/tests/forms/test_new_request.py b/tests/forms/test_new_request.py deleted file mode 100644 index 91cbf0dd..00000000 --- a/tests/forms/test_new_request.py +++ /dev/null @@ -1,103 +0,0 @@ -import pytest -from werkzeug.datastructures import ImmutableMultiDict - -from atst.forms.new_request import DetailsOfUseForm - - -class TestDetailsOfUseForm: - - form_data = { - "dod_component": "Army and Air Force Exchange Service", - "jedi_usage": "cloud-ify all the things", - "num_software_systems": "12", - "estimated_monthly_spend": "1000000", - "dollar_value": "42", - "number_user_sessions": "6", - "average_daily_traffic": "0", - "start_date": "12/12/2050", - "name": "blue-beluga", - } - migration_data = { - "jedi_migration": "yes", - "rationalization_software_systems": "yes", - "technical_support_team": "yes", - "organization_providing_assistance": "In-house staff", - "engineering_assessment": "yes", - "data_transfers": "Less than 100GB", - "expected_completion_date": "Less than 1 month", - } - - def _make_form(self, data): - form_data = ImmutableMultiDict(data.items()) - return DetailsOfUseForm(form_data) - - def test_require_cloud_native_when_not_migrating(self): - extra_data = {"jedi_migration": "no"} - request_form = self._make_form({**self.form_data, **extra_data}) - assert not request_form.validate() - assert request_form.errors == {"cloud_native": ["Not a valid choice"]} - - def test_require_migration_questions_when_migrating(self): - extra_data = { - "jedi_migration": "yes", - "data_transfers": "", - "expected_completion_date": "", - } - request_form = self._make_form({**self.form_data, **extra_data}) - assert not request_form.validate() - assert request_form.errors == { - "rationalization_software_systems": ["Not a valid choice"], - "technical_support_team": ["Not a valid choice"], - "organization_providing_assistance": ["Not a valid choice"], - "engineering_assessment": ["Not a valid choice"], - "data_transfers": ["This field is required."], - "expected_completion_date": ["This field is required."], - } - - def test_require_organization_when_technical_support_team(self): - data = {**self.form_data, **self.migration_data} - del data["organization_providing_assistance"] - - request_form = self._make_form(data) - assert not request_form.validate() - assert request_form.errors == { - "organization_providing_assistance": ["Not a valid choice"] - } - - def test_valid_form_data(self): - data = {**self.form_data, **self.migration_data} - data["technical_support_team"] = "no" - del data["organization_providing_assistance"] - - request_form = self._make_form(data) - assert request_form.validate() - - def test_sessions_required_for_large_applications(self): - data = {**self.form_data, **self.migration_data} - data["estimated_monthly_spend"] = "9999999" - del data["number_user_sessions"] - del data["average_daily_traffic"] - - request_form = self._make_form(data) - assert not request_form.validate() - assert request_form.errors == { - "number_user_sessions": ["This field is required."], - "average_daily_traffic": ["This field is required."], - } - - def test_sessions_not_required_low_monthly_spend(self): - data = {**self.form_data, **self.migration_data} - data["estimated_monthly_spend"] = "10" - del data["number_user_sessions"] - del data["average_daily_traffic"] - - request_form = self._make_form(data) - assert request_form.validate() - - def test_start_date_must_be_in_the_future(self): - data = {**self.form_data, **self.migration_data} - data["start_date"] = "01/01/2018" - - request_form = self._make_form(data) - assert not request_form.validate() - assert "Must be a date in the future." in request_form.errors["start_date"] From 6fb333acb9b4fae2e154bfdc8b339a79947df36a Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 20 Feb 2019 15:37:04 -0500 Subject: [PATCH 03/25] Remove Requests domain classes --- atst/domain/requests/__init__.py | 1 - atst/domain/requests/authorization.py | 29 -- .../domain/requests/financial_verification.py | 74 ----- atst/domain/requests/query.py | 73 ----- atst/domain/requests/requests.py | 239 --------------- atst/domain/requests/status_event_handler.py | 35 --- atst/routes/__init__.py | 1 - tests/domain/test_requests.py | 273 ------------------ tests/models/test_requests.py | 122 -------- 9 files changed, 847 deletions(-) delete mode 100644 atst/domain/requests/__init__.py delete mode 100644 atst/domain/requests/authorization.py delete mode 100644 atst/domain/requests/financial_verification.py delete mode 100644 atst/domain/requests/query.py delete mode 100644 atst/domain/requests/requests.py delete mode 100644 atst/domain/requests/status_event_handler.py delete mode 100644 tests/domain/test_requests.py delete mode 100644 tests/models/test_requests.py diff --git a/atst/domain/requests/__init__.py b/atst/domain/requests/__init__.py deleted file mode 100644 index 88072d60..00000000 --- a/atst/domain/requests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .requests import Requests, create_revision_from_request_body diff --git a/atst/domain/requests/authorization.py b/atst/domain/requests/authorization.py deleted file mode 100644 index 173c4bfc..00000000 --- a/atst/domain/requests/authorization.py +++ /dev/null @@ -1,29 +0,0 @@ -from atst.models.permissions import Permissions -from atst.domain.authz import Authorization -from atst.domain.exceptions import UnauthorizedError - - -class RequestsAuthorization(object): - def __init__(self, user, request): - self.user = user - self.request = request - - @property - def can_view(self): - return ( - Authorization.has_atat_permission( - self.user, Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST - ) - or self.request.creator == self.user - ) - - def check_can_view(self, message): - if not self.can_view: - raise UnauthorizedError(self.user, message) - - def check_can_approve(self): - return Authorization.check_atat_permission( - self.user, - Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST, - "cannot review and approve requests", - ) diff --git a/atst/domain/requests/financial_verification.py b/atst/domain/requests/financial_verification.py deleted file mode 100644 index 042080b4..00000000 --- a/atst/domain/requests/financial_verification.py +++ /dev/null @@ -1,74 +0,0 @@ -import re - -from atst.domain.legacy_task_orders import LegacyTaskOrders -from atst.domain.pe_numbers import PENumbers -from atst.domain.exceptions import NotFoundError - - -class PENumberValidator(object): - PE_REGEX = re.compile( - r""" - (0?\d) # program identifier - (0?\d) # category - (\d) # activity - (\d+) # sponsor element - (.+) # service - """, - re.X, - ) - - def validate(self, request, field): - if field.errors: - return False - - if self._same_as_previous(request, field.data): - return True - - try: - PENumbers.get(field.data) - except NotFoundError: - self._apply_error(field) - return False - - return True - - def suggest_pe_id(self, pe_id): - suggestion = pe_id - match = self.PE_REGEX.match(pe_id) - if match: - (program, category, activity, sponsor, service) = match.groups() - if len(program) < 2: - program = "0" + program - if len(category) < 2: - category = "0" + category - suggestion = "".join((program, category, activity, sponsor, service)) - - if suggestion != pe_id: - return suggestion - return None - - def _same_as_previous(self, request, pe_id): - return request.pe_number == pe_id - - def _apply_error(self, field): - suggestion = self.suggest_pe_id(field.data) - error_str = ( - "We couldn't find that PE number. {}" - "If you have double checked it you can submit anyway. " - "Your request will need to go through a manual review." - ).format('Did you mean "{}"? '.format(suggestion) if suggestion else "") - field.errors += (error_str,) - - -class TaskOrderNumberValidator(object): - def validate(self, field): - try: - LegacyTaskOrders.get(field.data) - except NotFoundError: - self._apply_error(field) - return False - - return True - - def _apply_error(self, field): - field.errors += ("Task Order number not found",) diff --git a/atst/domain/requests/query.py b/atst/domain/requests/query.py deleted file mode 100644 index d080f941..00000000 --- a/atst/domain/requests/query.py +++ /dev/null @@ -1,73 +0,0 @@ -from sqlalchemy import exists, and_, exc, text -from sqlalchemy.orm.exc import NoResultFound - -from atst.database import db -from atst.domain.common import Query -from atst.models.request import Request -from atst.domain.exceptions import NotFoundError - - -class RequestsQuery(Query): - model = Request - - @classmethod - def exists(cls, request_id, creator): - try: - return db.session.query( - exists().where( - and_(Request.id == request_id, Request.creator == creator) - ) - ).scalar() - - except exc.DataError: - return False - - @classmethod - def get_many(cls, creator=None): - filters = [] - if creator: - filters.append(Request.creator == creator) - - requests = ( - db.session.query(Request) - .filter(*filters) - .order_by(Request.time_created.desc()) - .all() - ) - return requests - - @classmethod - def get_with_lock(cls, request_id): - 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 - return ( - db.session.query(Request) - .filter_by(id=request_id) - .with_for_update(of=Request) - .one() - ) - - except NoResultFound: - raise NotFoundError("requests") - - @classmethod - def status_count(cls, status, creator=None): - bindings = {"status": status.name} - raw = """ -SELECT count(requests_with_status.id) -FROM ( - SELECT DISTINCT ON (rse.request_id) r.*, rse.new_status as status - FROM request_status_events rse JOIN requests r ON r.id = rse.request_id - ORDER BY rse.request_id, rse.sequence DESC -) as requests_with_status -WHERE requests_with_status.status = :status - """ - - if creator: - raw += " AND requests_with_status.user_id = :user_id" - bindings["user_id"] = creator.id - - results = db.session.execute(text(raw), bindings).fetchone() - (count,) = results - return count diff --git a/atst/domain/requests/requests.py b/atst/domain/requests/requests.py deleted file mode 100644 index 6b30c366..00000000 --- a/atst/domain/requests/requests.py +++ /dev/null @@ -1,239 +0,0 @@ -import dateutil - -from atst.domain.portfolios import Portfolios -from atst.models.request_revision import RequestRevision -from atst.models.request_status_event import RequestStatusEvent, RequestStatus -from atst.models.request_review import RequestReview -from atst.models.request_internal_comment import RequestInternalComment -from atst.utils import deep_merge -from atst.queue import queue -from atst.filters import dollars - -from .query import RequestsQuery -from .authorization import RequestsAuthorization -from .status_event_handler import RequestStatusEventHandler - - -def create_revision_from_request_body(body): - body = {k: v for p in body.values() for k, v in p.items()} - DATES = ["start_date", "date_latest_training"] - coerced_timestamps = { - k: dateutil.parser.parse(v) - for k, v in body.items() - if k in DATES and isinstance(v, str) - } - body = {**body, **coerced_timestamps} - return RequestRevision(**body) - - -class Requests(object): - AUTO_ACCEPT_THRESHOLD = 1_000_000 - ANNUAL_SPEND_THRESHOLD = 1_000_000 - - @classmethod - def create(cls, creator, body): - revision = create_revision_from_request_body(body) - request = RequestsQuery.create(creator=creator, revisions=[revision]) - request = Requests.set_status(request, RequestStatus.STARTED) - request = RequestsQuery.add_and_commit(request) - - return request - - @classmethod - def exists(cls, request_id, creator): - return RequestsQuery.exists(request_id, creator) - - @classmethod - def get(cls, user, request_id): - request = RequestsQuery.get(request_id) - RequestsAuthorization(user, request).check_can_view("get request") - return request - - @classmethod - def get_for_approval(cls, user, request_id): - request = RequestsQuery.get(request_id) - RequestsAuthorization(user, request).check_can_approve() - return request - - @classmethod - def get_many(cls, creator=None): - return RequestsQuery.get_many(creator) - - @classmethod - def submit(cls, request): - request = Requests.set_status(request, RequestStatus.SUBMITTED) - - if Requests.should_auto_accept(request): - request = Requests.set_status( - request, RequestStatus.PENDING_FINANCIAL_VERIFICATION - ) - Requests._add_review( - user=None, - request=request, - review_data={ - "comment": "Auto-acceptance for dollar value below {}".format( - dollars(Requests.AUTO_ACCEPT_THRESHOLD) - ) - }, - ) - else: - request = Requests.set_status( - request, RequestStatus.PENDING_CCPO_ACCEPTANCE - ) - - request = RequestsQuery.add_and_commit(request) - - return request - - @classmethod - def update(cls, request_id, request_delta): - request = RequestsQuery.get_with_lock(request_id) - return Requests._update(request, request_delta) - - @classmethod - def _update(cls, request, request_delta): - new_body = deep_merge(request_delta, request.body) - revision = create_revision_from_request_body(new_body) - request.revisions.append(revision) - - return RequestsQuery.add_and_commit(request) - - @classmethod - def approve_and_create_portfolio(cls, request): - approved_request = Requests.set_status(request, RequestStatus.APPROVED) - portfolio = Portfolios.create_from_request(approved_request) - - RequestsQuery.add_and_commit(approved_request) - - return portfolio - - @classmethod - def auto_approve_and_create_portfolio( - cls, - request, - reason="Financial verification information found in Electronic Document Access API", - ): - portfolio = Requests.approve_and_create_portfolio(request) - Requests._add_review( - user=None, request=request, review_data={"comment": reason} - ) - return portfolio - - @classmethod - def set_status(cls, request, status: RequestStatus): - old_status = request.status - status_event = RequestStatusEvent( - new_status=status, revision=request.latest_revision - ) - request.status_events.append(status_event) - updated_request = RequestsQuery.add_and_commit(request) - RequestStatusEventHandler(queue).handle_status_change( - updated_request, old_status, status - ) - - return updated_request - - @classmethod - def should_auto_accept(cls, request): - try: - dollar_value = request.body["details_of_use"]["dollar_value"] - except KeyError: - return False - - return dollar_value < cls.AUTO_ACCEPT_THRESHOLD - - _VALID_SUBMISSION_STATUSES = [ - RequestStatus.STARTED, - RequestStatus.CHANGES_REQUESTED, - ] - - @classmethod - def should_allow_submission(cls, request): - all_request_sections = [ - "details_of_use", - "information_about_you", - "primary_poc", - ] - existing_request_sections = request.body.keys() - return request.status in Requests._VALID_SUBMISSION_STATUSES and all( - section in existing_request_sections for section in all_request_sections - ) - - @classmethod - def status_count(cls, status, creator=None): - return RequestsQuery.status_count(status, creator) - - @classmethod - def in_progress_count(cls): - return sum( - [ - Requests.status_count(RequestStatus.STARTED), - Requests.status_count(RequestStatus.PENDING_FINANCIAL_VERIFICATION), - Requests.status_count(RequestStatus.CHANGES_REQUESTED), - ] - ) - - @classmethod - def pending_ccpo_count(cls): - return sum( - [ - Requests.status_count(RequestStatus.PENDING_CCPO_ACCEPTANCE), - Requests.status_count(RequestStatus.PENDING_CCPO_APPROVAL), - ] - ) - - @classmethod - def completed_count(cls): - return Requests.status_count(RequestStatus.APPROVED) - - @classmethod - def update_financial_verification( - cls, request_id, financial_data, legacy_task_order=None - ): - request = RequestsQuery.get_with_lock(request_id) - if legacy_task_order: - request.legacy_task_order = legacy_task_order - - request = Requests._update(request, {"financial_verification": financial_data}) - return request - - @classmethod - def submit_financial_verification(cls, request): - request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL) - request = RequestsQuery.add_and_commit(request) - return request - - @classmethod - def _add_review(cls, user=None, request=None, review_data=None): - request.latest_status.review = RequestReview(reviewer=user, **review_data) - request = RequestsQuery.add_and_commit(request) - return request - - @classmethod - def advance(cls, user, request, review_data): - if request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE: - Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION) - elif request.status == RequestStatus.PENDING_CCPO_APPROVAL: - Requests.approve_and_create_portfolio(request) - - return Requests._add_review(user=user, request=request, review_data=review_data) - - @classmethod - def request_changes(cls, user, request, review_data): - if request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE: - Requests.set_status(request, RequestStatus.CHANGES_REQUESTED) - elif request.status == RequestStatus.PENDING_CCPO_APPROVAL: - Requests.set_status(request, RequestStatus.CHANGES_REQUESTED_TO_FINVER) - - return Requests._add_review(user=user, request=request, review_data=review_data) - - @classmethod - def add_internal_comment(cls, user, request, comment_text): - RequestsAuthorization(user, request).check_can_approve() - comment = RequestInternalComment(request=request, text=comment_text, user=user) - RequestsQuery.add_and_commit(comment) - return request - - @classmethod - def possible_statuses(cls): - return [s[1].value for s in RequestStatus.__members__.items()] diff --git a/atst/domain/requests/status_event_handler.py b/atst/domain/requests/status_event_handler.py deleted file mode 100644 index 3c4c9f87..00000000 --- a/atst/domain/requests/status_event_handler.py +++ /dev/null @@ -1,35 +0,0 @@ -from flask import render_template - -from atst.models.request_status_event import RequestStatus - - -class RequestStatusEventHandler(object): - STATUS_TRANSITIONS = set( - [ - ( - RequestStatus.PENDING_CCPO_ACCEPTANCE, - RequestStatus.PENDING_FINANCIAL_VERIFICATION, - ), - (RequestStatus.PENDING_CCPO_ACCEPTANCE, RequestStatus.CHANGES_REQUESTED), - ( - RequestStatus.PENDING_CCPO_APPROVAL, - RequestStatus.CHANGES_REQUESTED_TO_FINVER, - ), - (RequestStatus.PENDING_CCPO_APPROVAL, RequestStatus.APPROVED), - ] - ) - - def __init__(self, queue): - self.queue = queue - - def handle_status_change(self, request, old_status, new_status): - if (old_status, new_status) in self.STATUS_TRANSITIONS: - self._send_email(request) - - def _send_email(self, request): - email_body = render_template( - "emails/request_status_change.txt", request=request - ) - self.queue.send_mail( - [request.creator.email], "Your JEDI request status has changed", email_body - ) diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 4f0e91cb..e24ed7be 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -16,7 +16,6 @@ import pendulum import os from werkzeug.exceptions import NotFound -from atst.domain.requests import Requests from atst.domain.users import Users from atst.domain.authnid import AuthenticationContext from atst.domain.audit_log import AuditLog diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py deleted file mode 100644 index a229ea50..00000000 --- a/tests/domain/test_requests.py +++ /dev/null @@ -1,273 +0,0 @@ -import pytest -from uuid import uuid4 - -from atst.domain.exceptions import NotFoundError -from atst.domain.requests import Requests -from atst.domain.requests.authorization import RequestsAuthorization -from atst.models.request import Request -from atst.models.request_status_event import RequestStatus - -from tests.factories import ( - RequestFactory, - UserFactory, - RequestStatusEventFactory, - RequestRevisionFactory, - RequestReviewFactory, -) - - -@pytest.fixture(scope="function") -def new_request(session): - return RequestFactory.create() - - -def test_can_get_request(): - factory_req = RequestFactory.create() - request = Requests.get(factory_req.creator, factory_req.id) - - assert request.id == factory_req.id - - -def test_nonexistent_request_raises(): - a_user = UserFactory.build() - with pytest.raises(NotFoundError): - Requests.get(a_user, uuid4()) - - -def test_new_request_has_started_status(): - request = Requests.create(UserFactory.build(), {}) - assert request.status == RequestStatus.STARTED - - -def test_auto_approve_less_than_1m(): - new_request = RequestFactory.create(initial_revision={"dollar_value": 999_999}) - request = Requests.submit(new_request) - - assert request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION - assert request.reviews - assert request.reviews[0].full_name_reviewer == "System" - - -def test_dont_auto_approve_if_dollar_value_is_1m_or_above(): - new_request = RequestFactory.create(initial_revision={"dollar_value": 1_000_000}) - request = Requests.submit(new_request) - - assert request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE - - -def test_dont_auto_approve_if_no_dollar_value_specified(): - new_request = RequestFactory.create(initial_revision={}) - request = Requests.submit(new_request) - - assert request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE - - -def test_should_allow_submission(): - new_request = RequestFactory.create() - - assert Requests.should_allow_submission(new_request) - - RequestStatusEventFactory.create( - request=new_request, - new_status=RequestStatus.CHANGES_REQUESTED, - revision=new_request.latest_revision, - ) - assert Requests.should_allow_submission(new_request) - - # new, blank revision - RequestRevisionFactory.create(request=new_request) - assert not Requests.should_allow_submission(new_request) - - -def test_request_knows_its_last_submission_timestamp(new_request): - submitted_request = Requests.submit(new_request) - assert submitted_request.last_submission_timestamp - - -def test_request_knows_if_it_has_no_last_submission_timestamp(new_request): - assert new_request.last_submission_timestamp is None - - -def test_exists(session): - user_allowed = UserFactory.create() - user_denied = UserFactory.create() - request = RequestFactory.create(creator=user_allowed) - assert Requests.exists(request.id, user_allowed) - assert not Requests.exists(request.id, user_denied) - - -def test_status_count(session): - # make sure table is empty - session.query(Request).delete() - - request1 = RequestFactory.create() - request2 = RequestFactory.create() - RequestStatusEventFactory.create( - sequence=2, - request_id=request2.id, - revision=request2.latest_revision, - new_status=RequestStatus.PENDING_FINANCIAL_VERIFICATION, - ) - - assert Requests.status_count(RequestStatus.PENDING_FINANCIAL_VERIFICATION) == 1 - assert Requests.status_count(RequestStatus.STARTED) == 1 - assert Requests.in_progress_count() == 2 - - -def test_status_count_scoped_to_creator(session): - # make sure table is empty - session.query(Request).delete() - - user = UserFactory.create() - request1 = RequestFactory.create() - request2 = RequestFactory.create(creator=user) - - assert Requests.status_count(RequestStatus.STARTED) == 2 - assert Requests.status_count(RequestStatus.STARTED, creator=user) == 1 - - -request_financial_data = { - "pe_id": "123", - "task_order_number": "021345", - "fname_co": "Contracting", - "lname_co": "Officer", - "email_co": "jane@mail.mil", - "office_co": "WHS", - "fname_cor": "Officer", - "lname_cor": "Representative", - "email_cor": "jane@mail.mil", - "office_cor": "WHS", - "uii_ids": "1234", - "treasury_code": "00123456", - "ba_code": "024A", -} - - -def test_set_status_sets_revision(): - request = RequestFactory.create() - Requests.set_status(request, RequestStatus.APPROVED) - assert request.latest_revision == request.status_events[-1].revision - - -def test_advance_to_financial_verification(): - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_ACCEPTANCE - ) - review_data = RequestReviewFactory.dictionary() - Requests.advance(UserFactory.create(), request, review_data) - assert request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION - current_review = request.latest_status.review - assert current_review.fname_mao == review_data["fname_mao"] - - -def test_advance_to_approval(): - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_APPROVAL - ) - review_data = RequestReviewFactory.dictionary() - Requests.advance(UserFactory.create(), request, review_data) - assert request.status == RequestStatus.APPROVED - - -def test_request_changes_to_request_application(): - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_ACCEPTANCE - ) - review_data = RequestReviewFactory.dictionary() - Requests.request_changes(UserFactory.create(), request, review_data) - assert request.status == RequestStatus.CHANGES_REQUESTED - current_review = request.latest_status.review - assert current_review.fname_mao == review_data["fname_mao"] - - -def test_request_changes_to_financial_verification_info(): - request = RequestFactory.create_with_status( - status=RequestStatus.PENDING_CCPO_APPROVAL - ) - review_data = RequestReviewFactory.dictionary() - Requests.request_changes(UserFactory.create(), request, review_data) - assert request.status == RequestStatus.CHANGES_REQUESTED_TO_FINVER - current_review = request.latest_status.review - assert current_review.fname_mao == review_data["fname_mao"] - - -def test_add_internal_comment(): - request = RequestFactory.create() - ccpo = UserFactory.from_atat_role("ccpo") - - assert len(request.internal_comments) == 0 - - request = Requests.add_internal_comment(ccpo, request, "this is my comment") - - assert len(request.internal_comments) == 1 - assert request.internal_comments[0].text == "this is my comment" - - -def test_creator_can_view_own_request(): - creator = UserFactory.create() - request = RequestFactory.create(creator=creator) - - assert RequestsAuthorization(creator, request).can_view - - -def test_ccpo_can_view_request(): - ccpo = UserFactory.from_atat_role("ccpo") - request = RequestFactory.create() - - assert RequestsAuthorization(ccpo, request).can_view - - -def test_random_user_cannot_view_request(): - user = UserFactory.create() - request = RequestFactory.create() - - assert not RequestsAuthorization(user, request).can_view - - -def test_auto_approve_and_create_portfolio(): - request = RequestFactory.create() - portfolio = Requests.auto_approve_and_create_portfolio(request) - assert portfolio - assert request.reviews[0] - assert request.reviews[0].full_name_reviewer == "System" - - -class TestStatusNotifications(object): - def _assert_job(self, queue, request): - assert len(queue.get_queue()) == 1 - job = queue.get_queue().jobs[0] - assert job.func == queue._send_mail - assert job.args[0] == [request.creator.email] - - def test_pending_finver_triggers_notification(self, queue): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.PENDING_CCPO_ACCEPTANCE) - request = Requests.set_status( - request, RequestStatus.PENDING_FINANCIAL_VERIFICATION - ) - self._assert_job(queue, request) - - def test_changes_requested_triggers_notification(self, queue): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.PENDING_CCPO_ACCEPTANCE) - request = Requests.set_status(request, RequestStatus.CHANGES_REQUESTED) - self._assert_job(queue, request) - - def test_changes_requested_to_finver_triggers_notification(self, queue): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL) - request = Requests.set_status( - request, RequestStatus.CHANGES_REQUESTED_TO_FINVER - ) - self._assert_job(queue, request) - - def test_approval_triggers_notification(self, queue): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL) - request = Requests.set_status(request, RequestStatus.APPROVED) - self._assert_job(queue, request) - - def test_submitted_does_not_trigger_notification(self, queue): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.SUBMITTED) - assert len(queue.get_queue()) == 0 diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py deleted file mode 100644 index f2a82528..00000000 --- a/tests/models/test_requests.py +++ /dev/null @@ -1,122 +0,0 @@ -from tests.factories import ( - RequestFactory, - UserFactory, - RequestStatusEventFactory, - RequestReviewFactory, - RequestRevisionFactory, -) -from atst.domain.requests import Requests -from atst.models.request_status_event import RequestStatus - - -def test_pending_financial_requires_mo_action(): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION) - - assert request.action_required_by == "mission_owner" - - -def test_pending_ccpo_approval_requires_ccpo(): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL) - - assert request.action_required_by == "ccpo" - - -def test_request_has_creator(): - user = UserFactory.create() - request = RequestFactory.create(creator=user) - - assert request.creator == user - - -def test_request_status_started_displayname(): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.STARTED) - - assert request.status_displayname == "Started" - - -def test_request_status_pending_financial_displayname(): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION) - - assert request.status_displayname == "Pending Financial Verification" - - -def test_request_status_pending_ccpo_displayname(): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL) - - assert request.status_displayname == "Pending CCPO Approval" - - -def test_request_status_pending_approved_displayname(): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.APPROVED) - - assert request.status_displayname == "Approved" - - -def test_request_status_pending_expired_displayname(): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.EXPIRED) - - assert request.status_displayname == "Expired" - - -def test_request_status_pending_deleted_displayname(): - request = RequestFactory.create() - request = Requests.set_status(request, RequestStatus.DELETED) - - assert request.status_displayname == "Deleted" - - -def test_annual_spend(): - request = RequestFactory.create() - monthly = request.body.get("details_of_use").get("estimated_monthly_spend") - assert request.annual_spend == monthly * 12 - - -def test_reviews(): - request = RequestFactory.create() - ccpo = UserFactory.from_atat_role("ccpo") - RequestStatusEventFactory.create( - request=request, - revision=request.latest_revision, - review=RequestReviewFactory.create(reviewer=ccpo), - ), - RequestStatusEventFactory.create( - request=request, - revision=request.latest_revision, - review=RequestReviewFactory.create(reviewer=ccpo), - ), - RequestStatusEventFactory.create(request=request, revision=request.latest_revision), - assert len(request.reviews) == 2 - - -def test_review_comment(): - request = RequestFactory.create() - ccpo = UserFactory.from_atat_role("ccpo") - RequestStatusEventFactory.create( - request=request, - revision=request.latest_revision, - new_status=RequestStatus.CHANGES_REQUESTED, - review=RequestReviewFactory.create(reviewer=ccpo, comment="do better"), - ) - assert request.review_comment == "do better" - - RequestStatusEventFactory.create( - request=request, - revision=request.latest_revision, - new_status=RequestStatus.APPROVED, - review=RequestReviewFactory.create(reviewer=ccpo, comment="much better"), - ) - - assert not request.review_comment - - -def test_finver_last_saved_at(): - request = RequestFactory.create() - RequestRevisionFactory.create(fname_co="Amanda", request=request) - assert request.last_finver_draft_saved_at From 2c62f54b8301dce0fd37186361382f37ddf7c1e8 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 20 Feb 2019 15:57:30 -0500 Subject: [PATCH 04/25] Remove request-related models --- atst/domain/csp/reports.py | 2 - atst/models/__init__.py | 5 - atst/models/audit_event.py | 3 - atst/models/mixins/auditable.py | 6 - atst/models/portfolio.py | 9 +- atst/models/request.py | 256 ------------------------ atst/models/request_internal_comment.py | 22 -- atst/models/request_review.py | 43 ---- atst/models/request_revision.py | 106 ---------- atst/models/request_status_event.py | 62 ------ templates/portfolios/index.html | 4 - tests/domain/test_applications.py | 5 +- tests/domain/test_portfolios.py | 53 +---- tests/domain/test_reports.py | 21 +- tests/factories.py | 145 +------------- tests/mocks.py | 2 +- tests/models/test_environments.py | 5 +- tests/models/test_portfolio_role.py | 19 +- tests/routes/test_home.py | 19 +- 19 files changed, 42 insertions(+), 745 deletions(-) delete mode 100644 atst/models/request.py delete mode 100644 atst/models/request_internal_comment.py delete mode 100644 atst/models/request_review.py delete mode 100644 atst/models/request_revision.py delete mode 100644 atst/models/request_status_event.py diff --git a/atst/domain/csp/reports.py b/atst/domain/csp/reports.py index fd498227..514b05c9 100644 --- a/atst/domain/csp/reports.py +++ b/atst/domain/csp/reports.py @@ -225,8 +225,6 @@ class MockReportingProvider(ReportingInterface): def get_budget(self, portfolio): if portfolio.name in self.REPORT_FIXTURE_MAP: return self.REPORT_FIXTURE_MAP[portfolio.name]["budget"] - elif portfolio.request and portfolio.legacy_task_order: - return portfolio.legacy_task_order.budget return 0 def get_total_spending(self, portfolio): diff --git a/atst/models/__init__.py b/atst/models/__init__.py index 32f7144a..d4d0e8cc 100644 --- a/atst/models/__init__.py +++ b/atst/models/__init__.py @@ -2,8 +2,6 @@ from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() -from .request import Request -from .request_status_event import RequestStatusEvent from .permissions import Permissions from .role import Role from .user import User @@ -14,9 +12,6 @@ from .portfolio import Portfolio from .application import Application from .environment import Environment from .attachment import Attachment -from .request_revision import RequestRevision -from .request_review import RequestReview -from .request_internal_comment import RequestInternalComment from .audit_event import AuditEvent from .invitation import Invitation from .task_order import TaskOrder diff --git a/atst/models/audit_event.py b/atst/models/audit_event.py index c705ab1d..0e46b086 100644 --- a/atst/models/audit_event.py +++ b/atst/models/audit_event.py @@ -17,9 +17,6 @@ class AuditEvent(Base, TimestampsMixin): portfolio_id = Column(UUID(as_uuid=True), ForeignKey("portfolios.id"), index=True) portfolio = relationship("Portfolio", backref="audit_events") - request_id = Column(UUID(as_uuid=True), ForeignKey("requests.id"), index=True) - request = relationship("Request", backref="audit_events") - changed_state = Column(JSONB()) event_details = Column(JSONB()) diff --git a/atst/models/mixins/auditable.py b/atst/models/mixins/auditable.py index 5424cc03..86ada487 100644 --- a/atst/models/mixins/auditable.py +++ b/atst/models/mixins/auditable.py @@ -14,7 +14,6 @@ class AuditableMixin(object): def create_audit_event(connection, resource, action): user_id = getattr_path(g, "current_user.id") portfolio_id = resource.portfolio_id - request_id = resource.request_id resource_type = resource.resource_type display_name = resource.displayname event_details = resource.event_details @@ -24,7 +23,6 @@ class AuditableMixin(object): audit_event = AuditEvent( user_id=user_id, portfolio_id=portfolio_id, - request_id=request_id, resource_type=resource_type, resource_id=resource.id, display_name=display_name, @@ -91,10 +89,6 @@ class AuditableMixin(object): def portfolio_id(self): return None - @property - def request_id(self): - return None - @property def displayname(self): return None diff --git a/atst/models/portfolio.py b/atst/models/portfolio.py index da7e8301..68b5710b 100644 --- a/atst/models/portfolio.py +++ b/atst/models/portfolio.py @@ -13,7 +13,6 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin): id = types.Id() name = Column(String) - request_id = Column(ForeignKey("requests.id"), nullable=True) applications = relationship("Application", back_populates="portfolio") roles = relationship("PortfolioRole") @@ -35,10 +34,6 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin): def user_count(self): return len(self.members) - @property - def legacy_task_order(self): - return self.request.legacy_task_order if self.request else None - @property def members(self): return ( @@ -60,6 +55,6 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin): return self.id def __repr__(self): - return "".format( - self.name, self.request_id, self.user_count, self.id + return "".format( + self.name, self.user_count, self.id ) diff --git a/atst/models/request.py b/atst/models/request.py deleted file mode 100644 index 68243468..00000000 --- a/atst/models/request.py +++ /dev/null @@ -1,256 +0,0 @@ -from sqlalchemy import Column, func, ForeignKey -from sqlalchemy.types import DateTime -from sqlalchemy.orm import relationship - -from atst.models import Base, types, mixins -from atst.models.request_status_event import RequestStatus -from atst.utils import first_or_none -from atst.models.request_revision import RequestRevision -from atst.models.legacy_task_order import Source as TaskOrderSource - - -def map_properties_to_dict(properties, instance): - return { - field: getattr(instance, field) - for field in properties - if getattr(instance, field) is not None - } - - -def update_dict_with_properties(instance, body, top_level_key, properties): - new_properties = map_properties_to_dict(properties, instance) - if new_properties: - body[top_level_key] = new_properties - - return body - - -class Request(Base, mixins.TimestampsMixin, mixins.AuditableMixin): - __tablename__ = "requests" - - id = types.Id() - time_created = Column(DateTime(timezone=True), server_default=func.now()) - status_events = relationship( - "RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence" - ) - - portfolio = relationship("Portfolio", uselist=False, backref="request") - - user_id = Column(ForeignKey("users.id"), nullable=False) - creator = relationship("User", backref="owned_requests") - - legacy_task_order_id = Column(ForeignKey("legacy_task_orders.id")) - legacy_task_order = relationship("LegacyTaskOrder") - - revisions = relationship( - "RequestRevision", back_populates="request", order_by="RequestRevision.sequence" - ) - - internal_comments = relationship("RequestInternalComment") - - @property - def latest_revision(self): - if self.revisions: - return self.revisions[-1] - - else: - return RequestRevision(request=self) - - PRIMARY_POC_FIELDS = ["am_poc", "dodid_poc", "email_poc", "fname_poc", "lname_poc"] - DETAILS_OF_USE_FIELDS = [ - "jedi_usage", - "start_date", - "cloud_native", - "dollar_value", - "dod_component", - "data_transfers", - "expected_completion_date", - "jedi_migration", - "num_software_systems", - "number_user_sessions", - "average_daily_traffic", - "engineering_assessment", - "technical_support_team", - "estimated_monthly_spend", - "average_daily_traffic_gb", - "rationalization_software_systems", - "organization_providing_assistance", - "name", - ] - INFORMATION_ABOUT_YOU_FIELDS = [ - "citizenship", - "designation", - "phone_number", - "phone_ext", - "email_request", - "fname_request", - "lname_request", - "service_branch", - "date_latest_training", - ] - FINANCIAL_VERIFICATION_FIELDS = [ - "pe_id", - "task_order_number", - "fname_co", - "lname_co", - "email_co", - "office_co", - "fname_cor", - "lname_cor", - "email_cor", - "office_cor", - "uii_ids", - "treasury_code", - "ba_code", - ] - - @property - def body(self): - current = self.latest_revision - body = {} - for top_level_key, properties in [ - ("primary_poc", Request.PRIMARY_POC_FIELDS), - ("details_of_use", Request.DETAILS_OF_USE_FIELDS), - ("information_about_you", Request.INFORMATION_ABOUT_YOU_FIELDS), - ("financial_verification", Request.FINANCIAL_VERIFICATION_FIELDS), - ]: - body = update_dict_with_properties(current, body, top_level_key, properties) - - return body - - @property - def latest_status(self): - return self.status_events[-1] if self.status_events else None - - @property - def status(self): - return self.latest_status.new_status if self.latest_status else None - - @property - def status_displayname(self): - return self.latest_status.displayname - - @property - def annual_spend(self): - monthly = self.latest_revision.estimated_monthly_spend or 0 - return monthly * 12 - - @property - def financial_verification(self): - return self.body.get("financial_verification", {}) - - @property - def is_financially_verified(self): - if self.legacy_task_order: - return self.legacy_task_order.verified - return False - - @property - def last_submission_timestamp(self): - def _is_submission(status_event): - return status_event.new_status == RequestStatus.SUBMITTED - - last_submission = first_or_none(_is_submission, reversed(self.status_events)) - if last_submission: - return last_submission.time_created - return None - - @property - def action_required_by(self): - return { - RequestStatus.PENDING_FINANCIAL_VERIFICATION: "mission_owner", - RequestStatus.CHANGES_REQUESTED: "mission_owner", - RequestStatus.CHANGES_REQUESTED_TO_FINVER: "mission_owner", - RequestStatus.PENDING_CCPO_APPROVAL: "ccpo", - RequestStatus.PENDING_CCPO_ACCEPTANCE: "ccpo", - }.get(self.status) - - @property - def reviews(self): - return [status.review for status in self.status_events if status.review] - - @property - def is_pending_financial_verification(self): - return self.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION - - @property - def is_pending_financial_verification_changes(self): - return self.status == RequestStatus.CHANGES_REQUESTED_TO_FINVER - - @property - def is_pending_ccpo_acceptance(self): - return self.status == RequestStatus.PENDING_CCPO_ACCEPTANCE - - @property - def is_pending_ccpo_approval(self): - return self.status == RequestStatus.PENDING_CCPO_APPROVAL - - @property - def is_pending_ccpo_action(self): - return self.is_pending_ccpo_acceptance or self.is_pending_ccpo_approval - - @property - def is_approved(self): - return self.status == RequestStatus.APPROVED - - @property - def review_comment(self): - if ( - self.status == RequestStatus.CHANGES_REQUESTED - or self.status == RequestStatus.CHANGES_REQUESTED_TO_FINVER - ): - review = self.latest_status.review - if review: # pragma: no branch - return review.comment - - @property - def has_financial_data(self): - return ( - self.is_pending_ccpo_approval - or self.is_pending_financial_verification_changes - or self.is_approved - ) and self.legacy_task_order - - @property - def displayname(self): - return self.latest_revision.name or self.id - - @property - def contracting_officer_full_name(self): - if self.latest_revision.fname_co: - return "{} {}".format( - self.latest_revision.fname_co, self.latest_revision.lname_co - ) - - @property - def contracting_officer_email(self): - return self.latest_revision.email_co - - @property - def pe_number(self): - return self.body.get("financial_verification", {}).get("pe_id") - - @property - def has_manual_task_order(self): - return ( - self.legacy_task_order.source == TaskOrderSource.MANUAL - if self.legacy_task_order is not None - else None - ) - - @property - def last_finver_draft_saved_at(self): - if self.latest_revision.any_finver_fields_saved: - return self.latest_revision.time_updated - else: - return None - - def __repr__(self): - return "".format( - self.status_displayname, - self.displayname, - self.creator.full_name, - self.is_approved, - self.time_created, - self.id, - ) diff --git a/atst/models/request_internal_comment.py b/atst/models/request_internal_comment.py deleted file mode 100644 index 14d9a298..00000000 --- a/atst/models/request_internal_comment.py +++ /dev/null @@ -1,22 +0,0 @@ -from sqlalchemy import Column, String, ForeignKey -from sqlalchemy.orm import relationship - -from atst.models import Base, types, mixins - - -class RequestInternalComment(Base, mixins.TimestampsMixin): - __tablename__ = "request_internal_comments" - - id = types.Id() - text = Column(String(), nullable=False) - - user_id = Column(ForeignKey("users.id"), nullable=False) - user = relationship("User") - - request_id = Column(ForeignKey("requests.id", ondelete="CASCADE"), nullable=False) - request = relationship("Request") - - def __repr__(self): # pragma: no cover - return "".format( - self.text, self.user.full_name, self.request_id, self.id - ) diff --git a/atst/models/request_review.py b/atst/models/request_review.py deleted file mode 100644 index f45fdfb7..00000000 --- a/atst/models/request_review.py +++ /dev/null @@ -1,43 +0,0 @@ -from sqlalchemy import Column, String, ForeignKey -from sqlalchemy.orm import relationship - -from atst.models import Base, mixins, types - - -class RequestReview(Base, mixins.TimestampsMixin, mixins.AuditableMixin): - __tablename__ = "request_reviews" - - id = types.Id() - status = relationship("RequestStatusEvent", uselist=False, back_populates="review") - - user_id = Column(ForeignKey("users.id")) - reviewer = relationship("User") - - comment = Column(String) - fname_mao = Column(String) - lname_mao = Column(String) - email_mao = Column(String) - phone_mao = Column(String) - phone_ext_mao = Column(String) - fname_ccpo = Column(String) - lname_ccpo = Column(String) - - @property - def full_name_reviewer(self): - if self.reviewer: - return self.reviewer.full_name - else: - return "System" - - @property - def full_name_mao(self): - return "{} {}".format(self.fname_mao, self.lname_mao) - - @property - def full_name_ccpo(self): - return "{} {}".format(self.fname_ccpo, self.lname_ccpo) - - def __repr__(self): - return "".format( - self.status.log_name, self.comment, self.full_name_reviewer, self.id - ) diff --git a/atst/models/request_revision.py b/atst/models/request_revision.py deleted file mode 100644 index 29c4e9b3..00000000 --- a/atst/models/request_revision.py +++ /dev/null @@ -1,106 +0,0 @@ -from sqlalchemy import ( - Column, - ForeignKey, - String, - Boolean, - Integer, - Date, - BigInteger, - Sequence, -) -from sqlalchemy.orm import relationship -from sqlalchemy.dialects.postgresql import ARRAY - -from atst.models import Base -from atst.models import mixins -from atst.models.types import Id - - -class RequestRevision(Base, mixins.TimestampsMixin, mixins.AuditableMixin): - __tablename__ = "request_revisions" - - id = Id() - request_id = Column(ForeignKey("requests.id"), nullable=False) - request = relationship("Request", back_populates="revisions") - sequence = Column( - BigInteger, Sequence("request_revisions_sequence_seq"), nullable=False - ) - - # primary_poc - am_poc = Column(Boolean) - dodid_poc = Column(String) - email_poc = Column(String) - fname_poc = Column(String) - lname_poc = Column(String) - - # details_of_use - jedi_usage = Column(String) - start_date = Column(Date) - cloud_native = Column(String) - dollar_value = Column(Integer) - dod_component = Column(String) - data_transfers = Column(String) - expected_completion_date = Column(String) - jedi_migration = Column(String) - num_software_systems = Column(Integer) - number_user_sessions = Column(Integer) - average_daily_traffic = Column(Integer) - engineering_assessment = Column(String) - technical_support_team = Column(String) - estimated_monthly_spend = Column(Integer) - average_daily_traffic_gb = Column(Integer) - rationalization_software_systems = Column(String) - organization_providing_assistance = Column(String) - name = Column(String) - - # information_about_you - citizenship = Column(String) - designation = Column(String) - phone_number = Column(String) - phone_ext = Column(String) - email_request = Column(String) - fname_request = Column(String) - lname_request = Column(String) - service_branch = Column(String) - date_latest_training = Column(Date) - - # financial_verification - pe_id = Column(String) - task_order_number = Column(String) - fname_co = Column(String) - lname_co = Column(String) - email_co = Column(String) - office_co = Column(String) - fname_cor = Column(String) - lname_cor = Column(String) - email_cor = Column(String) - office_cor = Column(String) - uii_ids = Column(ARRAY(String)) - treasury_code = Column(String) - ba_code = Column(String) - - def __repr__(self): # pragma: no cover - return "".format( - self.request_id, self.id - ) - - @property - def any_finver_fields_saved(self): - return any( - getattr(self, n, None) - for n in [ - "pe_id", - "task_order_number", - "fname_co", - "lname_co", - "email_co", - "office_co", - "fname_cor", - "lname_cor", - "email_cor", - "office_cor", - "uii_ids", - "treasury_code", - "ba_code", - ] - ) diff --git a/atst/models/request_status_event.py b/atst/models/request_status_event.py deleted file mode 100644 index e3367951..00000000 --- a/atst/models/request_status_event.py +++ /dev/null @@ -1,62 +0,0 @@ -from enum import Enum -from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum -from sqlalchemy.orm import relationship -from sqlalchemy.types import BigInteger -from sqlalchemy.schema import Sequence -from sqlalchemy.dialects.postgresql import UUID - -from atst.models import Base, mixins -from atst.models.types import Id - - -class RequestStatus(Enum): - STARTED = "Started" - SUBMITTED = "Submitted" - PENDING_FINANCIAL_VERIFICATION = "Pending Financial Verification" - PENDING_CCPO_ACCEPTANCE = "Pending CCPO Acceptance" - PENDING_CCPO_APPROVAL = "Pending CCPO Approval" - CHANGES_REQUESTED = "Changes Requested" - CHANGES_REQUESTED_TO_FINVER = "Change Requested to Financial Verification" - APPROVED = "Approved" - EXPIRED = "Expired" - DELETED = "Deleted" - - -class RequestStatusEvent(Base, mixins.TimestampsMixin, mixins.AuditableMixin): - __tablename__ = "request_status_events" - - id = Id() - new_status = Column(SQLAEnum(RequestStatus, native_enum=False)) - request_id = Column( - UUID(as_uuid=True), - ForeignKey("requests.id", ondelete="CASCADE"), - nullable=False, - ) - sequence = Column( - BigInteger, Sequence("request_status_events_sequence_seq"), nullable=False - ) - request_revision_id = Column(ForeignKey("request_revisions.id"), nullable=False) - revision = relationship("RequestRevision") - - request_review_id = Column(ForeignKey("request_reviews.id"), nullable=True) - review = relationship("RequestReview", back_populates="status") - - @property - def displayname(self): - return self.new_status.value if self.new_status else None - - @property - def log_name(self): - if self.new_status == RequestStatus.CHANGES_REQUESTED: - return "Denied" - if self.new_status == RequestStatus.CHANGES_REQUESTED_TO_FINVER: - return "Denied" - elif self.new_status == RequestStatus.PENDING_FINANCIAL_VERIFICATION: - return "Accepted" - else: - return self.displayname - - def __repr__(self): - return "".format( - self.log_name, self.request_id, self.id - ) diff --git a/templates/portfolios/index.html b/templates/portfolios/index.html index de713653..610658b7 100644 --- a/templates/portfolios/index.html +++ b/templates/portfolios/index.html @@ -6,7 +6,6 @@ Portfolio Name - Task Order Users @@ -16,9 +15,6 @@ {{ portfolio.name }}
- - #{{ portfolio.legacy_task_order.number }} - {{ portfolio.user_count }}Users diff --git a/tests/domain/test_applications.py b/tests/domain/test_applications.py index 9bc792d9..5ac13cde 100644 --- a/tests/domain/test_applications.py +++ b/tests/domain/test_applications.py @@ -1,11 +1,10 @@ from atst.domain.applications import Applications -from tests.factories import RequestFactory, UserFactory, PortfolioFactory +from tests.factories import UserFactory, PortfolioFactory from atst.domain.portfolios import Portfolios def test_create_application_with_multiple_environments(): - request = RequestFactory.create() - portfolio = Portfolios.create_from_request(request) + portfolio = PortfolioFactory.create() application = Applications.create( portfolio.owner, portfolio, "My Test Application", "Test", ["dev", "prod"] ) diff --git a/tests/domain/test_portfolios.py b/tests/domain/test_portfolios.py index 6c1da209..9972a682 100644 --- a/tests/domain/test_portfolios.py +++ b/tests/domain/test_portfolios.py @@ -8,12 +8,7 @@ from atst.domain.applications import Applications from atst.domain.environments import Environments from atst.models.portfolio_role import Status as PortfolioRoleStatus -from tests.factories import ( - RequestFactory, - UserFactory, - PortfolioRoleFactory, - PortfolioFactory, -) +from tests.factories import UserFactory, PortfolioRoleFactory, PortfolioFactory @pytest.fixture(scope="function") @@ -22,39 +17,21 @@ def portfolio_owner(): @pytest.fixture(scope="function") -def request_(portfolio_owner): - return RequestFactory.create(creator=portfolio_owner) - - -@pytest.fixture(scope="function") -def portfolio(request_): - portfolio = Portfolios.create_from_request(request_) +def portfolio(portfolio_owner): + portfolio = PortfolioFactory.create(owner=portfolio_owner) return portfolio -def test_can_create_portfolio(request_): - portfolio = Portfolios.create_from_request(request_, name="frugal-whale") +def test_can_create_portfolio(): + portfolio = PortfolioFactory.create(name="frugal-whale") assert portfolio.name == "frugal-whale" -def test_request_is_associated_with_portfolio(portfolio, request_): - assert portfolio.request == request_ - - -def test_default_portfolio_name_is_request_name(portfolio, request_): - assert portfolio.name == str(request_.displayname) - - def test_get_nonexistent_portfolio_raises(): with pytest.raises(NotFoundError): Portfolios.get(UserFactory.build(), uuid4()) -def test_can_get_portfolio_by_request(portfolio): - found = Portfolios.get_by_request(portfolio.request) - assert portfolio == found - - def test_creating_portfolio_adds_owner(portfolio, portfolio_owner): assert portfolio.roles[0].user == portfolio_owner @@ -162,10 +139,6 @@ def test_need_permission_to_update_portfolio_role_role(portfolio, portfolio_owne def test_owner_can_view_portfolio_members(portfolio, portfolio_owner): - portfolio_owner = UserFactory.create() - portfolio = Portfolios.create_from_request( - RequestFactory.create(creator=portfolio_owner) - ) portfolio = Portfolios.get_with_members(portfolio_owner, portfolio.id) assert portfolio @@ -258,7 +231,7 @@ def test_for_user_returns_active_portfolios_for_user(portfolio, portfolio_owner) PortfolioRoleFactory.create( user=bob, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE ) - Portfolios.create_from_request(RequestFactory.create()) + PortfolioFactory.create() bobs_portfolios = Portfolios.for_user(bob) @@ -268,7 +241,7 @@ def test_for_user_returns_active_portfolios_for_user(portfolio, portfolio_owner) def test_for_user_does_not_return_inactive_portfolios(portfolio, portfolio_owner): bob = UserFactory.from_atat_role("default") Portfolios.add_member(portfolio, bob, "developer") - Portfolios.create_from_request(RequestFactory.create()) + PortfolioFactory.create() bobs_portfolios = Portfolios.for_user(bob) assert len(bobs_portfolios) == 0 @@ -276,17 +249,13 @@ def test_for_user_does_not_return_inactive_portfolios(portfolio, portfolio_owner def test_for_user_returns_all_portfolios_for_ccpo(portfolio, portfolio_owner): sam = UserFactory.from_atat_role("ccpo") - Portfolios.create_from_request(RequestFactory.create()) + PortfolioFactory.create() sams_portfolios = Portfolios.for_user(sam) assert len(sams_portfolios) == 2 -def test_get_for_update_information(): - portfolio_owner = UserFactory.create() - portfolio = Portfolios.create_from_request( - RequestFactory.create(creator=portfolio_owner) - ) +def test_get_for_update_information(portfolio, portfolio_owner): owner_ws = Portfolios.get_for_update_information(portfolio_owner, portfolio.id) assert portfolio == owner_ws @@ -307,8 +276,8 @@ def test_get_for_update_information(): def test_can_create_portfolios_with_matching_names(): portfolio_name = "Great Portfolio" - Portfolios.create_from_request(RequestFactory.create(), name=portfolio_name) - Portfolios.create_from_request(RequestFactory.create(), name=portfolio_name) + PortfolioFactory.create(name=portfolio_name) + PortfolioFactory.create(name=portfolio_name) def test_able_to_revoke_portfolio_access_for_active_member(): diff --git a/tests/domain/test_reports.py b/tests/domain/test_reports.py index 2307abcb..d0e3a24e 100644 --- a/tests/domain/test_reports.py +++ b/tests/domain/test_reports.py @@ -1,27 +1,17 @@ from atst.domain.reports import Reports -from tests.factories import RequestFactory, LegacyTaskOrderFactory, PortfolioFactory - -CLIN_NUMS = ["0001", "0003", "1001", "1003", "2001", "2003"] +from tests.factories import PortfolioFactory def test_portfolio_totals(): - legacy_task_order = LegacyTaskOrderFactory.create() - - for num in CLIN_NUMS: - setattr(legacy_task_order, "clin_{}".format(num), 200) - - request = RequestFactory.create(legacy_task_order=legacy_task_order) - portfolio = PortfolioFactory.create(request=request) + portfolio = PortfolioFactory.create() report = Reports.portfolio_totals(portfolio) - total = 200 * len(CLIN_NUMS) - assert report == {"budget": total, "spent": 0} + assert report == {"budget": 0, "spent": 0} # this is sketched in until we do real reporting def test_monthly_totals(): - request = RequestFactory.create() - portfolio = PortfolioFactory.create(request=request) + portfolio = PortfolioFactory.create() monthly = Reports.monthly_totals(portfolio) assert not monthly["environments"] @@ -31,8 +21,7 @@ def test_monthly_totals(): # this is sketched in until we do real reporting def test_cumulative_budget(): - request = RequestFactory.create() - portfolio = PortfolioFactory.create(request=request) + portfolio = PortfolioFactory.create() months = Reports.cumulative_budget(portfolio) assert len(months["months"]) >= 12 diff --git a/tests/factories.py b/tests/factories.py index fd081e34..c4d50b02 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -9,10 +9,6 @@ from faker import Faker as _Faker from atst.forms import data from atst.models.attachment import Attachment from atst.models.environment import Environment -from atst.models.request import Request -from atst.models.request_revision import RequestRevision -from atst.models.request_review import RequestReview -from atst.models.request_status_event import RequestStatusEvent, RequestStatus from atst.models.pe_number import PENumber from atst.models.application import Application from atst.models.legacy_task_order import LegacyTaskOrder, Source, FundingType @@ -105,142 +101,6 @@ class UserFactory(Base): return cls.create(atat_role=role, **kwargs) -class RequestStatusEventFactory(Base): - class Meta: - model = RequestStatusEvent - - id = factory.Sequence(lambda x: uuid4()) - sequence = 1 - - -class RequestRevisionFactory(Base): - class Meta: - model = RequestRevision - - id = factory.Sequence(lambda x: uuid4()) - - -class RequestReviewFactory(Base): - class Meta: - model = RequestReview - - comment = factory.Faker("sentence") - fname_mao = factory.Faker("first_name") - lname_mao = factory.Faker("last_name") - email_mao = factory.Faker("email") - phone_mao = factory.LazyFunction( - lambda: "".join(random.choices(string.digits, k=10)) - ) - fname_ccpo = factory.Faker("first_name") - lname_ccpo = factory.Faker("last_name") - - -class RequestFactory(Base): - class Meta: - model = Request - - id = factory.Sequence(lambda x: uuid4()) - creator = factory.SubFactory(UserFactory) - revisions = factory.LazyAttribute( - lambda r: [RequestFactory.create_initial_revision(r)] - ) - status_events = factory.RelatedFactory( - RequestStatusEventFactory, - "request", - new_status=RequestStatus.STARTED, - revision=factory.LazyAttribute(lambda se: se.factory_parent.revisions[-1]), - ) - - class Params: - initial_revision = None - - @classmethod - def _adjust_kwargs(cls, **kwargs): - if kwargs.pop("with_task_order", False) and "legacy_task_order" not in kwargs: - kwargs["legacy_task_order"] = LegacyTaskOrderFactory.build() - return kwargs - - @classmethod - def create_initial_status_event(cls, request): - return RequestStatusEventFactory( - request=request, - new_status=RequestStatus.STARTED, - revision=request.revisions, - ) - - @classmethod - def create_initial_revision(cls, request, dollar_value=1_000_000): - user = request.creator - default_data = dict( - name=factory.Faker("domain_word"), - am_poc=False, - dodid_poc=user.dod_id, - email_poc=user.email, - fname_poc=user.first_name, - lname_poc=user.last_name, - jedi_usage="adf", - start_date=datetime.date(2050, 1, 1), - cloud_native="yes", - dollar_value=dollar_value, - dod_component=random_service_branch(), - data_transfers="Less than 100GB", - expected_completion_date="Less than 1 month", - jedi_migration="yes", - num_software_systems=1, - number_user_sessions=2, - average_daily_traffic=1, - engineering_assessment="yes", - technical_support_team="yes", - estimated_monthly_spend=100, - average_daily_traffic_gb=4, - rationalization_software_systems="yes", - organization_providing_assistance="In-house staff", - citizenship="United States", - designation="military", - phone_number="1234567890", - phone_ext="123", - email_request=user.email, - fname_request=user.first_name, - lname_request=user.last_name, - service_branch=random_service_branch(), - date_latest_training=datetime.date(2018, 8, 6), - ) - - data = ( - request.initial_revision - if request.initial_revision is not None - else default_data - ) - - return RequestRevisionFactory.build(**data) - - @classmethod - def create_with_status(cls, status=RequestStatus.STARTED, **kwargs): - request = RequestFactory(**kwargs) - RequestStatusEventFactory.create( - request=request, revision=request.latest_revision, new_status=status - ) - return request - - @classmethod - def mock_financial_data(cls): - fake = _Faker() - return { - "pe_id": "0101110F", - "fname_co": fake.first_name(), - "lname_co": fake.last_name(), - "email_co": fake.email(), - "office_co": fake.phone_number(), - "fname_cor": fake.first_name(), - "lname_cor": fake.last_name(), - "email_cor": fake.email(), - "office_cor": fake.phone_number(), - "uii_ids": "123abc", - "treasury_code": "00123456", - "ba_code": "02A", - } - - class PENumberFactory(Base): class Meta: model = PENumber @@ -269,9 +129,7 @@ class PortfolioFactory(Base): class Meta: model = Portfolio - request = factory.SubFactory(RequestFactory, with_task_order=True) - # name it the same as the request ID by default - name = factory.LazyAttribute(lambda w: w.request.id) + name = factory.Faker("name") @classmethod def _create(cls, model_class, *args, **kwargs): @@ -286,7 +144,6 @@ class PortfolioFactory(Base): for p in with_applications ] - portfolio.request.creator = owner PortfolioRoleFactory.create( portfolio=portfolio, role=Roles.get("owner"), diff --git a/tests/mocks.py b/tests/mocks.py index f5e9c2d3..8536eec8 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,4 +1,4 @@ -from tests.factories import RequestFactory, UserFactory +from tests.factories import UserFactory DOD_SDN_INFO = {"first_name": "ART", "last_name": "GARFUNKEL", "dod_id": "5892460358"} diff --git a/tests/models/test_environments.py b/tests/models/test_environments.py index cf48f2b1..1e415efa 100644 --- a/tests/models/test_environments.py +++ b/tests/models/test_environments.py @@ -1,14 +1,13 @@ from atst.domain.environments import Environments -from atst.domain.portfolios import Portfolios from atst.domain.applications import Applications -from tests.factories import RequestFactory, UserFactory +from tests.factories import PortfolioFactory, UserFactory def test_add_user_to_environment(): owner = UserFactory.create() developer = UserFactory.from_atat_role("developer") - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) application = Applications.create( owner, portfolio, diff --git a/tests/models/test_portfolio_role.py b/tests/models/test_portfolio_role.py index 33d8d119..06db96e3 100644 --- a/tests/models/test_portfolio_role.py +++ b/tests/models/test_portfolio_role.py @@ -9,7 +9,6 @@ from atst.models.invitation import Status as InvitationStatus from atst.models.audit_event import AuditEvent from atst.models.portfolio_role import Status as PortfolioRoleStatus from tests.factories import ( - RequestFactory, UserFactory, InvitationFactory, PortfolioRoleFactory, @@ -25,7 +24,7 @@ def test_has_no_ws_role_history(session): owner = UserFactory.create() user = UserFactory.create() - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) portfolio_role = PortfolioRoles.add(user, portfolio.id, "developer") create_event = ( session.query(AuditEvent) @@ -42,7 +41,7 @@ def test_has_ws_role_history(session): owner = UserFactory.create() user = UserFactory.create() - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) role = session.query(Role).filter(Role.name == "developer").one() # in order to get the history, we don't want the PortfolioRoleFactory # to commit after create() @@ -67,7 +66,7 @@ def test_has_ws_status_history(session): owner = UserFactory.create() user = UserFactory.create() - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) # in order to get the history, we don't want the PortfolioRoleFactory # to commit after create() PortfolioRoleFactory._meta.sqlalchemy_session_persistence = "flush" @@ -89,7 +88,7 @@ def test_has_ws_status_history(session): def test_has_no_env_role_history(session): owner = UserFactory.create() user = UserFactory.create() - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) application = ApplicationFactory.create(portfolio=portfolio) environment = EnvironmentFactory.create( application=application, name="new environment!" @@ -110,7 +109,7 @@ def test_has_no_env_role_history(session): def test_has_env_role_history(session): owner = UserFactory.create() user = UserFactory.create() - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user) application = ApplicationFactory.create(portfolio=portfolio) environment = EnvironmentFactory.create( @@ -137,7 +136,7 @@ def test_event_details(): owner = UserFactory.create() user = UserFactory.create() - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) portfolio_role = PortfolioRoles.add(user, portfolio.id, "developer") assert portfolio_role.event_details["updated_user_name"] == user.displayname @@ -154,7 +153,7 @@ def test_has_no_environment_roles(): "portfolio_role": "developer", } - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) portfolio_role = Portfolios.create_member(owner, portfolio, developer_data) assert not portfolio_role.has_environment_roles @@ -170,7 +169,7 @@ def test_has_environment_roles(): "portfolio_role": "developer", } - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) portfolio_role = Portfolios.create_member(owner, portfolio, developer_data) application = Applications.create( owner, @@ -195,7 +194,7 @@ def test_role_displayname(): "portfolio_role": "developer", } - portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) + portfolio = PortfolioFactory.create(owner=owner) portfolio_role = Portfolios.create_member(owner, portfolio, developer_data) assert portfolio_role.role_displayname == "Developer" diff --git a/tests/routes/test_home.py b/tests/routes/test_home.py index b2dd8d95..3e749efb 100644 --- a/tests/routes/test_home.py +++ b/tests/routes/test_home.py @@ -1,28 +1,27 @@ import pytest -from tests.factories import UserFactory, PortfolioFactory, RequestFactory +from tests.factories import UserFactory, PortfolioFactory from atst.domain.portfolios import Portfolios from atst.models.portfolio_role import Status as PortfolioRoleStatus -def test_request_owner_with_one_portfolio_redirected_to_reports(client, user_session): - request = RequestFactory.create() - portfolio = Portfolios.create_from_request(request) +def test_portfolio_owner_with_one_portfolio_redirected_to_reports(client, user_session): + portfolio = PortfolioFactory.create() - user_session(request.creator) + user_session(portfolio.owner) response = client.get("/home", follow_redirects=False) assert "/portfolios/{}/reports".format(portfolio.id) in response.location -def test_request_owner_with_more_than_one_portfolio_redirected_to_portfolios( +def test_portfolio_owner_with_more_than_one_portfolio_redirected_to_portfolios( client, user_session ): - request_creator = UserFactory.create() - Portfolios.create_from_request(RequestFactory.create(creator=request_creator)) - Portfolios.create_from_request(RequestFactory.create(creator=request_creator)) + owner = UserFactory.create() + PortfolioFactory.create(owner=owner) + PortfolioFactory.create(owner=owner) - user_session(request_creator) + user_session(owner) response = client.get("/home", follow_redirects=False) assert "/portfolios" in response.location From e2692bf6472a5f79521a882990d011d3e7c4f2a7 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 20 Feb 2019 15:58:41 -0500 Subject: [PATCH 05/25] Add migration for missing indexes A couple of us have generated migrations and these indexes are automatically added. Rather than removing them for each migration, let's make it official. --- alembic/versions/3777e9e39644_add_indexes.py | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 alembic/versions/3777e9e39644_add_indexes.py diff --git a/alembic/versions/3777e9e39644_add_indexes.py b/alembic/versions/3777e9e39644_add_indexes.py new file mode 100644 index 00000000..a48bfb4d --- /dev/null +++ b/alembic/versions/3777e9e39644_add_indexes.py @@ -0,0 +1,46 @@ +"""Add indexes + +Revision ID: 3777e9e39644 +Revises: fa3ba4049218 +Create Date: 2019-02-20 15:57:44.531311 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3777e9e39644' +down_revision = 'fa3ba4049218' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index(op.f('ix_audit_events_portfolio_id'), 'audit_events', ['portfolio_id'], unique=False) + op.drop_index('ix_audit_events_workspace_id', table_name='audit_events') + op.create_index(op.f('ix_invitations_portfolio_role_id'), 'invitations', ['portfolio_role_id'], unique=False) + op.drop_index('ix_invitations_workspace_role_id', table_name='invitations') + op.create_index(op.f('ix_portfolio_roles_portfolio_id'), 'portfolio_roles', ['portfolio_id'], unique=False) + op.create_index(op.f('ix_portfolio_roles_user_id'), 'portfolio_roles', ['user_id'], unique=False) + op.create_index('portfolio_role_user_portfolio', 'portfolio_roles', ['user_id', 'portfolio_id'], unique=True) + op.drop_index('ix_workspace_roles_user_id', table_name='portfolio_roles') + op.drop_index('ix_workspace_roles_workspace_id', table_name='portfolio_roles') + op.drop_index('workspace_role_user_workspace', table_name='portfolio_roles') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index('workspace_role_user_workspace', 'portfolio_roles', ['user_id', 'portfolio_id'], unique=True) + op.create_index('ix_workspace_roles_workspace_id', 'portfolio_roles', ['portfolio_id'], unique=False) + op.create_index('ix_workspace_roles_user_id', 'portfolio_roles', ['user_id'], unique=False) + op.drop_index('portfolio_role_user_portfolio', table_name='portfolio_roles') + op.drop_index(op.f('ix_portfolio_roles_user_id'), table_name='portfolio_roles') + op.drop_index(op.f('ix_portfolio_roles_portfolio_id'), table_name='portfolio_roles') + op.create_index('ix_invitations_workspace_role_id', 'invitations', ['portfolio_role_id'], unique=False) + op.drop_index(op.f('ix_invitations_portfolio_role_id'), table_name='invitations') + op.create_index('ix_audit_events_workspace_id', 'audit_events', ['portfolio_id'], unique=False) + op.drop_index(op.f('ix_audit_events_portfolio_id'), table_name='audit_events') + # ### end Alembic commands ### From 2e0ae917ff07d240390cd0445330b057e5f56293 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 20 Feb 2019 17:38:05 -0500 Subject: [PATCH 06/25] Add migration to remove request tables --- ...cec2f32d4_remove_request_related_models.py | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 alembic/versions/c92cec2f32d4_remove_request_related_models.py diff --git a/alembic/versions/c92cec2f32d4_remove_request_related_models.py b/alembic/versions/c92cec2f32d4_remove_request_related_models.py new file mode 100644 index 00000000..0f604966 --- /dev/null +++ b/alembic/versions/c92cec2f32d4_remove_request_related_models.py @@ -0,0 +1,150 @@ +"""Remove request related models + +Revision ID: c92cec2f32d4 +Revises: 3777e9e39644 +Create Date: 2019-02-20 17:37:33.992269 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'c92cec2f32d4' +down_revision = '3777e9e39644' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('request_status_events') + op.drop_table('request_reviews') + op.drop_table('request_internal_comments') + op.drop_table('request_revisions') + op.drop_index('ix_audit_events_request_id', table_name='audit_events') + op.drop_constraint('audit_events_request_id_fkey', 'audit_events', type_='foreignkey') + op.drop_column('audit_events', 'request_id') + op.drop_constraint('workspaces_request_id_fkey', 'portfolios', type_='foreignkey') + op.drop_table('requests') + op.drop_column('portfolios', 'request_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('portfolios', sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=True)) + op.create_foreign_key('workspaces_request_id_fkey', 'portfolios', 'requests', ['request_id'], ['id']) + op.add_column('audit_events', sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=True)) + op.create_foreign_key('audit_events_request_id_fkey', 'audit_events', 'requests', ['request_id'], ['id']) + op.create_index('ix_audit_events_request_id', 'audit_events', ['request_id'], unique=False) + op.create_table('request_revisions', + sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), + sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column('sequence', sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column('am_poc', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.Column('dodid_poc', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('email_poc', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('fname_poc', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('lname_poc', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('jedi_usage', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('start_date', sa.DATE(), autoincrement=False, nullable=True), + sa.Column('cloud_native', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('dollar_value', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('dod_component', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('data_transfers', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('expected_completion_date', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('jedi_migration', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('num_software_systems', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('number_user_sessions', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('average_daily_traffic', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('engineering_assessment', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('technical_support_team', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('estimated_monthly_spend', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('average_daily_traffic_gb', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('rationalization_software_systems', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('organization_providing_assistance', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('citizenship', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('designation', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('phone_number', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('email_request', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('fname_request', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('lname_request', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('service_branch', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('date_latest_training', sa.DATE(), autoincrement=False, nullable=True), + sa.Column('pe_id', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('task_order_number', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('fname_co', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('lname_co', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('email_co', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('office_co', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('fname_cor', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('lname_cor', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('email_cor', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('office_cor', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('uii_ids', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True), + sa.Column('treasury_code', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('ba_code', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('phone_ext', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['request_id'], ['requests.id'], name='request_revisions_request_id_fkey'), + sa.PrimaryKeyConstraint('id', name='request_revisions_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('request_internal_comments', + sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), + sa.Column('text', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['request_id'], ['requests.id'], name='request_internal_comments_request_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='request_internal_comments_user_id_fkey'), + sa.PrimaryKeyConstraint('id', name='request_internal_comments_pkey') + ) + op.create_table('requests', + sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), + sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column('legacy_task_order_id', postgresql.UUID(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['legacy_task_order_id'], ['legacy_task_orders.id'], name='requests_legacy_task_order_fkey'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='requests_user_id_fkey'), + sa.PrimaryKeyConstraint('id', name='requests_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('request_reviews', + sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), + sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=True), + sa.Column('comment', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('fname_mao', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('lname_mao', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('email_mao', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('phone_mao', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('fname_ccpo', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('lname_ccpo', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('phone_ext_mao', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='request_reviews_user_id_fkey'), + sa.PrimaryKeyConstraint('id', name='request_reviews_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('request_status_events', + sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), + sa.Column('new_status', sa.VARCHAR(length=30), autoincrement=False, nullable=True), + sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column('sequence', sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column('request_revision_id', postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column('request_review_id', postgresql.UUID(), autoincrement=False, nullable=True), + sa.CheckConstraint("(new_status)::text = ANY ((ARRAY['STARTED'::character varying, 'SUBMITTED'::character varying, 'PENDING_FINANCIAL_VERIFICATION'::character varying, 'PENDING_CCPO_ACCEPTANCE'::character varying, 'PENDING_CCPO_APPROVAL'::character varying, 'CHANGES_REQUESTED'::character varying, 'CHANGES_REQUESTED_TO_FINVER'::character varying, 'APPROVED'::character varying, 'EXPIRED'::character varying, 'DELETED'::character varying])::text[])", name='requeststatus'), + sa.ForeignKeyConstraint(['request_id'], ['requests.id'], name='request_status_events_request_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['request_review_id'], ['request_reviews.id'], name='request_status_events_request_review_id_fkey'), + sa.ForeignKeyConstraint(['request_revision_id'], ['request_revisions.id'], name='request_status_events_request_revision_id_fkey'), + sa.PrimaryKeyConstraint('id', name='request_status_events_pkey') + ) + # ### end Alembic commands ### From e8348066aa44085769c8e7d58c7969253d5a1269 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 20 Feb 2019 18:08:37 -0500 Subject: [PATCH 07/25] Remove method to create portfolio from a request --- atst/domain/portfolios/portfolios.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/atst/domain/portfolios/portfolios.py b/atst/domain/portfolios/portfolios.py index 1df66629..4ec1ae4f 100644 --- a/atst/domain/portfolios/portfolios.py +++ b/atst/domain/portfolios/portfolios.py @@ -24,16 +24,6 @@ class Portfolios(object): PortfoliosQuery.add_and_commit(portfolio) return portfolio - @classmethod - def create_from_request(cls, request, name=None): - name = name or request.displayname - portfolio = PortfoliosQuery.create(request=request, name=name) - Portfolios._create_portfolio_role( - request.creator, portfolio, "owner", status=PortfolioRoleStatus.ACTIVE - ) - PortfoliosQuery.add_and_commit(portfolio) - return portfolio - @classmethod def get(cls, user, portfolio_id): portfolio = PortfoliosQuery.get(portfolio_id) From 3ea95622846442b98b7fcb63182ea1376beda89f Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 20 Feb 2019 18:17:16 -0500 Subject: [PATCH 08/25] Remove skipped tests --- atst/routes/__init__.py | 4 ---- atst/utils/__init__.py | 18 ------------------ tests/routes/portfolios/test_applications.py | 6 ++---- tests/routes/portfolios/test_members.py | 2 -- tests/routes/test_home.py | 13 ------------- 5 files changed, 2 insertions(+), 41 deletions(-) diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index e24ed7be..4183d8ac 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -57,10 +57,6 @@ def helpdocs(doc=None): @bp.route("/home") def home(): user = g.current_user - - if user.atat_role_name == "ccpo": - return redirect(url_for("requests.requests_index")) - num_portfolios = len([role for role in user.portfolio_roles if role.is_active]) if num_portfolios == 0: diff --git a/atst/utils/__init__.py b/atst/utils/__init__.py index 5852ebf4..e54b1a16 100644 --- a/atst/utils/__init__.py +++ b/atst/utils/__init__.py @@ -5,24 +5,6 @@ def first_or_none(predicate, lst): return next((x for x in lst if predicate(x)), None) -def deep_merge(source, destination: dict): - """ - Merge source dict into destination dict recursively. - """ - - def _deep_merge(a, b): - for key, value in a.items(): - if isinstance(value, dict): - node = b.setdefault(key, {}) - _deep_merge(value, node) - else: - b[key] = value - - return b - - return _deep_merge(source, dict(destination)) - - def getattr_path(obj, path, default=None): _obj = obj for item in path.split("."): diff --git a/tests/routes/portfolios/test_applications.py b/tests/routes/portfolios/test_applications.py index 5fe52ddf..3d048e84 100644 --- a/tests/routes/portfolios/test_applications.py +++ b/tests/routes/portfolios/test_applications.py @@ -90,18 +90,16 @@ def test_user_without_permission_has_no_activity_log_link(client, user_session): ) -@pytest.mark.skip(reason="Temporarily no add application link") def test_user_with_permission_has_add_application_link(client, user_session): portfolio = PortfolioFactory.create() user_session(portfolio.owner) response = client.get("/portfolios/{}/applications".format(portfolio.id)) assert ( - 'href="/portfolios/{}/applications/new"'.format(portfolio.id).encode() + "href='/portfolios/{}/applications/new'".format(portfolio.id).encode() in response.data ) -@pytest.mark.skip(reason="Temporarily no add application link") def test_user_without_permission_has_no_add_application_link(client, user_session): user = UserFactory.create() portfolio = PortfolioFactory.create() @@ -109,7 +107,7 @@ def test_user_without_permission_has_no_add_application_link(client, user_sessio user_session(user) response = client.get("/portfolios/{}/applications".format(portfolio.id)) assert ( - 'href="/portfolios/{}/applications/new"'.format(portfolio.id).encode() + "href='/portfolios/{}/applications/new'".format(portfolio.id).encode() not in response.data ) diff --git a/tests/routes/portfolios/test_members.py b/tests/routes/portfolios/test_members.py index 58078f35..0deb6a06 100644 --- a/tests/routes/portfolios/test_members.py +++ b/tests/routes/portfolios/test_members.py @@ -37,7 +37,6 @@ def create_portfolio_and_invite_user( return portfolio -@pytest.mark.skip(reason="Temporarily no add member link") def test_user_with_permission_has_add_member_link(client, user_session): portfolio = PortfolioFactory.create() user_session(portfolio.owner) @@ -48,7 +47,6 @@ def test_user_with_permission_has_add_member_link(client, user_session): ) -@pytest.mark.skip(reason="Temporarily no add member link") def test_user_without_permission_has_no_add_member_link(client, user_session): user = UserFactory.create() portfolio = PortfolioFactory.create() diff --git a/tests/routes/test_home.py b/tests/routes/test_home.py index 3e749efb..a2eeba20 100644 --- a/tests/routes/test_home.py +++ b/tests/routes/test_home.py @@ -60,16 +60,3 @@ def test_non_owner_user_with_mulitple_portfolios_redirected_to_portfolios( alphabetically_first_portfolio = sorted(portfolios, key=lambda p: p.name)[0] assert "/portfolios" in response.location assert str(alphabetically_first_portfolio.id) in response.location - - -@pytest.mark.skip(reason="this may no longer be accurate") -def test_ccpo_user_redirected_to_requests(client, user_session): - user = UserFactory.from_atat_role("ccpo") - for _ in range(3): - portfolio = PortfolioFactory.create() - Portfolios._create_portfolio_role(user, portfolio, "developer") - - user_session(user) - response = client.get("/home", follow_redirects=False) - - assert "/requests" in response.location From 75f9b57979f50bd366927fb70b309548deddcc92 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 20 Feb 2019 18:26:56 -0500 Subject: [PATCH 09/25] Remove PE number model --- .../978bf56e21b6_remove_pe_number_model.py | 32 +++++++++++++++++++ atst/domain/pe_numbers.py | 24 -------------- atst/models/__init__.py | 1 - atst/models/pe_number.py | 15 --------- script/ingest_pe_numbers.py | 30 ----------------- tests/domain/test_pe_numbers.py | 28 ---------------- tests/factories.py | 6 ---- 7 files changed, 32 insertions(+), 104 deletions(-) create mode 100644 alembic/versions/978bf56e21b6_remove_pe_number_model.py delete mode 100644 atst/domain/pe_numbers.py delete mode 100644 atst/models/pe_number.py delete mode 100644 script/ingest_pe_numbers.py delete mode 100644 tests/domain/test_pe_numbers.py diff --git a/alembic/versions/978bf56e21b6_remove_pe_number_model.py b/alembic/versions/978bf56e21b6_remove_pe_number_model.py new file mode 100644 index 00000000..17cd2f1b --- /dev/null +++ b/alembic/versions/978bf56e21b6_remove_pe_number_model.py @@ -0,0 +1,32 @@ +"""Remove PE number model + +Revision ID: 978bf56e21b6 +Revises: c92cec2f32d4 +Create Date: 2019-02-20 18:24:37.970323 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '978bf56e21b6' +down_revision = 'c92cec2f32d4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('pe_numbers') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('pe_numbers', + sa.Column('number', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('number', name='pe_numbers_pkey') + ) + # ### end Alembic commands ### diff --git a/atst/domain/pe_numbers.py b/atst/domain/pe_numbers.py deleted file mode 100644 index 756ea026..00000000 --- a/atst/domain/pe_numbers.py +++ /dev/null @@ -1,24 +0,0 @@ -from sqlalchemy.dialects.postgresql import insert - -from atst.database import db -from atst.models.pe_number import PENumber -from .exceptions import NotFoundError - - -class PENumbers(object): - @classmethod - def get(cls, number): - pe_number = db.session.query(PENumber).get(number) - if not pe_number: - raise NotFoundError("pe_number") - - return pe_number - - @classmethod - def create_many(cls, list_of_pe_numbers): - stmt = insert(PENumber).values(list_of_pe_numbers) - do_update = stmt.on_conflict_do_update( - index_elements=["number"], set_=dict(description=stmt.excluded.description) - ) - db.session.execute(do_update) - db.session.commit() diff --git a/atst/models/__init__.py b/atst/models/__init__.py index d4d0e8cc..7055a637 100644 --- a/atst/models/__init__.py +++ b/atst/models/__init__.py @@ -6,7 +6,6 @@ from .permissions import Permissions from .role import Role from .user import User from .portfolio_role import PortfolioRole -from .pe_number import PENumber from .legacy_task_order import LegacyTaskOrder from .portfolio import Portfolio from .application import Application diff --git a/atst/models/pe_number.py b/atst/models/pe_number.py deleted file mode 100644 index 626d6842..00000000 --- a/atst/models/pe_number.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import String, Column - -from atst.models import Base - - -class PENumber(Base): - __tablename__ = "pe_numbers" - - number = Column(String, primary_key=True) - description = Column(String) - - def __repr__(self): # pragma: no cover - return "".format( - self.number, self.description - ) diff --git a/script/ingest_pe_numbers.py b/script/ingest_pe_numbers.py deleted file mode 100644 index 5dd55580..00000000 --- a/script/ingest_pe_numbers.py +++ /dev/null @@ -1,30 +0,0 @@ -from urllib.request import urlopen -import csv - -# Add root project dir to the python path -import os -import sys - -parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -sys.path.append(parent_dir) - -from atst.app import make_app, make_config -from atst.domain.pe_numbers import PENumbers - - -def get_pe_numbers(url): - response = urlopen(url) - t = response.read().decode("utf-8") - return list(csv.reader(t.split("\r\n"))) - - -if __name__ == "__main__": - config = make_config({"DISABLE_CRL_CHECK": True}) - url = config["PE_NUMBER_CSV_URL"] - print("Fetching PE numbers from {}".format(url)) - pe_numbers = get_pe_numbers(url) - - app = make_app(config) - with app.app_context(): - print("Inserting {} PE numbers".format(len(pe_numbers))) - PENumbers.create_many(pe_numbers) diff --git a/tests/domain/test_pe_numbers.py b/tests/domain/test_pe_numbers.py deleted file mode 100644 index 945a6510..00000000 --- a/tests/domain/test_pe_numbers.py +++ /dev/null @@ -1,28 +0,0 @@ -import pytest - -from atst.domain.exceptions import NotFoundError -from atst.domain.pe_numbers import PENumbers - -from tests.factories import PENumberFactory - - -def test_can_get_pe_number(): - new_pen = PENumberFactory.create( - number="0701367F", description="Combat Support - Offensive" - ) - pen = PENumbers.get(new_pen.number) - - assert pen.number == new_pen.number - - -def test_nonexistent_pe_number_raises(): - with pytest.raises(NotFoundError): - PENumbers.get("some fake number") - - -def test_create_many(): - pen_list = [["123456", "Land Speeder"], ["7891011", "Lightsaber"]] - PENumbers.create_many(pen_list) - - assert PENumbers.get(pen_list[0][0]) - assert PENumbers.get(pen_list[1][0]) diff --git a/tests/factories.py b/tests/factories.py index c4d50b02..47034d77 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -9,7 +9,6 @@ from faker import Faker as _Faker from atst.forms import data from atst.models.attachment import Attachment from atst.models.environment import Environment -from atst.models.pe_number import PENumber from atst.models.application import Application from atst.models.legacy_task_order import LegacyTaskOrder, Source, FundingType from atst.models.task_order import TaskOrder @@ -101,11 +100,6 @@ class UserFactory(Base): return cls.create(atat_role=role, **kwargs) -class PENumberFactory(Base): - class Meta: - model = PENumber - - class LegacyTaskOrderFactory(Base): class Meta: model = LegacyTaskOrder From 2d03111a9d4ab70cc7b8c6f1f925df05f0e543fe Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Wed, 20 Feb 2019 18:29:14 -0500 Subject: [PATCH 10/25] Remove legacy task order model --- ...e47972a3_remove_legacy_task_order_table.py | 49 ++++++++++++ atst/domain/legacy_task_orders.py | 60 --------------- atst/models/__init__.py | 1 - atst/models/legacy_task_order.py | 75 ------------------- tests/domain/test_legacy_task_orders.py | 28 ------- tests/factories.py | 20 ----- tests/models/test_legacy_task_order.py | 20 ----- 7 files changed, 49 insertions(+), 204 deletions(-) create mode 100644 alembic/versions/fb22e47972a3_remove_legacy_task_order_table.py delete mode 100644 atst/domain/legacy_task_orders.py delete mode 100644 atst/models/legacy_task_order.py delete mode 100644 tests/domain/test_legacy_task_orders.py delete mode 100644 tests/models/test_legacy_task_order.py diff --git a/alembic/versions/fb22e47972a3_remove_legacy_task_order_table.py b/alembic/versions/fb22e47972a3_remove_legacy_task_order_table.py new file mode 100644 index 00000000..987cfaba --- /dev/null +++ b/alembic/versions/fb22e47972a3_remove_legacy_task_order_table.py @@ -0,0 +1,49 @@ +"""Remove legacy task order table + +Revision ID: fb22e47972a3 +Revises: 978bf56e21b6 +Create Date: 2019-02-20 18:28:56.386152 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'fb22e47972a3' +down_revision = '978bf56e21b6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('legacy_task_orders') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('legacy_task_orders', + sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), + sa.Column('number', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('source', sa.VARCHAR(length=6), autoincrement=False, nullable=True), + sa.Column('funding_type', sa.VARCHAR(length=5), autoincrement=False, nullable=True), + sa.Column('funding_type_other', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('clin_0001', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('clin_0003', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('clin_1001', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('clin_1003', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('clin_2001', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('clin_2003', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('expiration_date', sa.DATE(), autoincrement=False, nullable=True), + sa.Column('attachment_id', postgresql.UUID(), autoincrement=False, nullable=True), + sa.CheckConstraint("(funding_type)::text = ANY ((ARRAY['RDTE'::character varying, 'OM'::character varying, 'PROC'::character varying, 'OTHER'::character varying])::text[])", name='fundingtype'), + sa.CheckConstraint("(source)::text = ANY ((ARRAY['MANUAL'::character varying, 'EDA'::character varying])::text[])", name='source'), + sa.ForeignKeyConstraint(['attachment_id'], ['attachments.id'], name='task_orders_attachment_id_fkey'), + sa.PrimaryKeyConstraint('id', name='task_orders_pkey'), + sa.UniqueConstraint('number', name='task_orders_number_key') + ) + # ### end Alembic commands ### diff --git a/atst/domain/legacy_task_orders.py b/atst/domain/legacy_task_orders.py deleted file mode 100644 index e7a5c44f..00000000 --- a/atst/domain/legacy_task_orders.py +++ /dev/null @@ -1,60 +0,0 @@ -from sqlalchemy.orm.exc import NoResultFound -from flask import current_app as app - -from atst.database import db -from atst.models.legacy_task_order import LegacyTaskOrder, Source, FundingType -from .exceptions import NotFoundError -from atst.utils import update_obj - - -class LegacyTaskOrders(object): - TASK_ORDER_DATA = [ - col.name for col in LegacyTaskOrder.__table__.c if col.name != "id" - ] - - @classmethod - def get(cls, order_number): - try: - legacy_task_order = ( - db.session.query(LegacyTaskOrder).filter_by(number=order_number).one() - ) - except NoResultFound: - if LegacyTaskOrders._client(): - legacy_task_order = LegacyTaskOrders.get_from_eda(order_number) - else: - raise NotFoundError("legacy_task_order") - - return legacy_task_order - - @classmethod - def get_from_eda(cls, order_number): - to_data = LegacyTaskOrders._client().get_contract(order_number, status="y") - if to_data: - # TODO: we need to determine exactly what we're getting and storing from the EDA client - return LegacyTaskOrders.create( - source=Source.EDA, funding_type=FundingType.PROC, **to_data - ) - - else: - raise NotFoundError("legacy_task_order") - - @classmethod - def create(cls, source=Source.MANUAL, **kwargs): - to_data = {k: v for k, v in kwargs.items() if v not in ["", None]} - legacy_task_order = LegacyTaskOrder(source=source, **to_data) - - db.session.add(legacy_task_order) - db.session.commit() - - return legacy_task_order - - @classmethod - def _client(cls): - return app.eda_client - - @classmethod - def update(cls, legacy_task_order, dct): - updated = update_obj(legacy_task_order, dct, ignore_vals=["", None]) - db.session.add(updated) - db.session.commit() - return updated diff --git a/atst/models/__init__.py b/atst/models/__init__.py index 7055a637..ee3a7958 100644 --- a/atst/models/__init__.py +++ b/atst/models/__init__.py @@ -6,7 +6,6 @@ from .permissions import Permissions from .role import Role from .user import User from .portfolio_role import PortfolioRole -from .legacy_task_order import LegacyTaskOrder from .portfolio import Portfolio from .application import Application from .environment import Environment diff --git a/atst/models/legacy_task_order.py b/atst/models/legacy_task_order.py deleted file mode 100644 index 72e466b5..00000000 --- a/atst/models/legacy_task_order.py +++ /dev/null @@ -1,75 +0,0 @@ -from enum import Enum - -from sqlalchemy import Column, Integer, String, ForeignKey, Enum as SQLAEnum, Date -from sqlalchemy.orm import relationship - -from atst.models import Base, types, mixins - - -class Source(Enum): - MANUAL = "Manual" - EDA = "EDA" - - -class FundingType(Enum): - RDTE = "RDTE" - OM = "OM" - PROC = "PROC" - OTHER = "OTHER" - - -class LegacyTaskOrder(Base, mixins.TimestampsMixin): - __tablename__ = "legacy_task_orders" - - id = types.Id() - number = Column(String, unique=True) - source = Column(SQLAEnum(Source, native_enum=False)) - funding_type = Column(SQLAEnum(FundingType, native_enum=False)) - funding_type_other = Column(String) - clin_0001 = Column(Integer) - clin_0003 = Column(Integer) - clin_1001 = Column(Integer) - clin_1003 = Column(Integer) - clin_2001 = Column(Integer) - clin_2003 = Column(Integer) - expiration_date = Column(Date) - - attachment_id = Column(ForeignKey("attachments.id")) - pdf = relationship("Attachment") - - @property - def verified(self): - return self.source == Source.EDA - - def to_dictionary(self): - return { - c.name: getattr(self, c.name) - for c in self.__table__.columns - if c.name not in ["id", "attachment_id"] - } - - @property - def budget(self): - return sum( - filter( - None, - [ - self.clin_0001, - self.clin_0003, - self.clin_1001, - self.clin_1003, - self.clin_2001, - self.clin_2003, - ], - ) - ) - - def __repr__(self): # pragma: no cover - return "".format( - self.number, - self.verified, - self.budget, - self.expiration_date, - self.pdf, - self.id, - ) diff --git a/tests/domain/test_legacy_task_orders.py b/tests/domain/test_legacy_task_orders.py deleted file mode 100644 index 5defb88e..00000000 --- a/tests/domain/test_legacy_task_orders.py +++ /dev/null @@ -1,28 +0,0 @@ -import pytest - -from atst.domain.exceptions import NotFoundError -from atst.domain.legacy_task_orders import LegacyTaskOrders -from atst.eda_client import MockEDAClient - -from tests.factories import LegacyTaskOrderFactory - - -def test_can_get_task_order(): - new_to = LegacyTaskOrderFactory.create(number="0101969F") - to = LegacyTaskOrders.get(new_to.number) - - assert to.id == to.id - - -def test_nonexistent_task_order_raises_without_client(): - with pytest.raises(NotFoundError): - LegacyTaskOrders.get("some fake number") - - -def test_nonexistent_task_order_raises_with_client(monkeypatch): - monkeypatch.setattr( - "atst.domain.legacy_task_orders.LegacyTaskOrders._client", - lambda: MockEDAClient(), - ) - with pytest.raises(NotFoundError): - LegacyTaskOrders.get("some other fake numer") diff --git a/tests/factories.py b/tests/factories.py index 47034d77..1226d79a 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -10,7 +10,6 @@ from atst.forms import data from atst.models.attachment import Attachment from atst.models.environment import Environment from atst.models.application import Application -from atst.models.legacy_task_order import LegacyTaskOrder, Source, FundingType from atst.models.task_order import TaskOrder from atst.models.user import User from atst.models.role import Role @@ -100,25 +99,6 @@ class UserFactory(Base): return cls.create(atat_role=role, **kwargs) -class LegacyTaskOrderFactory(Base): - class Meta: - model = LegacyTaskOrder - - source = Source.MANUAL - funding_type = FundingType.PROC - funding_type_other = None - number = factory.LazyFunction( - lambda: "".join(random.choices(string.ascii_uppercase + string.digits, k=13)) - ) - expiration_date = factory.LazyFunction(random_future_date) - clin_0001 = random.randrange(100, 100_000) - clin_0003 = random.randrange(100, 100_000) - clin_1001 = random.randrange(100, 100_000) - clin_1003 = random.randrange(100, 100_000) - clin_2001 = random.randrange(100, 100_000) - clin_2003 = random.randrange(100, 100_000) - - class PortfolioFactory(Base): class Meta: model = Portfolio diff --git a/tests/models/test_legacy_task_order.py b/tests/models/test_legacy_task_order.py deleted file mode 100644 index 80e02b05..00000000 --- a/tests/models/test_legacy_task_order.py +++ /dev/null @@ -1,20 +0,0 @@ -from tests.factories import LegacyTaskOrderFactory -from tests.assert_util import dict_contains - - -def test_as_dictionary(): - data = LegacyTaskOrderFactory.dictionary() - real_task_order = LegacyTaskOrderFactory.create(**data) - assert dict_contains(real_task_order.to_dictionary(), data) - - -def test_budget(): - legacy_task_order = LegacyTaskOrderFactory.create( - clin_0001=500, - clin_0003=200, - clin_1001=None, - clin_1003=None, - clin_2001=None, - clin_2003=None, - ) - assert legacy_task_order.budget == 700 From 1ef16c74b9bf43663784aa67002a003271d43fef Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 21 Feb 2019 16:18:41 -0500 Subject: [PATCH 11/25] Update scriptz commit --- script/include | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/include b/script/include index 78c51d8d..eb9ea572 160000 --- a/script/include +++ b/script/include @@ -1 +1 @@ -Subproject commit 78c51d8dd29b47fd42570896daaded5e2181e923 +Subproject commit eb9ea572e4c5157c8e7ba6105ac4efd1df39392e From b778942dd7fa0cae176a00aa4e08a64a686cc056 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 11:23:31 -0500 Subject: [PATCH 12/25] Remove unused get_by_request helper --- atst/domain/portfolios/portfolios.py | 4 ---- atst/domain/portfolios/query.py | 12 ------------ atst/models/portfolio.py | 2 +- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/atst/domain/portfolios/portfolios.py b/atst/domain/portfolios/portfolios.py index 4ec1ae4f..fbb40bad 100644 --- a/atst/domain/portfolios/portfolios.py +++ b/atst/domain/portfolios/portfolios.py @@ -66,10 +66,6 @@ class Portfolios(object): return portfolio - @classmethod - def get_by_request(cls, request): - return PortfoliosQuery.get_by_request(request) - @classmethod def get_with_members(cls, user, portfolio_id): portfolio = PortfoliosQuery.get(portfolio_id) diff --git a/atst/domain/portfolios/query.py b/atst/domain/portfolios/query.py index 486009eb..df82efbb 100644 --- a/atst/domain/portfolios/query.py +++ b/atst/domain/portfolios/query.py @@ -1,8 +1,5 @@ -from sqlalchemy.orm.exc import NoResultFound - from atst.database import db from atst.domain.common import Query -from atst.domain.exceptions import NotFoundError from atst.models.portfolio import Portfolio from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus @@ -10,15 +7,6 @@ from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleSta class PortfoliosQuery(Query): model = Portfolio - @classmethod - def get_by_request(cls, request): - try: - portfolio = db.session.query(Portfolio).filter_by(request=request).one() - except NoResultFound: - raise NotFoundError("portfolio") - - return portfolio - @classmethod def get_for_user(cls, user): return ( diff --git a/atst/models/portfolio.py b/atst/models/portfolio.py index 68b5710b..83043bcd 100644 --- a/atst/models/portfolio.py +++ b/atst/models/portfolio.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, ForeignKey, String +from sqlalchemy import Column, String from sqlalchemy.orm import relationship from itertools import chain From f614a3ff83285d80864c99a15423f7c60500fbff Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 11:26:20 -0500 Subject: [PATCH 13/25] Remove unused form fields --- atst/forms/fields.py | 35 +---------------------------------- tests/forms/test_fields.py | 36 +----------------------------------- 2 files changed, 2 insertions(+), 69 deletions(-) diff --git a/atst/forms/fields.py b/atst/forms/fields.py index 584aeca8..84f4cd1e 100644 --- a/atst/forms/fields.py +++ b/atst/forms/fields.py @@ -1,29 +1,4 @@ -from wtforms.fields import Field, FormField, StringField, SelectField as SelectField_ -from wtforms.widgets import TextArea - - -class NewlineListField(Field): - widget = TextArea() - - def _value(self): - if isinstance(self.data, list): - return "\n".join(self.data) - elif self.data: - return self.data - else: - return "" - - def process_formdata(self, valuelist): - if valuelist: - self.data = [l.strip() for l in valuelist[0].split("\n") if l] - else: - self.data = [] - - def process_data(self, value): - if isinstance(value, list): - self.data = "\n".join(value) - else: - self.data = value +from wtforms.fields import FormField, SelectField as SelectField_ class SelectField(SelectField_): @@ -33,14 +8,6 @@ class SelectField(SelectField_): super().__init__(*args, **kwargs) -class NumberStringField(StringField): - def process_data(self, value): - if isinstance(value, int): - self.data = str(value) - else: - self.data = value - - class FormFieldWrapper(FormField): def has_changes(self): if not self.object_data: diff --git a/tests/forms/test_fields.py b/tests/forms/test_fields.py index 8e6f4002..bdae6c85 100644 --- a/tests/forms/test_fields.py +++ b/tests/forms/test_fields.py @@ -4,41 +4,7 @@ from wtforms.fields import StringField import pendulum from werkzeug.datastructures import ImmutableMultiDict -from atst.forms.fields import NewlineListField, FormFieldWrapper - - -class NewlineListForm(Form): - newline_list = NewlineListField() - - -@pytest.mark.parametrize( - "input_,expected", - [ - ("", []), - ("hello", ["hello"]), - ("hello\n", ["hello"]), - ("hello\nworld", ["hello", "world"]), - ("hello\nworld\n", ["hello", "world"]), - ], -) -def test_newline_list_process(input_, expected): - form_data = ImmutableMultiDict({"newline_list": input_}) - form = NewlineListForm(form_data) - - assert form.validate() - assert form.data == {"newline_list": expected} - - -@pytest.mark.parametrize( - "input_,expected", - [([], ""), (["hello"], "hello"), (["hello", "world"], "hello\nworld")], -) -def test_newline_list_value(input_, expected): - form_data = {"newline_list": input_} - form = NewlineListForm(data=form_data) - - assert form.validate() - assert form.newline_list._value() == expected +from atst.forms.fields import FormFieldWrapper class PersonForm(Form): From 019e7f24c8344f3287b3e8176fa6f91bc7b68326 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 11:29:23 -0500 Subject: [PATCH 14/25] Remove unused util method --- atst/utils/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/atst/utils/__init__.py b/atst/utils/__init__.py index e54b1a16..0b77df0d 100644 --- a/atst/utils/__init__.py +++ b/atst/utils/__init__.py @@ -15,13 +15,6 @@ def getattr_path(obj, path, default=None): return _obj -def update_obj(obj, dct, ignore_vals=[None]): - for k, v in dct.items(): - if hasattr(obj, k) and v not in ignore_vals: - setattr(obj, k, v) - return obj - - def camel_to_snake(camel_cased): s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_cased) return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() From 95f2126881591ab6c30ecdcf5b4dca635be63c56 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 11:35:58 -0500 Subject: [PATCH 15/25] Remove request-related templates --- templates/requests/_new.html | 49 ---- templates/requests/_review.html | 215 -------------- templates/requests/approval.html | 271 ------------------ templates/requests/details.html | 30 -- .../requests/financial_verification.html | 220 -------------- .../financial_verification_submitted.html | 18 -- templates/requests/index.html | 178 ------------ templates/requests/menu.html | 21 -- templates/requests/review_menu.html | 21 -- templates/requests/screen-1.html | 143 --------- templates/requests/screen-2.html | 32 --- templates/requests/screen-3.html | 48 ---- templates/requests/screen-4.html | 40 --- templates/requests/sidebar.html | 33 --- 14 files changed, 1319 deletions(-) delete mode 100644 templates/requests/_new.html delete mode 100644 templates/requests/_review.html delete mode 100644 templates/requests/approval.html delete mode 100644 templates/requests/details.html delete mode 100644 templates/requests/financial_verification.html delete mode 100644 templates/requests/financial_verification_submitted.html delete mode 100644 templates/requests/index.html delete mode 100644 templates/requests/menu.html delete mode 100644 templates/requests/review_menu.html delete mode 100644 templates/requests/screen-1.html delete mode 100644 templates/requests/screen-2.html delete mode 100644 templates/requests/screen-3.html delete mode 100644 templates/requests/screen-4.html delete mode 100644 templates/requests/sidebar.html diff --git a/templates/requests/_new.html b/templates/requests/_new.html deleted file mode 100644 index 59ceeaa8..00000000 --- a/templates/requests/_new.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -
- - {% include 'requests/menu.html' %} - - {% include "fragments/flash.html" %} - - {% block form_action %} - {% if request_id %} -
- {% else %} - - {% endif %} - {% endblock %} - -
- -
-

{% block heading %}{% endblock %}

-
{{ "requests._new.new_request" | translate }}
-
- -
- - {{ f.csrf_token }} - {% block form %} - form goes here - {% endblock %} - -
- -
- - {% block next %} - -
- -
- - {% endblock %} - -
- -
- -{% endblock %} diff --git a/templates/requests/_review.html b/templates/requests/_review.html deleted file mode 100644 index 9ced019f..00000000 --- a/templates/requests/_review.html +++ /dev/null @@ -1,215 +0,0 @@ -{% macro RequiredLabel() -%} - Response Required -{%- endmacro %} - -{% macro DefinitionReviewField(title, section, item_name, filter=None, filter_args=[]) -%} -
-
{{ title | safe }}
-
- {% set value = data.get(section, {}).get(item_name) %} - {% if value is not none %} - {{ value | findFilter(filter, filter_args) }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
-{% endmacro %} - -{% macro EditLink(screen) %} - {% if request_id %} - {{ url_for('requests.requests_form_update', screen=screen, request_id=request_id)}} - {% else %} - {{ url_for('requests.requests_form_update', screen=screen, request_id=None) }} - {% endif %} -{% endmacro %} - -

- Details of Use - {% if editable %} - - {{ Icon('edit') }} - Edit this section - - {% endif %} -

- -
- - {{ DefinitionReviewField("DoD Component", "details_of_use", "dod_component", filter="getOptionLabel", filter_args=[service_branches]) }} - - {{ DefinitionReviewField("JEDI Cloud Usage", "details_of_use", "jedi_usage") }} - - {{ DefinitionReviewField("Number of software systems", "details_of_use", "num_software_systems", filter="readableInteger") }} - - {{ DefinitionReviewField("JEDI Cloud Migration", "details_of_use", "jedi_migration") }} - - {% if data['details_of_use']['jedi_migration'] == 'yes' %} - {{ DefinitionReviewField("Rationalization of Software Systems", "details_of_use", "rationalization_software_systems") }} - - {{ DefinitionReviewField("Technical Support Team", "details_of_use", "technical_support_team") }} - - {% if data['details_of_use']['technical_support_team'] == 'yes' %} - {{ DefinitionReviewField("Organization Providing Assistance", "details_of_use", "organization_providing_assistance", filter="getOptionLabel", filter_args=[assistance_org_types]) }} - {% endif %} - - {{ DefinitionReviewField("Engineering Assessment", "details_of_use", "engineering_assessment") }} - - {{ DefinitionReviewField("Data Transfers", "details_of_use", "data_transfers", filter="getOptionLabel", filter_args=[data_transfer_amounts]) }} - - {{ DefinitionReviewField("Expected Completion Date", "details_of_use", "expected_completion_date", filter="getOptionLabel", filter_args=[completion_date_ranges]) }} - - {% else %} - - {{ DefinitionReviewField("Cloud Native", "details_of_use", "cloud_native") }} - - {% endif %} - - {{ DefinitionReviewField("Estimated Monthly Spend", "details_of_use", "estimated_monthly_spend", filter="dollars") }} - - {% if jedi_request and jedi_request.annual_spend > annual_spend_threshold %} - - {{ DefinitionReviewField("Number of User Sessions", "details_of_use", "number_user_sessions", filter="readableInteger") }} - - {{ DefinitionReviewField("Average Daily Traffic (Number of Requests)", "details_of_use", "average_daily_traffic", filter="readableInteger") }} - - {{ DefinitionReviewField("Average Daily Traffic (GB)", "details_of_use", "average_daily_traffic_gb", filter="readableInteger") }} - - {% endif %} - - {{ DefinitionReviewField("Total Spend", "details_of_use", "dollar_value", filter="dollars") }} - - {{ DefinitionReviewField("Start Date", "details_of_use", "start_date") }} - - {{ DefinitionReviewField("Request Name", "details_of_use", "name") }} -
- -
-

- Information About You - {% if editable %} - - {{ Icon('edit') }} - Edit this section - - {% endif %} -

- -
- {{ DefinitionReviewField("First Name", "information_about_you", "fname_request") }} - - {{ DefinitionReviewField("Last Name", "information_about_you", "lname_request") }} - - {{ DefinitionReviewField("Email Address", "information_about_you", "email_request") }} - -
-
Phone Number
-
- {% if data.information_about_you.phone_number is not none %} - {{ data.information_about_you.phone_number }} - {% else %} - {{ RequiredLabel() }} - {% endif %} - - {% if data.information_about_you.phone_ext %} - ext. {{ data.information_about_you.phone_ext }} - {% endif %} -
-
- - {{ DefinitionReviewField("Service Branch or Agency", "information_about_you", "service_branch", filter="getOptionLabel", filter_args=[service_branches]) }} - - {{ DefinitionReviewField("Citizenship", "information_about_you", "citizenship") }} - - {{ DefinitionReviewField("Designation of Person", "information_about_you", "designation", filter="capitalize") }} - - {{ DefinitionReviewField("Latest Information Assurance (IA) Training completion date", "information_about_you", "date_latest_training") }} -
- -
-

- Portfolio Owner - {% if editable %} - - {{ Icon('edit') }} - Edit this section - - {% endif %} -

- -
- {{ DefinitionReviewField("POC First Name", "primary_poc", "fname_poc") }} - - {{ DefinitionReviewField("POC Last Name", "primary_poc", "lname_poc") }} - - {{ DefinitionReviewField("POC Email Address", "primary_poc", "email_poc") }} - - {{ DefinitionReviewField("DoD ID", "primary_poc", "dodid_poc") }} -
- -{% if jedi_request.has_financial_data %} -
-

- Financial Verification -

- -
- {% if jedi_request.legacy_task_order.pdf %} - - Download the Task Order PDF - - {% else %} -

No Task Order PDF attached.

- {% endif %} -
- -
- {{ DefinitionReviewField("Task Order Information Source", "legacy_task_order", "source", filter="getOptionLabel", filter_args=[task_order_sources]) }} - - {{ DefinitionReviewField("Task Order Number", "legacy_task_order", "number") }} - - {{ DefinitionReviewField("What is the source of funding?", "legacy_task_order", "funding_type", filter="getOptionLabel", filter_args=[funding_types]) }} - - {% if data["legacy_task_order"] and data["legacy_task_order"]["funding_type"].value == "OTHER" %} - {{ DefinitionReviewField("If other, please specify", "legacy_task_order", "funding_type_other") }} - {% endif %} - - {{ DefinitionReviewField("Task Order Expiration Date", "legacy_task_order", "expiration_date") }} - - {{ DefinitionReviewField("
CLIN 0001
-
Unclassified IaaS and PaaS Amount
", "legacy_task_order", "clin_0001", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 0003
-
Unclassified Cloud Support Package
", "legacy_task_order", "clin_0003", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 1001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 1
", "legacy_task_order", "clin_1001", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 1003
-
Unclassified Cloud Support Package
OPTION PERIOD 1
", "legacy_task_order", "clin_1003", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 2001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 2
", "legacy_task_order", "clin_2001", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 2003
-
Unclassified Cloud Support Package
OPTION PERIOD 2
", "legacy_task_order", "clin_2003", filter="dollars") }} - - {{ DefinitionReviewField("Unique Item Identifier (UII)s related to your application(s) if you already have them", "financial_verification", "uii_ids", filter="renderList") }} - - {{ DefinitionReviewField("Program Element (PE) Number related to your request", "financial_verification", "pe_id") }} - - {{ DefinitionReviewField("Program Treasury Code", "financial_verification", "treasury_code") }} - - {{ DefinitionReviewField("Program Budget Activity (BA) Code", "financial_verification", "ba_code") }} - - {{ DefinitionReviewField("Contracting Officer First Name", "financial_verification", "fname_co") }} - - {{ DefinitionReviewField("Contracting Officer Last Name", "financial_verification", "lname_co") }} - - {{ DefinitionReviewField("Contracting Officer Email", "financial_verification", "email_co") }} - - {{ DefinitionReviewField("Contracting Officer Office", "financial_verification", "office_co") }} - - {{ DefinitionReviewField("Contracting Officer Representative (COR) First Name", "financial_verification", "fname_cor") }} - - {{ DefinitionReviewField("Contracting Officer Representative (COR) Last Name", "financial_verification", "lname_cor") }} - - {{ DefinitionReviewField("Contracting Officer Representative (COR) Email", "financial_verification", "email_cor") }} - - {{ DefinitionReviewField("Contracting Officer Representative (COR) Office", "financial_verification", "office_cor") }} -
-{% endif %} diff --git a/templates/requests/approval.html b/templates/requests/approval.html deleted file mode 100644 index bc454e37..00000000 --- a/templates/requests/approval.html +++ /dev/null @@ -1,271 +0,0 @@ -{% extends "base.html" %} - -{% from "components/icon.html" import Icon %} -{% from "components/text_input.html" import TextInput %} -{% from "components/phone_input.html" import PhoneInput %} - -{% block content %} - -
- -{% include "fragments/flash.html" %} - -
-
-

- {{ "requests.approval.request_title" | translate({ "displayname": jedi_request.displayname }) }} -

- {{ current_status }} -
- -
- - {% with data=data, request_id=jedi_request.id %} - {% include "requests/_review.html" %} - {% endwith %} - -
- -
- -
-
-
-
-

{{ "requests.approval.ccpo_internal_comments" | translate }}

-
- -
- {% if comments %} -
    - {% for comment in comments %} -
  1. -
    -
    -

    {{ comment.user.full_name }}

    -

    {{ comment.text }}

    -
    - {% set timestamp=comment.time_created | formattedDate("%Y-%m-%d %H:%M:%S %Z") %} -
    - -
    -
    -
  2. - {% endfor %} -
- {% else %} -
-

- {{ "requests.approval.no_ccpo_comments" | translate }} -

-
- {% endif %} - -
- -
-

- {{ "requests.approval.add_comment" | translate }} -

-
- -
- {{ internal_comment_form.csrf_token }} - {{ TextInput(internal_comment_form.text, paragraph=True, noMaxWidth=True) }} -
-
-
- -
-
-
- - -
-
- {{ review_form.csrf_token }} - - {% set initialState = 'approving' if review_form.errors else '' %} - -
-
- -
-

- {{ "requests.approval.ccpo_review_activity" | translate }} -

-
- -
- {% if reviews %} -
    - {% for review in reviews %} -
  1. -
    -
    -

    {{ review.status.log_name }} by {{ review.full_name_reviewer }}

    - {% if review.comment %} -

    {{ review.comment }}

    - {% endif %} - -
    - {% if review.lname_mao %} -
    -

    - {{ "requests.approval.mission_owner_approval_on_behalf_of" | translate }} -

    - {{ review.full_name_mao }} - {{ review.email_mao }} - - {{ review.phone_mao }} - {% if review.phone_ext_mao %} - ext. {{ review.phone_ext_mao }} - {% endif %} - -
    - {% endif %} - - {% if review.lname_ccpo %} -
    -

    - {{ "requests.approval.ccpo_approval_on_behalf_of" | translate }} -

    - {{ review.full_name_ccpo }} -
    - {% endif %} -
    -
    - {% set timestamp=review.status.time_created | formattedDate("%Y-%m-%d %H:%M:%S %Z") %} -
    - -
    -
    -
  2. - {% endfor %} -
- {% else %} -
-

- {{ "requests.approval.no_ccpo_approval_request_changes" | translate }} -

-
- {% endif %} -
- - {% if jedi_request.is_pending_ccpo_action %} -
-

- {{ "requests.approval.review_request" | translate }} -

-
- -
- -
-
- - - - - -
-
- -
-

Message to Requestor (optional)

-
- {{ TextInput( - review_form.comment, - label=("requests.approval.approve_comments_or_notes_label" | translate), - description=("requests.approval.approve_comments_or_notes_description" | translate), - paragraph=True, - noMaxWidth=True - ) }} -
- -
- {{ TextInput( - review_form.comment, - label=("requests.approval.revision_instructions_or_notes_label" | translate), - paragraph=True, - noMaxWidth=True - ) }} -
-
- -
- -

- {{ "requests.approval.authorizing_officials_title" | translate }} - (optional) -

- -

- {{ "requests.approval.authorizing_officials_paragraph" | translate }} -

- -
- -

- {{ "requests.approval.mission_authorizing_official_title" | translate }} -

- -
-
- {{ TextInput(review_form.fname_mao, placeholder="First name of mission authorizing official") }} -
- -
- {{ TextInput(review_form.lname_mao, placeholder="Last name of mission authorizing official") }} -
-
- - {{ TextInput(review_form.email_mao, placeholder="name@mail.mil", validation='email') }} - {{ PhoneInput(review_form.phone_mao, review_form.phone_ext_mao) }} - -
- -

- {{ "requests.approval.ccpo_authorizing_official_title" | translate }} -

- -
-
- {{ TextInput(review_form.fname_ccpo, placeholder="First name of CCPO authorizing official") }} -
- -
- {{ TextInput(review_form.lname_ccpo, placeholder="Last name of CCPO authorizing official") }} -
-
-
-
- {% endif %} - -
- - {% if jedi_request.is_pending_ccpo_action %} -
- - - - {{ Icon('x') }} - Cancel - -
- {% endif %} - -
-
-
-
- -
- -{% endblock %} diff --git a/templates/requests/details.html b/templates/requests/details.html deleted file mode 100644 index 98e65dd0..00000000 --- a/templates/requests/details.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "base.html" %} - -{% from "components/alert.html" import Alert %} - -{% block content %} -
- - {% if jedi_request.is_pending_ccpo_acceptance %} - {{ Alert('Request submitted. Approval pending.', fragment="fragments/pending_ccpo_acceptance_alert.html") }} - {% elif jedi_request.is_pending_ccpo_approval %} - {{ Alert('Request submitted. Approval pending.', fragment="fragments/pending_ccpo_approval_modal.html") }} - {% elif requires_fv_action %} - {% include 'requests/review_menu.html' %} - {{ Alert('Pending Financial Verification', fragment="fragments/pending_financial_verification.html") }} - {% endif %} - -
-
-

Request Details

-

Request: {{ jedi_request.displayname }}

{{ jedi_request.status_displayname }}
-
- -
- - {% include "requests/_review.html" %} - -
-
-
-{% endblock %} diff --git a/templates/requests/financial_verification.html b/templates/requests/financial_verification.html deleted file mode 100644 index b66ab719..00000000 --- a/templates/requests/financial_verification.html +++ /dev/null @@ -1,220 +0,0 @@ -{% extends "base.html" %} - -{% from "components/alert.html" import Alert %} -{% from "components/text_input.html" import TextInput %} -{% from "components/options_input.html" import OptionsInput %} -{% from "components/date_input.html" import DateInput %} - -{% block content %} - -{% include 'requests/review_menu.html' %} - -{% include "fragments/flash.html" %} - -{% if saved_draft %} - {% call Alert(("requests.financial_verification.draft_saved" | translate), level='success') %} - {% endcall %} -{% endif %} - - -{% if jedi_request.is_pending_financial_verification and not f.errors and not extended %} - {{ Alert(("requests.financial_verification.pending_financial_verification" | translate), fragment="fragments/pending_financial_verification.html") }} -{% endif %} - - -
- {% if extended %} - {{ Alert(("requests.financial_verification.manually_enter_task_information_label" | translate), - message=("requests.financial_verification.manually_enter_task_information_description" | translate), - level='warning', - actions=[ - { - 'href': url_for('atst.helpdocs'), - 'label': ("requests.financial_verification.manually_enter_task_information_help_label" | translate), - 'icon': 'help' - } - ] - ) }} - {% endif %} - - {% if f.is_missing_task_order_number %} - {% set extended_url = url_for('requests.financial_verification', request_id=jedi_request.id, extended=True) %} - {% call Alert(("requests.financial_verification.task_order_not_found_eda_label"), level='warning') %} - {{ "requsts.financial_verification.task_order_not_found_eda_description" | translate }} -
- - {{ "requests.financial_verification.enter_task_order_manually_link_text" | translate }} - - {% endcall %} - {% endif %} - -
- - {{ f.csrf_token }} - {% block form %} - {% autoescape false %} - - {% if f.errors and not f.is_only_missing_task_order_number %} - {{ Alert(("requests.financial_verification.some_errors_label" | translate), - message="

Please see below.

", - level='error' - ) }} - {% endif %} - -
- -
-

{{ "requests.financial_verification.financial_verification_title" | translate }}

-
-

- {{ "requests.financial_verification.request_title" | translate({ "displayname" : jedi_request.displayname }) }} -

-
-
- -
- -

- {{ "requests.financial_verification.permissions_paragraph" | translate }} -

- - {% if extended %} -
- {{ OptionsInput(f.legacy_task_order.funding_type) }} - - - - {{ - DateInput( - f.legacy_task_order.expiration_date, - placeholder='MM / DD / YYYY', - validation='date', - tooltip=("requests.financial_verification.expiration_date_placeholder" | translate) - ) - }} - - {{ TextInput( - f.legacy_task_order.clin_0001, - validation='dollars' - ) }} - - {{ TextInput( - f.legacy_task_order.clin_0003, - validation='dollars' - ) }} - - {{ TextInput( - f.legacy_task_order.clin_1001, - validation='dollars' - ) }} - - {{ TextInput( - f.legacy_task_order.clin_1003, - validation='dollars' - ) }} - - {{ TextInput( - f.legacy_task_order.clin_2001, - validation='dollars' - ) }} - - {{ TextInput( - f.legacy_task_order.clin_2003, - validation='dollars' - ) }} - - - -
- {% endif %} - - {{ TextInput( - f.legacy_task_order.number, - placeholder="e.g.: 1234567899C0001", - tooltip=("requests.financial_verification.number_placeholder" | translate), - validation="requiredField" - ) }} - - {{ TextInput(f.request.uii_ids, - paragraph=True, - placeholder="examples: \nDI 0CVA5786950 \nUN1945326361234786950", - tooltip=("requests.financial_verification.uui_ids_placeholder" | translate) - ) }} - - {{ TextInput(f.request.pe_id, - placeholder="e.g.: 0105688F", - validation="peNumber" - ) }} - - {{ TextInput(f.request.treasury_code,placeholder="e.g.: 00123456",validation="treasuryCode") }} - - {{ TextInput(f.request.ba_code,placeholder="e.g.: 02A",validation="baCode") }} - -
- -

- {{ "requests.financial_verification.contracting_officer_information_title" | translate }} -

- -
-
{{ TextInput(f.request.fname_co, validation="requiredField") }}
-
{{ TextInput(f.request.lname_co, validation="requiredField") }}
-
- -
-
{{ TextInput(f.request.email_co,validation='email', placeholder='e.g. jane@mail.mil') }}
-
{{ TextInput(f.request.office_co, validation="requiredField", placeholder="e.g.: WHS") }}
-
- -
- -

- {{ "requests.financial_verification.contracting_officer_representative_information_title" | translate }} -

-
-
{{ TextInput(f.request.fname_cor, validation="requiredField") }}
-
{{ TextInput(f.request.lname_cor, validation="requiredField") }}
-
- -
-
{{ TextInput(f.request.email_cor,validation='email', placeholder='e.g. jane@mail.mil') }}
-
{{ TextInput(f.request.office_cor, validation="requiredField", placeholder="e.g.: WHS") }}
-
- - - {% endautoescape %} - -
-
- - {% endblock form %} - {% block next %} -
- - - {% if jedi_request.last_finver_draft_saved_at %} - Draft saved at - {% endif %} -
- {% endblock %} -
- -
-
- -{% endblock %} diff --git a/templates/requests/financial_verification_submitted.html b/templates/requests/financial_verification_submitted.html deleted file mode 100644 index 01d6ef89..00000000 --- a/templates/requests/financial_verification_submitted.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -
- -
- -
- -
-

Submitted

-
-
-
-
- -{% endblock %} diff --git a/templates/requests/index.html b/templates/requests/index.html deleted file mode 100644 index b0ac66f2..00000000 --- a/templates/requests/index.html +++ /dev/null @@ -1,178 +0,0 @@ -{% extends "base.html" %} - -{% from "components/modal.html" import Modal %} -{% from "components/empty_state.html" import EmptyState %} -{% from "components/icon.html" import Icon %} - -{% block content %} - - {% call Modal(name='pendingFinancialVerification', dismissable=True) %} -

{{ "requests.index.request_submitted_title" | translate }}

- - {% include 'fragments/pending_financial_verification.html' %} - -
- -
- {% endcall %} - - {% call Modal(name='pendingCCPOApproval', dismissable=True) %} -

{{ "requests.index.financial_verification_submitted_title" | translate }}

- - {% include 'fragments/pending_ccpo_approval_modal.html' %} - -
- -
- {% endcall %} - - {% call Modal(name='pendingCCPOAcceptance', dismissable=True) %} -

{{ "requests.index.request_submitted_title" | translate }}

- - {% include 'fragments/pending_ccpo_acceptance_alert.html' %} - -
- -
- {% endcall %} - - -
- -{% include "fragments/flash.html" %} - -{% if not requests %} - - {{ EmptyState( - ("requests.index.no_portfolios_label" | translate), - sub_message=("requests.index.no_portfolios_sub_message" | translate), - action_label=("requests.index.no_portfolios_action_label" | translate), - action_href=url_for('requests.requests_form_new', screen=1), - icon='document' - ) }} - -{% else %} - {% if extended_view %} -
-
-
{{ kpi_inprogress }}
-
{{ "requests.index.requests_in_progress" | translate }}
-
-
-
{{ kpi_pending }}
-
{{ "requests.index.pending_ccpo_action" | translate }}
-
-
-
{{ kpi_completed }}
-
{{ "requests.index.approved_requests" | translate }}
-
-
- {% endif %} - -
- - {% if extended_view %} - - {% endif %} - -
- - - - - - - - - - - {% if extended_view %} - - - {% endif %} - - - {% if extended_view %} - - {% endif %} - - -
- !{ column.displayName } - - {{ Icon("caret_down") }} - - - {{ Icon("caret_up") }} - -
- !{ r.name } - - {{ "requests.index.action_required" | translate }} - - - - - - !{ r.full_name }!{ dollars(r.annual_usage) } - - !{ r.status } - - - !{ r.status } - - !{ r.dod_component }
-
- {{ EmptyState( - ("requests.index.no_requests_found" | translate), - action_label=None, - action_href=None, - sub_message=("requests.index.try_different_search" | translate), - icon=None - ) }} -
-
-
-{% endif %} - -
-
- -{% endblock %} diff --git a/templates/requests/menu.html b/templates/requests/menu.html deleted file mode 100644 index 40de55d3..00000000 --- a/templates/requests/menu.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
    - {% for s in screens %} - {% if jedi_request and s.section in jedi_request.body %} - {% set step_indicator = 'complete' %} - {% elif loop.index == current %} - {% set step_indicator = 'active' %} - {% else %} - {% set step_indicator = 'incomplete' %} - {% endif %} - -
  • - - {{ s['title'] }} - -
  • - {% endfor %} -
-
diff --git a/templates/requests/review_menu.html b/templates/requests/review_menu.html deleted file mode 100644 index 51ae0fdd..00000000 --- a/templates/requests/review_menu.html +++ /dev/null @@ -1,21 +0,0 @@ -{% set pending_url=url_for('requests.view_request_details', request_id=jedi_request.id) %} -{% set financial_url=url_for('requests.financial_verification', request_id=jedi_request.id) %} -
- -
diff --git a/templates/requests/screen-1.html b/templates/requests/screen-1.html deleted file mode 100644 index 0d8b52ef..00000000 --- a/templates/requests/screen-1.html +++ /dev/null @@ -1,143 +0,0 @@ -{% extends 'requests/_new.html' %} - -{% from "components/text_input.html" import TextInput %} -{% from "components/options_input.html" import OptionsInput %} -{% from "components/date_input.html" import DateInput %} - -{% block heading %} - Details of Use -{% endblock %} - -{% block form %} - -{% include "fragments/flash.html" %} - - -
- - {{ "requests.screen-1.form_instructions" | translate }} - -

{{ "requests.screen-1.general_title_text"| translate }}

- {{ OptionsInput(f.dod_component) }} - {{ - TextInput( - f.jedi_usage, - paragraph=True, - placeholder=("requests.screen-1.jedi_usage_placeholder" | translate) - ) - }} - -

{{ "requests.screen-1.cloud_readiness_title_text" | translate }}

- {{ - TextInput( - f.num_software_systems, - validation="integer", - tooltip=("requests.screen-1.num_software_systems_tooltip" | translate), - placeholder="0" - ) - }} - {{ - OptionsInput( - f.jedi_migration, - tooltip=("requests.screen-1.jedi_migration_tooltip" | translate) - ) - }} - - - - - -

{{ "requests.screen-1.financial_usage_title" | translate }}

- {{ - TextInput( - f.estimated_monthly_spend, - tooltip=("requests.screen-1.estimated_monthly_spend_tooltip" | translate), - validation="dollars", - placeholder="$0" - ) - }} - -
-
-
- {{ "requests.screen-1.approximate_annual_spend_paragraph" | translate }} -
-
-
- - - - - - {{ - TextInput( - f.dollar_value, - validation='dollars', - placeholder='$0', - tooltip=("requests.screen-1.dollar_value_tooltip" | translate) - ) - }} - {{ DateInput(f.start_date, placeholder='MM / DD / YYYY', validation='date') }} - {{ TextInput(f.name, placeholder='Request Name', validation='portfolioName') }} - -
-
- -{% endblock %} diff --git a/templates/requests/screen-2.html b/templates/requests/screen-2.html deleted file mode 100644 index 03984dd4..00000000 --- a/templates/requests/screen-2.html +++ /dev/null @@ -1,32 +0,0 @@ -{% extends 'requests/_new.html' %} - -{% from "components/text_input.html" import TextInput %} -{% from "components/options_input.html" import OptionsInput %} -{% from "components/date_input.html" import DateInput %} -{% from "components/phone_input.html" import PhoneInput %} - -{% block heading %} - Information About You -{% endblock %} - -{% block form %} - -{% include "fragments/flash.html" %} - -

Please tell us more about you.

- -
-
{{ TextInput(f.fname_request) }}
-
{{ TextInput(f.lname_request) }}
-
- -{{ TextInput(f.email_request, placeholder='e.g. jane@mail.mil', validation='email') }} -{{ PhoneInput(f.phone_number, f.phone_ext, placeholder_phone='e.g. (123) 456-7890') }} - -

We want to collect the following information from you for security auditing and determining priviledged user access.

- -{{ OptionsInput(f.service_branch) }} -{{ OptionsInput(f.citizenship) }} -{{ OptionsInput(f.designation) }} -{{ DateInput(f.date_latest_training,tooltip="When was the last time you completed the IA training?
Information Assurance (IA) training is an important step in cyber awareness.",placeholder="MM / DD / YYYY", validation="date") }} -{% endblock %} diff --git a/templates/requests/screen-3.html b/templates/requests/screen-3.html deleted file mode 100644 index 2ac4e223..00000000 --- a/templates/requests/screen-3.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends 'requests/_new.html' %} - -{% from "components/text_input.html" import TextInput %} -{% from "components/checkbox_input.html" import CheckboxInput %} - -{% block heading %} - Designate a Portfolio Owner -{% endblock %} - -{% block form %} - -{% include "fragments/flash.html" %} - - -
- -

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

-
    -
  • Organize your cloud-hosted systems into applications and environments
  • -
  • Add users to this portfolio and manage members
  • -
  • Manage access to the JEDI Cloud service provider’s portal
  • -
-

- -

This person must be a DoD employee (not a contractor).

-

The Portfolio 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/templates/requests/screen-4.html b/templates/requests/screen-4.html deleted file mode 100644 index 4c4443c4..00000000 --- a/templates/requests/screen-4.html +++ /dev/null @@ -1,40 +0,0 @@ -{% macro RequiredLabel() -%} - Response Required -{%- endmacro %} - -{% extends 'requests/_new.html' %} - -{% from "components/text_input.html" import TextInput %} -{% from "components/icon.html" import Icon %} - -{% block heading %} - Review & Submit -{% endblock %} - - -{% block form_action %} -
-{% endblock %} - - {% block form %} - -

Before you can submit your request, please take a moment to review the information entered in the form. You may make changes by clicking the edit link on each section. When all information looks right, go ahead and submit.

- - {% include "fragments/flash.html" %} - - {% with editable=True %} - {% include "requests/_review.html" %} - {% endwith %} - - -{% endblock %} - -{% block next %} - -
- -
- -
- -{% endblock %} diff --git a/templates/requests/sidebar.html b/templates/requests/sidebar.html deleted file mode 100644 index f8516917..00000000 --- a/templates/requests/sidebar.html +++ /dev/null @@ -1,33 +0,0 @@ -
- - - -
From d3e6b7fbab664ca2d8c271e271ef9e1810b98e62 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 11:44:07 -0500 Subject: [PATCH 16/25] Remove no longer used js components --- js/components/__tests__/requests_list.test.js | 86 ---------- js/components/forms/ccpo_approval.js | 34 ---- js/components/forms/details_of_use.js | 64 ------- js/components/forms/financial.js | 48 ------ js/components/requests_list.js | 161 ------------------ js/index.js | 8 - 6 files changed, 401 deletions(-) delete mode 100644 js/components/__tests__/requests_list.test.js delete mode 100644 js/components/forms/ccpo_approval.js delete mode 100644 js/components/forms/details_of_use.js delete mode 100644 js/components/forms/financial.js delete mode 100644 js/components/requests_list.js diff --git a/js/components/__tests__/requests_list.test.js b/js/components/__tests__/requests_list.test.js deleted file mode 100644 index e9fa1e9d..00000000 --- a/js/components/__tests__/requests_list.test.js +++ /dev/null @@ -1,86 +0,0 @@ -import { shallowMount } from '@vue/test-utils' - -import RequestsList from '../requests_list' - -describe('RequestsList', () => { - describe('isExtended', () => { - it('should disallow sorting if not extended', () => { - const wrapper = shallowMount(RequestsList, { - propsData: { isExtended: false }, - }) - expect(wrapper.vm.sort.columnName).toEqual('') - wrapper.vm.updateSortValue('full_name') - expect(wrapper.vm.sort.columnName).toEqual('') - }) - - it('should allow sorting when in extended mode', () => { - const wrapper = shallowMount(RequestsList, { - propsData: { isExtended: true }, - }) - expect(wrapper.vm.sort.columnName).toEqual('last_submission_timestamp') - wrapper.vm.updateSortValue('full_name') - expect(wrapper.vm.sort.columnName).toEqual('full_name') - }) - }) - - describe('sorting', () => { - const requests = [ - { - name: 'X Wing', - last_edited_timestamp: 'Mon, 2 Jan 2017 12:34:56 GMT', - last_submission_timestamp: 'Mon, 2 Jan 2017 12:34:56 GMT', - full_name: 'Luke Skywalker', - annual_usage: '80000', - status: 'Approved', - dod_component: 'Rebels', - }, - { - name: 'TIE Fighter', - last_edited_timestamp: 'Mon, 12 Nov 2018 12:34:56 GMT', - last_submission_timestamp: 'Mon, 12 Nov 2018 12:34:56 GMT', - full_name: 'Darth Vader', - annual_usage: '999999', - status: 'Approved', - dod_component: 'Empire', - }, - ] - - const mountWrapper = () => - shallowMount(RequestsList, { propsData: { requests, isExtended: true } }) - - it('should default to sorting by submission recency', () => { - const wrapper = mountWrapper() - const displayedRequests = wrapper.vm.filteredRequests - const requestNames = displayedRequests.map(req => req.name) - expect(requestNames).toEqual(['TIE Fighter', 'X Wing']) - }) - - it('should reverse sort by submission time when selected', () => { - const wrapper = mountWrapper() - wrapper.vm.updateSortValue('last_submission_timestamp') - const displayedRequests = wrapper.vm.filteredRequests - const requestNames = displayedRequests.map(req => req.name) - expect(requestNames).toEqual(['X Wing', 'TIE Fighter']) - }) - - it('handles sorting with un-submitted requests', () => { - const unsubmittedRequest = { - name: 'Death Star', - status: 'Started', - last_submission_timestamp: null, - } - const wrapper = shallowMount(RequestsList, { - propsData: { - requests: [unsubmittedRequest, ...requests], - isExtended: true, - }, - }) - const displayedRequests = wrapper.vm.filteredRequests - expect(displayedRequests).toEqual([ - requests[1], - requests[0], - unsubmittedRequest, - ]) - }) - }) -}) diff --git a/js/components/forms/ccpo_approval.js b/js/components/forms/ccpo_approval.js deleted file mode 100644 index 20b69908..00000000 --- a/js/components/forms/ccpo_approval.js +++ /dev/null @@ -1,34 +0,0 @@ -import textinput from '../text_input' -import LocalDatetime from '../local_datetime' - -export default { - name: 'ccpo-approval', - - components: { - textinput, - LocalDatetime, - }, - - props: { - initialState: String, - }, - - data: function() { - return { - approving: this.initialState === 'approving', - denying: this.initialState === 'denying', - } - }, - - methods: { - setReview: function(e) { - if (e.target.value === 'approving') { - this.approving = true - this.denying = false - } else { - this.approving = false - this.denying = true - } - }, - }, -} diff --git a/js/components/forms/details_of_use.js b/js/components/forms/details_of_use.js deleted file mode 100644 index 74d461bd..00000000 --- a/js/components/forms/details_of_use.js +++ /dev/null @@ -1,64 +0,0 @@ -import createNumberMask from 'text-mask-addons/dist/createNumberMask' -import { conformToMask } from 'vue-text-mask' - -import FormMixin from '../../mixins/form' -import textinput from '../text_input' -import optionsinput from '../options_input' - -export default { - name: 'details-of-use', - - mixins: [FormMixin], - - components: { - textinput, - optionsinput, - }, - - props: { - initialData: { - type: Object, - default: () => ({}), - }, - }, - - data: function() { - const { - estimated_monthly_spend = 0, - jedi_migration = '', - technical_support_team = '', - } = this.initialData - - return { - estimated_monthly_spend, - jedi_migration, - technical_support_team, - } - }, - - computed: { - annualSpend: function() { - const monthlySpend = this.estimated_monthly_spend || 0 - return monthlySpend * 12 - }, - annualSpendStr: function() { - return this.formatDollars(this.annualSpend) - }, - jediMigrationOptionSelected: function() { - return this.jedi_migration !== '' - }, - isJediMigration: function() { - return this.jedi_migration === 'yes' - }, - hasTechnicalSupportTeam: function() { - return this.technical_support_team === 'yes' - }, - }, - - methods: { - formatDollars: function(intValue) { - const mask = createNumberMask({ prefix: '$', allowDecimal: true }) - return conformToMask(intValue.toString(), mask).conformedValue - }, - }, -} diff --git a/js/components/forms/financial.js b/js/components/forms/financial.js deleted file mode 100644 index 84402a85..00000000 --- a/js/components/forms/financial.js +++ /dev/null @@ -1,48 +0,0 @@ -import FormMixin from '../../mixins/form' -import optionsinput from '../options_input' -import textinput from '../text_input' -import localdatetime from '../local_datetime' - -export default { - name: 'financial', - - mixins: [FormMixin], - - components: { - optionsinput, - textinput, - localdatetime, - }, - - props: { - initialData: { - type: Object, - default: () => ({}), - }, - }, - - data: function() { - const { funding_type = '' } = this.initialData - - return { - funding_type, - shouldForceShowTaskOrder: false, - } - }, - - computed: { - showTaskOrderUpload: function() { - return ( - !this.initialData.legacy_task_order.pdf || this.shouldForceShowTaskOrder - ) - }, - }, - - methods: { - forceShowTaskOrderUpload: function(e) { - console.log('forceShowTaskOrder', e) - e.preventDefault() - this.shouldForceShowTaskOrder = true - }, - }, -} diff --git a/js/components/requests_list.js b/js/components/requests_list.js deleted file mode 100644 index 6112fd75..00000000 --- a/js/components/requests_list.js +++ /dev/null @@ -1,161 +0,0 @@ -import LocalDatetime from '../components/local_datetime' -import { formatDollars } from '../lib/dollars' -import { parse } from 'date-fns' -import { - compose, - partial, - indexBy, - prop, - propOr, - sortBy, - reverse, - pipe, -} from 'ramda' - -export default { - name: 'requests-list', - - components: { - LocalDatetime, - }, - - props: { - requests: { - type: Array, - default: () => [], - }, - isExtended: { - type: Boolean, - default: false, - }, - statuses: { - type: Array, - default: () => [], - }, - dodComponents: { - type: Array, - default: () => [], - }, - }, - - data: function() { - const defaultSort = (sort, requests) => - sortBy(prop(sort.columnName), requests) - const dateSort = (sort, requests) => { - const parseDate = compose( - partial(parse), - propOr(sort.columnName, '') - ) - return sortBy(parseDate, requests) - } - - const columnList = [ - { - displayName: 'JEDI Cloud Request Name', - attr: 'name', - sortFunc: defaultSort, - }, - { - displayName: 'Date Request Submitted', - attr: 'last_submission_timestamp', - sortFunc: dateSort, - }, - { - displayName: 'Date Request Last Edited', - attr: 'last_edited_timestamp', - extendedOnly: true, - sortFunc: dateSort, - }, - { - displayName: 'Requester', - attr: 'full_name', - extendedOnly: true, - sortFunc: defaultSort, - }, - { - displayName: 'Applicationed Annual Usage ($)', - attr: 'annual_usage', - sortFunc: defaultSort, - }, - { - displayName: 'Request Status', - attr: 'status', - sortFunc: defaultSort, - }, - { - displayName: 'DOD Component', - attr: 'dod_component', - extendedOnly: true, - sortFunc: defaultSort, - }, - ] - - const defaultSortColumn = this.isExtended ? 'last_submission_timestamp' : '' - return { - searchValue: '', - statusValue: '', - dodComponentValue: '', - sort: { - columnName: defaultSortColumn, - isAscending: false, - }, - columns: indexBy(prop('attr'), columnList), - } - }, - - computed: { - filteredRequests: function() { - return pipe( - partial(this.applySearch, [this.searchValue]), - partial(this.applyFilters, [this.statusValue, this.dodComponentValue]), - partial(this.applySort, [this.sort]) - )(this.requests) - }, - }, - - methods: { - getColumns: function() { - return Object.values(this.columns).filter( - column => !column.extendedOnly || this.isExtended - ) - }, - applySearch: (query, requests) => { - return requests.filter(request => - query !== '' - ? request.name.toLowerCase().includes(query.toLowerCase()) - : true - ) - }, - applyFilters: (status, dodComponent, requests) => { - return requests - .filter(request => (status !== '' ? request.status === status : true)) - .filter(request => - dodComponent !== '' ? request.dod_component === dodComponent : true - ) - }, - applySort: function(sort, requests) { - if (sort.columnName === '') { - return requests - } else { - const { sortFunc } = this.columns[sort.columnName] - const sorted = sortFunc(sort, requests) - return sort.isAscending ? sorted : reverse(sorted) - } - }, - dollars: value => formatDollars(value, false), - updateSortValue: function(columnName) { - if (!this.isExtended) { - return - } - - // toggle ascending / descending if column is clicked twice - if (columnName === this.sort.columnName) { - this.sort.isAscending = !this.sort.isAscending - } - - this.sort.columnName = columnName - }, - }, - - template: '
', -} diff --git a/js/index.js b/js/index.js index fbdd5814..dbc72654 100644 --- a/js/index.js +++ b/js/index.js @@ -11,11 +11,9 @@ import optionsinput from './components/options_input' import multicheckboxinput from './components/multi_checkbox_input' import textinput from './components/text_input' import checkboxinput from './components/checkbox_input' -import DetailsOfUse from './components/forms/details_of_use' import EditOfficerForm from './components/forms/edit_officer_form' import poc from './components/forms/poc' import oversight from './components/forms/oversight' -import financial from './components/forms/financial' import toggler from './components/toggler' import NewApplication from './components/forms/new_application' import EditEnvironmentRole from './components/forms/edit_environment_role' @@ -27,10 +25,8 @@ import selector from './components/selector' import BudgetChart from './components/charts/budget_chart' import SpendTable from './components/tables/spend_table' import TaskOrderList from './components/tables/task_order_list.js' -import CcpoApproval from './components/forms/ccpo_approval' import MembersList from './components/members_list' import LocalDatetime from './components/local_datetime' -import RequestsList from './components/requests_list' import ConfirmationPopover from './components/confirmation_popover' import { isNotInVerticalViewport } from './lib/viewport' import DateSelector from './components/date_selector' @@ -51,21 +47,17 @@ const app = new Vue({ multicheckboxinput, textinput, checkboxinput, - DetailsOfUse, poc, oversight, - financial, NewApplication, selector, BudgetChart, SpendTable, TaskOrderList, - CcpoApproval, MembersList, LocalDatetime, EditEnvironmentRole, EditApplicationRoles, - RequestsList, ConfirmationPopover, funding, uploadinput, From 69ebd21de9b7374da374ff8ebdc504b2de62872a Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 11:49:16 -0500 Subject: [PATCH 17/25] Remove unused filters --- atst/filters.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/atst/filters.py b/atst/filters.py index 011d930b..02d51c5c 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -36,23 +36,6 @@ def usPhone(number): return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:]) -def readableInteger(value): - try: - numberValue = int(value) - except ValueError: - numberValue = 0 - return "{:,}".format(numberValue) - - -def getOptionLabel(value, options): - if hasattr(value, "value"): - value = value.name - try: - return next(tup[1] for tup in options if tup[0] == value) # pragma: no branch - except StopIteration: - return - - def findFilter(value, filter_name, filter_args=[]): if not filter_name: return value @@ -62,10 +45,6 @@ def findFilter(value, filter_name, filter_args=[]): raise ValueError("filter name {} not found".format(filter_name)) -def renderList(value): - return app.jinja_env.filters["safe"]("
".join(value)) - - def formattedDate(value, formatter="%m/%d/%Y"): if value: return value.strftime(formatter) @@ -95,11 +74,6 @@ def renderAuditEvent(event): return render_template("audit_log/events/default.html", event=event) -def removeHtml(text): - html_tags = re.compile("<.*?>") - return re.sub(html_tags, "", text) - - def normalizeOrder(title): # reorders titles from "Army, Department of the" to "Department of the Army" text = title.split(", ") @@ -114,15 +88,11 @@ def register_filters(app): app.jinja_env.filters["justDollars"] = justDollars app.jinja_env.filters["justCents"] = justCents app.jinja_env.filters["usPhone"] = usPhone - app.jinja_env.filters["readableInteger"] = readableInteger - app.jinja_env.filters["getOptionLabel"] = getOptionLabel app.jinja_env.filters["findFilter"] = findFilter - app.jinja_env.filters["renderList"] = renderList app.jinja_env.filters["formattedDate"] = formattedDate app.jinja_env.filters["dateFromString"] = dateFromString app.jinja_env.filters["pageWindow"] = pageWindow app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent - app.jinja_env.filters["removeHtml"] = removeHtml app.jinja_env.filters["normalizeOrder"] = normalizeOrder app.jinja_env.filters["translateDuration"] = translate_duration From c9ca22899d13c9a018e188ff324a8721f1005c2b Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 14:25:08 -0500 Subject: [PATCH 18/25] Add some tests for portfolio index routes --- .../portfolios/test_portfolios_index.py | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/routes/portfolios/test_portfolios_index.py b/tests/routes/portfolios/test_portfolios_index.py index 5899f307..39849480 100644 --- a/tests/routes/portfolios/test_portfolios_index.py +++ b/tests/routes/portfolios/test_portfolios_index.py @@ -1,6 +1,12 @@ from flask import url_for -from tests.factories import PortfolioFactory, UserFactory +from tests.factories import ( + random_future_date, + random_past_date, + PortfolioFactory, + TaskOrderFactory, + UserFactory, +) from atst.utils.localization import translate @@ -40,3 +46,46 @@ def test_portfolio_index_without_existing_portfolios(client, user_session): assert ( translate("portfolios.index.empty.start_button").encode("utf8") in response.data ) + + +def test_portfolio_admin_screen(client, user_session): + portfolio = PortfolioFactory.create() + user_session(portfolio.owner) + response = client.get( + url_for("portfolios.portfolio_admin", portfolio_id=portfolio.id) + ) + assert response.status_code == 200 + assert portfolio.name in response.data.decode() + + +def test_portfolio_reports(client, user_session): + portfolio = PortfolioFactory.create( + applications=[ + {"name": "application1", "environments": [{"name": "application1 prod"}]} + ] + ) + task_order = TaskOrderFactory.create( + number="42", + start_date=random_past_date(), + end_date=random_future_date(), + portfolio=portfolio, + ) + user_session(portfolio.owner) + response = client.get( + url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id) + ) + assert response.status_code == 200 + assert portfolio.name in response.data.decode() + expiration_date = task_order.end_date.strftime("%Y-%m-%d") + assert expiration_date in response.data.decode() + + +def test_portfolio_reports_with_mock_portfolio(client, user_session): + portfolio = PortfolioFactory.create(name="Aardvark") + user_session(portfolio.owner) + response = client.get( + url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id) + ) + assert response.status_code == 200 + assert portfolio.name in response.data.decode() + assert "$237,617.00 Total spend to date" in response.data.decode() From ae1b2087091316fdc7774a70886d18b00b0a3134 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 14:34:48 -0500 Subject: [PATCH 19/25] Remove unused portfolio route The activity was moved into the admin page, so this separate route is not necessary anymore. --- atst/routes/portfolios/index.py | 16 ---------------- templates/portfolios/activity/index.html | 11 ----------- 2 files changed, 27 deletions(-) delete mode 100644 templates/portfolios/activity/index.html diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index 6be58772..274c26b4 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -98,19 +98,3 @@ def portfolio_reports(portfolio_id): expiration_date=expiration_date, remaining_days=remaining_days, ) - - -@portfolios_bp.route("/portfolios//activity") -def portfolio_activity(portfolio_id): - portfolio = Portfolios.get(g.current_user, portfolio_id) - pagination_opts = Paginator.get_pagination_opts(http_request) - audit_events = AuditLog.get_portfolio_events( - g.current_user, portfolio, pagination_opts - ) - - return render_template( - "portfolios/activity/index.html", - portfolio_name=portfolio.name, - portfolio_id=portfolio_id, - audit_events=audit_events, - ) diff --git a/templates/portfolios/activity/index.html b/templates/portfolios/activity/index.html deleted file mode 100644 index c28b766c..00000000 --- a/templates/portfolios/activity/index.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "portfolios/base.html" %} -{% from "components/pagination.html" import Pagination %} - -{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.admin" | translate %} - -{% block portfolio_content %} -
- {% include "fragments/audit_events_log.html" %} - {{ Pagination(audit_events, 'portfolios.portfolio_activity', portfolio_id=portfolio_id) }} -
-{% endblock %} From 8a0d254224493ba92874402d189ada3dbb73f908 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 14:36:51 -0500 Subject: [PATCH 20/25] Remove unused flashes --- atst/utils/flash.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 3614dffb..89261795 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -88,29 +88,6 @@ MESSAGES = { "message_template": "", "category": "success", }, - "request_incomplete": { - "title_template": "Please complete all sections", - "message_template": """ -

In order to submit your JEDI Cloud request, you'll need to complete all required sections of this form without error. Missing or invalid fields are noted below.

- """, - "category": "error", - }, - "requests_action_required": { - "title_template": "Action required on {{ count }} requests.", - "message_template": "", - "category": "info", - }, - "request_review_comment": { - "title_template": "Changes Requested", - "message_template": """ -

CCPO has requested changes to your submission with the following notes: -
- {{ comment }} -
- Please contact info@jedi.cloud or 123-123-4567 for further discussion.

- """, - "category": "warning", - }, "environment_access_changed": { "title_template": "User access successfully changed.", "message_template": "", From 73b553bbce82c2d4e2bf941ae75a2488b8bb2847 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 14:38:14 -0500 Subject: [PATCH 21/25] Remove unused email template for request status change --- templates/emails/request_status_change.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 templates/emails/request_status_change.txt diff --git a/templates/emails/request_status_change.txt b/templates/emails/request_status_change.txt deleted file mode 100644 index 5b48c739..00000000 --- a/templates/emails/request_status_change.txt +++ /dev/null @@ -1,5 +0,0 @@ -Your JEDI request status has changed - -The status of your JEDI Cloud request - {{ request.displayname }} - was recently updated. Log in to see whether this change requires an action or response from you. - -{{ url_for('requests.edit', request_id=request.id, _external=True) }} From d6a1ba08fb467a8f5771ed51bf17d02b10e4d5a1 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 14:53:54 -0500 Subject: [PATCH 22/25] Add a test for creating applications --- tests/routes/portfolios/test_applications.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/routes/portfolios/test_applications.py b/tests/routes/portfolios/test_applications.py index 3d048e84..573773ca 100644 --- a/tests/routes/portfolios/test_applications.py +++ b/tests/routes/portfolios/test_applications.py @@ -112,6 +112,24 @@ def test_user_without_permission_has_no_add_application_link(client, user_sessio ) +def test_creating_application(client, user_session): + portfolio = PortfolioFactory.create() + user_session(portfolio.owner) + response = client.post( + url_for("portfolios.create_application", portfolio_id=portfolio.id), + data={ + "name": "Test Application", + "description": "This is only a test", + "environment_names-0": "dev", + "environment_names-1": "staging", + "environment_names-2": "prod", + }, + ) + assert response.status_code == 302 + assert len(portfolio.applications) == 1 + assert len(portfolio.applications[0].environments) == 3 + + def test_view_edit_application(client, user_session): portfolio = PortfolioFactory.create() application = Applications.create( From 9d97a45a8d92daaa1658ad5018238d3cac08060f Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Fri, 22 Feb 2019 14:55:25 -0500 Subject: [PATCH 23/25] Remove some more unused utils --- atst/utils/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/atst/utils/__init__.py b/atst/utils/__init__.py index 0b77df0d..01988e10 100644 --- a/atst/utils/__init__.py +++ b/atst/utils/__init__.py @@ -20,11 +20,6 @@ def camel_to_snake(camel_cased): return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() -def drop(keys, dct): - _keys = set(keys) - return {k: v for k, v in dct.items() if k not in _keys} - - def pick(keys, dct): _keys = set(keys) return {k: v for (k, v) in dct.items() if k in _keys} From 4081fa04e733e123218cab8065426b9106873c2f Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 25 Feb 2019 14:51:49 -0500 Subject: [PATCH 24/25] Use pendulum to traverse mock reporting dates Previously, subtracting 29 days could result in the same month listed twice (for example, subtracting 29 days from Nov 30, would result in two Novembers listed). --- atst/domain/csp/reports.py | 5 ++--- tests/routes/portfolios/test_portfolios_index.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/atst/domain/csp/reports.py b/atst/domain/csp/reports.py index 514b05c9..704ffa46 100644 --- a/atst/domain/csp/reports.py +++ b/atst/domain/csp/reports.py @@ -1,4 +1,3 @@ -import datetime from itertools import groupby from collections import OrderedDict import pendulum @@ -33,10 +32,10 @@ class MockApplication: def generate_sample_dates(_max=8): - current = datetime.datetime.today() + current = pendulum.now() sample_dates = [] for _i in range(_max): - current = current - datetime.timedelta(days=29) + current = current.subtract(months=1) sample_dates.append(current.strftime("%m/%Y")) reversed(sample_dates) diff --git a/tests/routes/portfolios/test_portfolios_index.py b/tests/routes/portfolios/test_portfolios_index.py index 39849480..49787575 100644 --- a/tests/routes/portfolios/test_portfolios_index.py +++ b/tests/routes/portfolios/test_portfolios_index.py @@ -88,4 +88,4 @@ def test_portfolio_reports_with_mock_portfolio(client, user_session): ) assert response.status_code == 200 assert portfolio.name in response.data.decode() - assert "$237,617.00 Total spend to date" in response.data.decode() + assert "$251,626.00 Total spend to date" in response.data.decode() From 9594e0256235c94d9616c7f6828996989d4b376a Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 25 Feb 2019 15:36:12 -0500 Subject: [PATCH 25/25] Re-order migration so downgrade/upgrade work correctly --- ...cec2f32d4_remove_request_related_models.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/alembic/versions/c92cec2f32d4_remove_request_related_models.py b/alembic/versions/c92cec2f32d4_remove_request_related_models.py index 0f604966..6ca7e507 100644 --- a/alembic/versions/c92cec2f32d4_remove_request_related_models.py +++ b/alembic/versions/c92cec2f32d4_remove_request_related_models.py @@ -34,9 +34,18 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.add_column('portfolios', sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=True)) - op.create_foreign_key('workspaces_request_id_fkey', 'portfolios', 'requests', ['request_id'], ['id']) op.add_column('audit_events', sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=True)) - op.create_foreign_key('audit_events_request_id_fkey', 'audit_events', 'requests', ['request_id'], ['id']) + op.create_table('requests', + sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), + sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column('legacy_task_order_id', postgresql.UUID(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['legacy_task_order_id'], ['legacy_task_orders.id'], name='requests_legacy_task_order_fkey'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='requests_user_id_fkey'), + sa.PrimaryKeyConstraint('id', name='requests_pkey'), + postgresql_ignore_search_path=False + ) op.create_index('ix_audit_events_request_id', 'audit_events', ['request_id'], unique=False) op.create_table('request_revisions', sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), @@ -104,17 +113,6 @@ def downgrade(): sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='request_internal_comments_user_id_fkey'), sa.PrimaryKeyConstraint('id', name='request_internal_comments_pkey') ) - op.create_table('requests', - sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), - sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), - sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=False), - sa.Column('legacy_task_order_id', postgresql.UUID(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['legacy_task_order_id'], ['legacy_task_orders.id'], name='requests_legacy_task_order_fkey'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='requests_user_id_fkey'), - sa.PrimaryKeyConstraint('id', name='requests_pkey'), - postgresql_ignore_search_path=False - ) op.create_table('request_reviews', sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), @@ -147,4 +145,6 @@ def downgrade(): sa.ForeignKeyConstraint(['request_revision_id'], ['request_revisions.id'], name='request_status_events_request_revision_id_fkey'), sa.PrimaryKeyConstraint('id', name='request_status_events_pkey') ) + op.create_foreign_key('workspaces_request_id_fkey', 'portfolios', 'requests', ['request_id'], ['id']) + op.create_foreign_key('audit_events_request_id_fkey', 'audit_events', 'requests', ['request_id'], ['id']) # ### end Alembic commands ###