Merge pull request #508 from dod-ccpo/task-order-download

Task Order review and download
This commit is contained in:
dandds 2019-01-02 13:48:45 -05:00 committed by GitHub
commit 2c87738ac2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 313 additions and 7 deletions

View File

@ -57,9 +57,13 @@ class TaskOrder(Base, mixins.TimestampsMixin):
self.number, self.budget, self.end_date, self.id
)
@property
def portfolio_name(self):
return self.workspace.name
def to_dictionary(self):
return {
"portfolio_name": self.workspace.name,
"portfolio_name": self.portfolio_name,
**{
c.name: getattr(self, c.name)
for c in self.__table__.columns

View File

@ -3,3 +3,5 @@ from flask import Blueprint
task_orders_bp = Blueprint("task_orders", __name__)
from . import new
from . import index
from . import invite

View File

@ -0,0 +1,19 @@
from io import BytesIO
from flask import Response
from . import task_orders_bp
from atst.domain.task_orders import TaskOrders
from atst.utils.docx import Docx
@task_orders_bp.route("/task_orders/download_summary/<task_order_id>")
def download_summary(task_order_id):
task_order = TaskOrders.get(task_order_id)
byte_str = BytesIO()
Docx.render(byte_str, data=task_order.to_dictionary())
filename = "{}.docx".format(task_order.portfolio_name)
return Response(
byte_str,
headers={"Content-Disposition": "attachment; filename={}".format(filename)},
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)

View File

@ -0,0 +1,15 @@
from flask import redirect, url_for
from . import task_orders_bp
from atst.domain.task_orders import TaskOrders
from atst.utils.flash import formatted_flash as flash
# TODO: add a real implementation for this
@task_orders_bp.route("/task_orders/invite/<task_order_id>", methods=["POST"])
def invite(task_order_id):
task_order = TaskOrders.get(task_order_id)
flash("task_order_submitted", task_order=task_order)
return redirect(
url_for("workspaces.workspace_members", workspace_id=task_order.workspace.id)
)

View File

@ -113,6 +113,7 @@ def new(screen, task_order_id=None):
workflow.template,
current=screen,
task_order_id=task_order_id,
task_order=workflow.task_order,
screens=workflow.display_screens,
form=workflow.form,
)

57
atst/utils/docx.py Normal file
View File

@ -0,0 +1,57 @@
import os
from zipfile import ZipFile
from flask import render_template, current_app as app
class Docx:
DOCUMENT_FILE = "word/document.xml"
@classmethod
def _template_path(cls, docx_file):
return os.path.join(app.root_path, "..", "templates", docx_file)
@classmethod
def _template(cls, docx_file):
return ZipFile(Docx._template_path(docx_file), mode="r")
@classmethod
def _write(cls, docx_template, docx_file, document_content):
"""
This method takes an existing docx as its starting
point and copies over every file from it to a new zip
file, overwriting the document.xml file with new
document content.
zipfile.ZipFile does not provide a way to replace file
contents in a zip in-place, so we copy over the entire
zip archive instead.
docx_template: The source docx file we harvest from.
docx_file: A ZipFile instance that content from the docx_template is copied to
document_content: The new content for the document.xml file
"""
with docx_template as template:
for item in template.infolist():
if item.filename != Docx.DOCUMENT_FILE:
content = template.read(item.filename).decode()
else:
content = document_content
docx_file.writestr(item, content)
return docx_file
@classmethod
def render(
cls,
file_like,
doc_template="docx/document.xml",
file_template="docx/template.docx",
**args,
):
document = render_template(doc_template, **args)
with ZipFile(file_like, mode="w") as docx_file:
docx_template = Docx._template(file_template)
Docx._write(docx_template, docx_file, document)
file_like.seek(0)
return file_like

View File

@ -101,6 +101,13 @@ MESSAGES = {
"message_template": "",
"category": "success",
},
"task_order_submitted": {
"title_template": "Task Order Form Submitted",
"message_template": """
Your task order form for {{ task_order.portfolio_name }} has been submitted.
""",
"category": "success",
},
}

View File

@ -0,0 +1,8 @@
{% from "components/icon.html" import Icon %}
{% macro EditLink(url) -%}
<a class='icon-link' href='{{ url }}'>
{{ Icon('edit') }}
<span>edit</span>
</a>
{% endmacro %}

View File

@ -0,0 +1,3 @@
{% macro RequiredLabel() -%}
<span class='label label--error'>Response Required</span>
{%- endmacro %}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:ve="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml">
<w:body>
{% for key,val in data.items() %}
<w:p>
<w:r>
<w:t>{{ key }}: {{ val }}</w:t>
</w:r>
</w:p>
{% endfor %}
</w:body>
</w:document>

Binary file not shown.

View File

@ -1,8 +1,8 @@
{% extends 'task_orders/_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/edit_link.html" import EditLink %}
{% from "components/required_label.html" import RequiredLabel %}
{% from "components/icon.html" import Icon %}
{% block heading %}
Review & Download
@ -12,6 +12,143 @@
{% include "fragments/flash.html" %}
<a href="#">Download your Task Order Packet.</a>
{% macro TOEditLink(screen=1) %}
{% if task_order %}
{{ EditLink(url_for("task_orders.new", screen=screen, task_order_id=task_order.id)) }}
{% else %}
{{ EditLink(url_for("task_orders.new", screen=screen)) }}
{% endif %}
{% endmacro %}
<section>
<h3>Scope (Statement of Work) {{ TOEditLink() }}</h3>
<p>
{{ task_order.scope or RequiredLabel() }}
</p>
<div class="row">
<div class="col col--grow">
<h3>Period of Performance length {{ TOEditLink(screen=2) }}</h3>
{{ task_order.period or RequiredLabel() }}
</div>
<div class="col col--grow">
<h3>Total funding requested {{ TOEditLink(screen=2) }}</h3>
{{ task_order.budget }}
</div>
</div>
</section>
<hr>
<section>
<h2>Generated Documents</h2>
<ul class="usa-unstyled-list">
<li>
<a href="#" download>
{{ Icon('download') }}
Cover Sheet
</a>
</li>
<li>
<a href="#" download>
{{ Icon('download') }}
Market Research
</a>
</li
>
{% if task_order %}
<li>
<a href="{{ url_for('task_orders.download_summary', task_order_id=task_order.id) }}" download>
{{ Icon('download') }}
Task Order Draft
</a>
</li>
{% endif %}
<li>
<a href="#" download>
{{ Icon('download') }}
DD 254
</a>
</li>
</ul>
</section>
<hr>
<section>
<h2>Invite Signatories/Collaborators</h2>
<div class="form-row">
<div class="form-col">
<div class="usa-input">
<fieldset class="usa-input__choices">
<legend>Financial Oversight</legend>
<p>
{% if task_order.ko_first_name %}
{{ task_order.ko_first_name }}
{{ task_order.ko_last_name }}
{% else %}
{{ RequiredLabel() }}
{% endif %}
(Contracting Officer)
</p>
<p>
{% if task_order.ko_first_name %}
{{ task_order.cor_first_name }}
{{ task_order.cor_last_name }}
{% else %}
{{ RequiredLabel() }}
{% endif %}
(Contracting Officer Representative)
</p>
</fieldset>
</div>
</div>
<div class="form-col">
<div class="usa-input">
<fieldset class="usa-input__choices">
<legend>Invite?</legend>
</fieldset>
</div>
</div>
</div>
<div class="form-row">
<div class="form-col">
<div class="usa-input">
<fieldset class="usa-input__choices">
<legend>Security Officer</legend>
<p>
{% if task_order.so_first_name %}
{{ task_order.so_first_name }}
{{ task_order.so_last_name }}
{% else %}
{{ RequiredLabel() }}
{% endif %}
(Security Officer)
</p>
</fieldset>
</div>
</div>
<div class="form-col">
<div class="usa-input">
</div>
</div>
</div>
</section>
{% endblock %}
{% block next %}
<div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='Send Invitations' />
</div>
{% endblock %}
{% block form_action %}
<form method='POST' action="{{ url_for('task_orders.invite', task_order_id=task_order_id) }}" autocomplete="off">
{% endblock %}

View File

@ -377,8 +377,8 @@ class TaskOrderFactory(Base):
defense_component = factory.LazyFunction(random_service_branch)
app_migration = random_choice(data.APP_MIGRATION)
native_apps = random.choices(["yes", "no", "not_sure"])
complexity = random_choice(data.PROJECT_COMPLEXITY)
dev_team = random_choice(data.DEV_TEAM)
complexity = [random_choice(data.PROJECT_COMPLEXITY)]
dev_team = [random_choice(data.DEV_TEAM)]
team_experience = random_choice(data.TEAM_EXPERIENCE)
scope = factory.Faker("sentence")

View File

@ -0,0 +1,28 @@
from flask import url_for
from io import BytesIO
import re
from zipfile import ZipFile
from atst.utils.docx import Docx
from tests.factories import TaskOrderFactory
def xml_translated(val):
return re.sub("'", "&#39;", str(val))
def test_download_summary(client, user_session):
user_session()
task_order = TaskOrderFactory.create()
response = client.get(
url_for("task_orders.download_summary", task_order_id=task_order.id)
)
bytes_str = BytesIO(response.data)
zip_ = ZipFile(bytes_str, mode="r")
doc = zip_.read(Docx.DOCUMENT_FILE).decode()
for attr, val in task_order.to_dictionary().items():
assert attr in doc
if not xml_translated(val) in doc:
__import__("ipdb").set_trace()
assert xml_translated(val) in doc

13
tests/utils/test_docx.py Normal file
View File

@ -0,0 +1,13 @@
from io import BytesIO
from zipfile import ZipFile
from atst.utils.docx import Docx
def test_render_docx():
data = {"droid_class": "R2"}
byte_str = BytesIO()
docx_file = Docx.render(byte_str, data=data)
zip_ = ZipFile(docx_file, mode="r")
document = zip_.read(Docx.DOCUMENT_FILE)
assert b"droid_class: R2" in document