From 59510819e78466c12f9167ec69a6f3ce4fd180e5 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 20 Dec 2018 10:04:29 -0500 Subject: [PATCH 1/8] docx utility --- atst/utils/docx.py | 45 +++++++++++++++++++++++++++++++++++ templates/docx/document.xml | 12 ++++++++++ templates/docx/template.docx | Bin 0 -> 9926 bytes tests/utils/test_docx.py | 12 ++++++++++ 4 files changed, 69 insertions(+) create mode 100644 atst/utils/docx.py create mode 100644 templates/docx/document.xml create mode 100644 templates/docx/template.docx create mode 100644 tests/utils/test_docx.py diff --git a/atst/utils/docx.py b/atst/utils/docx.py new file mode 100644 index 00000000..86fa04a0 --- /dev/null +++ b/atst/utils/docx.py @@ -0,0 +1,45 @@ +import os +from io import BytesIO +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): + 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 + + docx_file.writestr(item, content) + + return docx_file + + @classmethod + def render( + cls, + doc_template="docx/document.xml", + file_template="docx/template.docx", + **args, + ): + document = render_template(doc_template, **args) + byte_str = BytesIO() + docx_file = ZipFile(byte_str, mode="w") + docx_template = Docx._template(file_template) + Docx._write(docx_template, docx_file, document) + docx_file.close() + byte_str.seek(0) + return byte_str.read() diff --git a/templates/docx/document.xml b/templates/docx/document.xml new file mode 100644 index 00000000..810064ad --- /dev/null +++ b/templates/docx/document.xml @@ -0,0 +1,12 @@ + + + + {% for key,val in data.items() %} + + + {{ key }}: {{ val }} + + + {% endfor %} + + diff --git a/templates/docx/template.docx b/templates/docx/template.docx new file mode 100644 index 0000000000000000000000000000000000000000..7d4e30425ebc49470fb19ce92e89c4afb7202a3a GIT binary patch literal 9926 zcmeHNWmH_rwrUH|tr_Qc#*On?tQ3yy(05kv=0058z79V0b^uYiC5-0!w0{{!I^3lTFR?pm4 zL)OVs&qnQ)qnRm5CL}m@IshE>|Nmb9!5XNN9WZZaLTyyo1@PNQ@@cyQ3^{e3Rc6pq>GSY(RPnI3|wY0O-{BD_!3 zZKT4dokpwCJJO7#{QJqBG1Dxh5T0FwYz3FL;|g~lmtq9`i&m+ev+wZP)12#ky%Nk2 zZu9$#R=Y%A9fT%`?1bEWl-SUf+ib2&w1JxF1H`p3CMA)9s3Ec7!ZaFhF1;z2nK373 zlZ9=F#*q3nA4oo`afz4he<3%?lm~_ibSh{GLu=fA{dQ)pW#pGhu;n4HA*cLdUL(tQ zhccT#RYj%>i>_*ZpSOT~wfp^@JC@4AE9Wt;Fdho=FI-74_J-&rxs=dIGX3oBdIVwk zD-O^*j)4u`7SbNk4l6^I?lmw-gTnnmZh?XE_?$_{3TMhYQDpK=t}XHR%dHJQRku+* zPi-NOc2W*6gorGTYFBYU%zA!?07(ADJc%RlYtKPX#6i}F05VSvYdupN##cYu|C;50 zur2=b=^>HxA`pCte#cPDTD`6u^YCP2vr9r54rKcfJ_!e44#>RT7v z0}@k7-boX0^SGf)wUj2)XOc7pmrWeaYJ*`Um3mbYy$p~9hs%u2Ev6W`Bce07LzoLQ z*DC$^D0s;9S<13heS4c_>w$#Aa5CW$g9Mi$h4sxn7YY!G3AuAhNJa+7@2h(h*)^t+ z)j06o;PsDZ1_H5qVYDn}Uun~t3U^zF8q-|T0 zi+*2Hd=Cg}ddjGvs7k7FyNCZM?GJsu7OeTna4V`R>CnNVlL~q-Sq?}^16!3-_-3Bk zk1fVy_M?ec7l~Pj zp%?WJqe%U7i9CHwLmm%gG6!@}S{jSq)X*=Io=5EM7GrMK`gOu%OJ#c*xf@As8VS1; ziSTuyFjxYsNJ_nnVj(klF{uoYNmVdCVK8{l9}rph>h5|d_qoDgpK8LT!%WzFPRb}g z9@697X}%;h*oP!#gnsw@kKC~^7-Q}QJ-GpK2Mt7tpWOMCHOVsbVdG4wu7Y#!0S`+r z7iBZ}#T1m~PJF7gS0Pk;gy~Jg^(657r&)$`FoE(|j8^*H+sSoK&lep#8}zP{nshZv z2J{i~?~6@U$}Kc_OYG_4dd-H|6qU+@5f64xPRx0u*#ee?g5jiQBvWbTNb6sVdGBPe zY9Wa$gNy@7@U?3a{!z=dIb|zn*j{#3yXalK9#oQ1x^`3T=I8R)B95@5nm*B^tRlU) zil|S-i*QcKGQ?OijBJ|#cH4em-=POaf|wZwitXAUPJ*Tdf`xa+)!Pt59Xm9KU@nK; zuz283;W8)v-};uuQVDQrN&>z}-^keXbz16`vRb`;N_@6+G{IMYW0V)M=Ri6U3~@LH zwWSAw~953r7UEq|;T zGk+}=PwbhTd)C=ErbMHqWrEU?U5e_F>@z{dJQV1+>-W#ZFY*hgk9rV^&`D_|=F}gp zhdxrAI=zo7J6t5bVk~wNqD_X3KlQ0D+3doi2=F;M)cbV;2gPnbiX)(w@5VkZ29}+h{&R?T~KcpD#>EtF5xKgDW)%j#no2@tHvn(@p46>AED!wKtrc;4(h0ru6p5DK#Z*t}v0hAqk=gP;v9{S{7ywqnD z`~J!*)xLJBh)vZc@Tos}uvk2A+*GeZF%w$%1%b^?^_;Pe&1@S1u#02+By?E6wqb7t zsdr6b`{a?Xc+xMJBR$~zh9+_`(cH1Y1VV3o<)O$8e(SU`o{OKCA$6u`CGI&tLr^vD z@_Hr;07+NzJ7m6*NcW6C_0gIm1@#o-$(yzuytf+zb)-WG!WyHp)lDcehVCfKplW$| zaAHFpGk7^1%$W9YbVO?T z3mT$`=4?w8g?da4BD@%)(NAz7|4-i`%DRPfZCpOE<)R~!>r_ygvc&4ULJ z{_se8Z^ka}&P$X>e&>|{(Puib*V*J$iTc4cMu=_+^1Fyn_NJ9zVC*;x$DD%+>xFvZ z2Wfp?pl`*WzDcDfREQv4i;qEagDCte(%U9`qo*oCdtxhuE7;^Aq*T2X3vd+`Uv85u zr(J9w$Sn`X3?i2O-ss82NM3ux$I))3>`OFCL94?Hgh9>3J+zX8THZAG+i^I75XY{+ zN2UyGkSK+&HQEVaML>PWf;JZ?S&#xxIKFY*&zvP0Zic9;%H;=sv$o%Z6>(XEy(Tsj zRq$Htp8rv2FdT#_Mb`#*r9(h3~pO1`T z*?#Iv^!^ZAYN>uo!h=CeO0!V^muj;*S~4$iP2T|(56@bWOLtZuR3(AT4Pu7KXyrg^hHuSiY4(}I7{&nv8Q~XA^GOP<<))9>-3ReFb0Lv z56!Qa78tq_aCXV?Ry}li#+*m)znUjU_XO3!dCcUWMDT2+Xjh}x#=g9VUKn1>e%;Uw zZeMr3)G6u&IntPYe#QT$B0D*mo=?!MbW+vaxwpq?o}eQEn|K|w@s-_(zY;9Y$7Xc) z0|_zxs7)4&8tbkFimx4PzSZ~1?czj2zOpKcpV(OkWwF8Ozb6+x42Uu93(#niKeA;j zDtT+(OlJe$)|XJOGoQ-M&gP(*y}eJ2k>-)&N6cJ-4V~dX(6MYmmK&u?M=yE#SzwgW zGCa4~EO%ionNsccs($hvU~{-MNca{(I)||P znMLlx4V(jt&hyFxW%?U9c}l5SI0BZ{*AGVgkIf$4Z4<}=*x!PEjbLfNeP(}#;+l9i z8&<-9S-jescV!d+>H8=o;01g~E&B3KoK$|~>Xm4up3i6gn9i7dtS|h?a0Gk!-wLkn zQ3O!;auap+T35uo)9zFCMyV&n#JQG}@ud%%*kPV_hkIVKP;X9YjgM|zG4dD9$Q?RY zaxZ@w2-*o$N}BAg1dorY@sf|dDy3=Xs#R7g-(poa-)i^7*=F-=%On+9ZHC$9Eqj() zWx3b;VDa^1GfFj9n?JM|Vmox^U8ET|@Df@!TF8wS+@sp)LRD$WWW6YCA!>Arr`%{crqYqi1VtWNu*d!-*x! z&YLHL;+N71_xIcmI9HKoJ|D=;GP8>#YiM_25W79FmUWXT=Sz=qErsT2gYJA>hX+qp zxf&cXx!*|YBt$wjD(719Q%Ye)i4i|Hwx}m}N7#|jt%!_q^GFkDB)7ObOc}(!@z-bn z0B6J!(~}aBK*@lmiQ|QqRaCI(MIDh}ukRzC=6J^BwD2ack|H5FRzxPyxcW7CvKs9; zMb>r)^+?0u|fL)Oy>d+d9aEz){lt$+i5~S!L$N+OQ?j$yujAJ(y*e+Hpq_Q zb$P4hG}Aid`>@BK;6fq?Lvt9ef0mzu*+Suswr)cb)QmD`pa0%bX`vk=oZ9huIU&V-Qq=xS`FAE)UqAR`hm9_Frk~<>7JyY?NQNp;{dtw21a#yOFBb|2ue)VLC5Z*+m6NTyRq##&zp3#yf)K%~L}Zp4xEcNK8nF4{8R1rO zeN&vWl%6^A5~#38h*wIS&W-ZPg{kss;PE}~d^%{R2#MMnkMb)X`KEwew({?!S>fMo zshq)R~|TYCG;H(3(gP{_t~5xePws+O`^ z+NOFxIA0a1Vd2Gu*|i*Z@D|uCLHk667HV^Dg%W%ZP@o_4smZSGf4FLpoWM26M4^L& z+ph8-2xm!n{7N>15InZyWA`>0>O-DB`6av7a^dDcJk-v6i*zPsiUShhQ0|CC6zy<` zJ?^MeXt|i;kt&`)X>OgQ2aycCn7V!uxL~+FK_Y!rz9Yx3INV`-z`2&809e>kXdQh- zwbA4|qnPrfu|!#tv9f{_C-`jrar^r8AU5hmXE0w?m04U$L+?z{CE_5T(*VX>kE6@U z?WVio_BW(MZJCyNp$h|e)JlY5wDRcR336}EKU*wp`D|_j_<<`=hShE7c?2skMi$&#LJCUblc%7a7{%I0 zGG-EDwuGgR{H9U!sEtvuSe}CCr#qi@U2Y!@$R@h|*3HNkH@*3>)4$?dW)ZM~No2k} zQ{*6~I(6XSJbsqon2TJw0M)PlK35_$L6HrhIf4Yu7v>*x<)Ei6^UDeqGTa7AMG?DC zzi1t!+G?a+GKmODQ)e-bUV^C#8X=bJQkX4WU9g$PEjnYS-JBkyXc(FCNU^>~I;k+r z;DhHDs7JDN;(d3VA$87r8v_S(^U1g4rfOGI;l%k<b1!c zd+e1I7MIaU>rP={%bYG06gCYtu6AC9waV$DgYfRkPW*KAGT6vpl|PQ%?Nh#h>z?bK z)ehoy)aHW65wOxZeyR?4_{X&vcmg2t1byfNXgx*+)geG>q@=Zlr46H&rR5J=f(my3 ztv!G=mn_|8KF@^Mgfhd6+st7dq=gIrsSBt*lQ9q9X*A-+Ki{q^44lb)x|gzAtKTtu z6A?Z!zMi#mWoJ8ykZbC_JUK5Wqp)MgGzbh37xW=>;iIe(=74TBQ6j&2<)8@^{f6zf)Yb)Jz*ak;JLt0JxonMbb%BxHk*h?A|I; zm+%ay$(fgOAf-ix;)GmRJ&@g^{F99i@6y*>foQu7qAl95wAHb&{<)m~m$x9&{@`t- zm;`8{@;erK@Ta|I--U*dGou2V3ltVY^!(=AIp01)eNK*u){FNoX=9QfIalmpLMg`xcmrlKHao^K>D1D`%|ds=~J98LyWJ!DKX89?9| zSp_$aH}@tFBZOi8gRt2K))>xofAqxdsA*ZQsUU|3mCv9hT$uG9wYf(IalltWYj%>$ zB}@hn7DT9Ml9Vnh%=OACPnyFtB(u#AO7IFMEi$}Elu4B(oL|G`5!(w2GL7fLR=2}X zd(LjF)z~mAPJ_VuWroqIRKc8rMg-j}?n^B+OGHsT8JQp^Xrj*dj%p6MDDev&Bo+EC z_vb%F9ItC8pv~RF9HeLYDBw)g=eU2b20Te#KP=yF;X}ZDfAHZpKgRpeB4(n_L2=SL zA7Q(dLVG;klQ#*ziGdykT<|&76*!9Ar6=8}R5F-Yo*Ad?cPn$pknk8zIE@>02PymS zzQl%blpGfZRI1|w06;|kSE*j`zW5x22MHVMn0H}uKLp430xGmKp5I@_|QZE)+&waSs%)6y3$Y8Nf` zb1!YaUK7EysLije)gwb##=q9aYuYBLk*KbSyVX-y#a1}k&Kj(OH6Nei8G9MQN{TjiQ$6%^v?9Qp>D zdB>}Raiz&9J;THjg&HHXgI6D8c~w%Bbue)CZ);yKEM~PO(OK&Ckg-^GBhDL|gBz}C zh32+HJJ-j@;qYGN8)<%FK!C+%Dl1U3QA)DHP{7!X89=v*vuN(!p$b=+TTt|CeH}WD z6ITbU8@(PlxilGI4W(cV z;a+4Xdj2T| z+?HE%FGw0gwIX?O!!!=Erfh2Q2z_AEt=Ou>E15@)Ug1sLtk`i#{GMstQ54&4hkYCq z7|9`{Xj@Hd>xF-i@c&nMc}Wj<=T^lO2iby8sz9)<;?N1u}r<&PM$ zrt3Vl9?8tniHgvv>zc}(2C{2Md@7#>sN=ndyI3voMAEEe5w{tfX3Zby!n@p5#F5s_ zbdN5QQMd5HRS!}#aoG8XCMVy*695KLRqN7Gzz)bC9J+kXm%DM-Tj&HNs=`l}u}Y}s z{2zA07?q5cDjy6yl+_W==HA8S4|3dBsX{cJm64QchPNVlHU>1gMhnsR(%nS3K<7#6 zHrHvLMH2?Y5K&6@u3BO$n%J@ zaOqk(K!9;y_>}{(2F59HwJV~PY@ni48)$E+rw)vn^){r`%&re~d^T2UI;r_sROLmm@j#27s-LfR9u# zPYeWc^ofLHi;a5&X+n7V$)g+10@Cm=9lu})US00fPi4JxD{R%YOR%U)#N1zh+_^t| z_w!yuDtrnL=J5J}EGg57hG}hU?6s#MjYe{t)|z5=fX~f`90c8w|2A06iC^ zX6DSFx_-6v2t4g1Xd^T6o}MTGym^ztnA)&$W7|vYGOT>qVZxz<*hDB86m0XOeFCPX{zuoORL#IYBdEcKIK$Cuq@}E*nU?QnQRc7 zhzhRci3^3N0fH&i4JaM%uoOT3adQ2GC3hb&;cK_O`xLH z{V}@a9b4YQvA0jF8P77Jn7<>OqE4Z^8scqKtJ@|%Tw)c|M^<^5vJZFkA1U*1w7CLz z?CfgTP3NkCdUffvbFI=g?wnHZZMfATp#M=T?6K{6O0{?W?bcx8{WiB)%WKY>+|825 z@6i@j4PRU!?JD$*8LDIFub#L}$zddr@t9;(?Yh1#Cs#}CT;Nya+UvbqF~?*pycbH5B&UyYks}{;rNfF=wAi= z^+>|6;55)C+Ajwbeh2<_pY~T^2Po11zjkeZm-PFt-Ji0aL8qGjwukpS{P*3TKjByK z|Azm*7xcS`-|KFFif93q;eL+b*DBoa@ZW0~f5Iz3Y4T6_Un&~EiRqSuQ|i7XmI>r(0^nQzr%kI<^B{!_{@#}v82Zcvl literal 0 HcmV?d00001 diff --git a/tests/utils/test_docx.py b/tests/utils/test_docx.py new file mode 100644 index 00000000..6d3064e0 --- /dev/null +++ b/tests/utils/test_docx.py @@ -0,0 +1,12 @@ +from io import BytesIO +from zipfile import ZipFile + +from atst.utils.docx import Docx + + +def test_render_docx(): + data = {"droid_class": "R2"} + docx_file = Docx.render(data=data) + zip_ = ZipFile(BytesIO(docx_file), mode="r") + document = zip_.read(Docx.DOCUMENT_FILE) + assert b"droid_class: R2" in document From 718f88d8289358a6571c70f6a4351e58a6f93a6a Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 20 Dec 2018 10:41:54 -0500 Subject: [PATCH 2/8] download link for task order summary --- atst/models/task_order.py | 6 +++++- atst/routes/task_orders/__init__.py | 1 + atst/routes/task_orders/index.py | 17 ++++++++++++++++ atst/utils/docx.py | 2 +- tests/factories.py | 4 ++-- tests/routes/task_orders/test_index.py | 28 ++++++++++++++++++++++++++ tests/utils/test_docx.py | 2 +- 7 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 atst/routes/task_orders/index.py create mode 100644 tests/routes/task_orders/test_index.py diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 53afe102..85e4f4ba 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -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 diff --git a/atst/routes/task_orders/__init__.py b/atst/routes/task_orders/__init__.py index 2de14f43..7d798729 100644 --- a/atst/routes/task_orders/__init__.py +++ b/atst/routes/task_orders/__init__.py @@ -3,3 +3,4 @@ from flask import Blueprint task_orders_bp = Blueprint("task_orders", __name__) from . import new +from . import index diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py new file mode 100644 index 00000000..1146a9ab --- /dev/null +++ b/atst/routes/task_orders/index.py @@ -0,0 +1,17 @@ +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/") +def download_summary(task_order_id): + task_order = TaskOrders.get(task_order_id) + byte_str = Docx.render(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", + ) diff --git a/atst/utils/docx.py b/atst/utils/docx.py index 86fa04a0..df543948 100644 --- a/atst/utils/docx.py +++ b/atst/utils/docx.py @@ -42,4 +42,4 @@ class Docx: Docx._write(docx_template, docx_file, document) docx_file.close() byte_str.seek(0) - return byte_str.read() + return byte_str diff --git a/tests/factories.py b/tests/factories.py index d2b9e82a..fb2779dd 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -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") diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py new file mode 100644 index 00000000..53b8e9be --- /dev/null +++ b/tests/routes/task_orders/test_index.py @@ -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("'", "'", 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 diff --git a/tests/utils/test_docx.py b/tests/utils/test_docx.py index 6d3064e0..e35e2fc4 100644 --- a/tests/utils/test_docx.py +++ b/tests/utils/test_docx.py @@ -7,6 +7,6 @@ from atst.utils.docx import Docx def test_render_docx(): data = {"droid_class": "R2"} docx_file = Docx.render(data=data) - zip_ = ZipFile(BytesIO(docx_file), mode="r") + zip_ = ZipFile(docx_file, mode="r") document = zip_.read(Docx.DOCUMENT_FILE) assert b"droid_class: R2" in document From c516f0f74c0a2f3233f22de087ffedc99525a660 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 20 Dec 2018 13:12:30 -0500 Subject: [PATCH 3/8] review page --- atst/routes/task_orders/new.py | 1 + templates/components/edit_link.html | 8 ++ templates/components/required_label.html | 3 + templates/task_orders/new/review.html | 128 ++++++++++++++++++++++- 4 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 templates/components/edit_link.html create mode 100644 templates/components/required_label.html diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 507f2471..9cf07f05 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -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, ) diff --git a/templates/components/edit_link.html b/templates/components/edit_link.html new file mode 100644 index 00000000..01f4fc0d --- /dev/null +++ b/templates/components/edit_link.html @@ -0,0 +1,8 @@ +{% from "components/icon.html" import Icon %} + +{% macro EditLink(url) -%} + + {{ Icon('edit') }} + edit + +{% endmacro %} diff --git a/templates/components/required_label.html b/templates/components/required_label.html new file mode 100644 index 00000000..926a136f --- /dev/null +++ b/templates/components/required_label.html @@ -0,0 +1,3 @@ +{% macro RequiredLabel() -%} + Response Required +{%- endmacro %} diff --git a/templates/task_orders/new/review.html b/templates/task_orders/new/review.html index 6296fe91..1e27de15 100644 --- a/templates/task_orders/new/review.html +++ b/templates/task_orders/new/review.html @@ -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,126 @@ {% include "fragments/flash.html" %} -Download your Task Order Packet. +{% if task_order %} + {% set edit_link = EditLink(url_for("task_orders.new", screen=1, task_order_id=task_order.id)) %} +{% else %} + {% set edit_link = EditLink(url_for("task_orders.new", screen=1)) %} +{% endif %} + +

Scope (Statement of Work) {{ edit_link }}

+

+ {{ task_order.scope or RequiredLabel() }} +

+ +
+
+

Period of Performance length {{ edit_link }}

+

+ {{ task_order.scope or RequiredLabel() }} +

+
+ +
+

Total funding requested {{ edit_link }}

+

+ {{ task_order.budget }} +

+
+
+ +
+ +

Generated Documents

+ +

+ + {{ Icon('download') }} + Cover Sheet + +

+ +

+ + {{ Icon('download') }} + Market Research + +

+ +{% if task_order %} +

+ + {{ Icon('download') }} + Task Order Draft + +

+{% endif %} + +

+ + {{ Icon('download') }} + DD 254 + +

+ +
+ +

Invite Signatories/Collaborators

+ +
+
+
+
+ Financial Oversight +

+ {% if task_order.ko_first_name %} + {{ task_order.ko_first_name }} + {{ task_order.ko_last_name }} + {% else %} + {{ RequiredLabel() }} + {% endif %} + (Contracting Officer) +

+

+ {% if task_order.ko_first_name %} + {{ task_order.cor_first_name }} + {{ task_order.cor_last_name }} + {% else %} + {{ RequiredLabel() }} + {% endif %} + (Contracting Officer Representative) +

+
+
+
+
+
+
+ Invite? +
+
+
+
+
+
+
+
+ Security Officer +

+ {% if task_order.so_first_name %} + {{ task_order.so_first_name }} + {{ task_order.so_last_name }} + {% else %} + {{ RequiredLabel() }} + {% endif %} + (Security Officer) +

+
+
+
+
+
+
+
+
{% endblock %} From c024d12e3b427787e76cdab373f955c7ce602b03 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 20 Dec 2018 14:30:15 -0500 Subject: [PATCH 4/8] touch up review page and add route for submitting task order invites --- atst/routes/task_orders/__init__.py | 1 + atst/routes/task_orders/invite.py | 15 ++ atst/utils/flash.py | 7 + templates/task_orders/new/review.html | 206 ++++++++++++++------------ 4 files changed, 133 insertions(+), 96 deletions(-) create mode 100644 atst/routes/task_orders/invite.py diff --git a/atst/routes/task_orders/__init__.py b/atst/routes/task_orders/__init__.py index 7d798729..15395177 100644 --- a/atst/routes/task_orders/__init__.py +++ b/atst/routes/task_orders/__init__.py @@ -4,3 +4,4 @@ task_orders_bp = Blueprint("task_orders", __name__) from . import new from . import index +from . import invite diff --git a/atst/routes/task_orders/invite.py b/atst/routes/task_orders/invite.py new file mode 100644 index 00000000..6fab5b04 --- /dev/null +++ b/atst/routes/task_orders/invite.py @@ -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/", methods=["POST"]) +def invite(task_order_id): + task_order = TaskOrders.get(task_order_id) + flash("task_order_complete", task_order=task_order) + return redirect( + url_for("workspaces.workspace_members", workspace_id=task_order.workspace.id) + ) diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 7e9a879c..70bd0fbd 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -101,6 +101,13 @@ MESSAGES = { "message_template": "", "category": "success", }, + "task_order_complete": { + "title_template": "Task Order Complete", + "message_template": """ + Invitations for {{ task_order.portfolio_name }} have been sent. + """, + "category": "success", + }, } diff --git a/templates/task_orders/new/review.html b/templates/task_orders/new/review.html index 1e27de15..49dc2227 100644 --- a/templates/task_orders/new/review.html +++ b/templates/task_orders/new/review.html @@ -18,120 +18,134 @@ {% set edit_link = EditLink(url_for("task_orders.new", screen=1)) %} {% endif %} -

Scope (Statement of Work) {{ edit_link }}

-

+

+

Scope (Statement of Work) {{ edit_link }}

+

{{ task_order.scope or RequiredLabel() }} -

+

-
-
-

Period of Performance length {{ edit_link }}

-

- {{ task_order.scope or RequiredLabel() }} -

-
+
+
+

Period of Performance length {{ edit_link }}

+ {{ task_order.period or RequiredLabel() }} +
-
-

Total funding requested {{ edit_link }}

-

- {{ task_order.budget }} -

+
+

Total funding requested {{ edit_link }}

+ {{ task_order.budget }} +
-
+

-

Generated Documents

+
+

Generated Documents

-

- - {{ Icon('download') }} - Cover Sheet - -

+ +

-

Invite Signatories/Collaborators

+
+

Invite Signatories/Collaborators

-
-
-
-
- Financial Oversight -

- {% if task_order.ko_first_name %} - {{ task_order.ko_first_name }} - {{ task_order.ko_last_name }} - {% else %} - {{ RequiredLabel() }} - {% endif %} - (Contracting Officer) -

-

- {% if task_order.ko_first_name %} - {{ task_order.cor_first_name }} - {{ task_order.cor_last_name }} - {% else %} - {{ RequiredLabel() }} - {% endif %} - (Contracting Officer Representative) -

-
+
+
+
+
+ Financial Oversight +

+ {% if task_order.ko_first_name %} + {{ task_order.ko_first_name }} + {{ task_order.ko_last_name }} + {% else %} + {{ RequiredLabel() }} + {% endif %} + (Contracting Officer) +

+

+ {% if task_order.ko_first_name %} + {{ task_order.cor_first_name }} + {{ task_order.cor_last_name }} + {% else %} + {{ RequiredLabel() }} + {% endif %} + (Contracting Officer Representative) +

+
+
+
+
+
+
+ Invite? +
+
-
-
-
- Invite? -
+
+
+
+
+ Security Officer +

+ {% if task_order.so_first_name %} + {{ task_order.so_first_name }} + {{ task_order.so_last_name }} + {% else %} + {{ RequiredLabel() }} + {% endif %} + (Security Officer) +

+
+
+
+
+
+
-
-
-
-
-
- Security Officer -

- {% if task_order.so_first_name %} - {{ task_order.so_first_name }} - {{ task_order.so_last_name }} - {% else %} - {{ RequiredLabel() }} - {% endif %} - (Security Officer) -

-
-
-
-
-
-
-
-
+
{% endblock %} + +{% block next %} +
+ +
+{% endblock %} + +{% block form_action %} +
+{% endblock %} From 5cf2534445dee8a22339cd90936b394f3d5c3fe1 Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 21 Dec 2018 13:46:24 -0500 Subject: [PATCH 5/8] fix edit links and formatting in TO review template --- templates/task_orders/new/review.html | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/templates/task_orders/new/review.html b/templates/task_orders/new/review.html index 49dc2227..34a8d28c 100644 --- a/templates/task_orders/new/review.html +++ b/templates/task_orders/new/review.html @@ -12,26 +12,28 @@ {% include "fragments/flash.html" %} +{% macro TOEditLink(screen=1) %} {% if task_order %} - {% set edit_link = EditLink(url_for("task_orders.new", screen=1, task_order_id=task_order.id)) %} + {{ EditLink(url_for("task_orders.new", screen=screen, task_order_id=task_order.id)) }} {% else %} - {% set edit_link = EditLink(url_for("task_orders.new", screen=1)) %} + {{ EditLink(url_for("task_orders.new", screen=screen)) }} {% endif %} +{% endmacro %}
-

Scope (Statement of Work) {{ edit_link }}

+

Scope (Statement of Work) {{ TOEditLink() }}

{{ task_order.scope or RequiredLabel() }}

-

Period of Performance length {{ edit_link }}

+

Period of Performance length {{ TOEditLink(screen=2) }}

{{ task_order.period or RequiredLabel() }}
-

Total funding requested {{ edit_link }}

+

Total funding requested {{ TOEditLink(screen=2) }}

{{ task_order.budget }}
@@ -55,7 +57,8 @@ {{ Icon('download') }} Market Research - + {% if task_order %}
  • @@ -87,19 +90,19 @@ Financial Oversight

    {% if task_order.ko_first_name %} - {{ task_order.ko_first_name }} - {{ task_order.ko_last_name }} + {{ task_order.ko_first_name }} + {{ task_order.ko_last_name }} {% else %} - {{ RequiredLabel() }} + {{ RequiredLabel() }} {% endif %} (Contracting Officer)

    {% if task_order.ko_first_name %} - {{ task_order.cor_first_name }} - {{ task_order.cor_last_name }} + {{ task_order.cor_first_name }} + {{ task_order.cor_last_name }} {% else %} - {{ RequiredLabel() }} + {{ RequiredLabel() }} {% endif %} (Contracting Officer Representative)

    @@ -121,10 +124,10 @@ Security Officer

    {% if task_order.so_first_name %} - {{ task_order.so_first_name }} - {{ task_order.so_last_name }} + {{ task_order.so_first_name }} + {{ task_order.so_last_name }} {% else %} - {{ RequiredLabel() }} + {{ RequiredLabel() }} {% endif %} (Security Officer)

    From 6527f72e78c2585591edafd7367cad55a57657a3 Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 21 Dec 2018 14:05:45 -0500 Subject: [PATCH 6/8] pass file-like object to Docx.render method --- atst/routes/task_orders/index.py | 4 +++- atst/utils/docx.py | 14 ++++++-------- tests/utils/test_docx.py | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 1146a9ab..97364476 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -1,3 +1,4 @@ +from io import BytesIO from flask import Response from . import task_orders_bp @@ -8,7 +9,8 @@ from atst.utils.docx import Docx @task_orders_bp.route("/task_orders/download_summary/") def download_summary(task_order_id): task_order = TaskOrders.get(task_order_id) - byte_str = Docx.render(data=task_order.to_dictionary()) + byte_str = BytesIO() + Docx.render(byte_str, data=task_order.to_dictionary()) filename = "{}.docx".format(task_order.portfolio_name) return Response( byte_str, diff --git a/atst/utils/docx.py b/atst/utils/docx.py index df543948..bd4b1ddd 100644 --- a/atst/utils/docx.py +++ b/atst/utils/docx.py @@ -1,5 +1,4 @@ import os -from io import BytesIO from zipfile import ZipFile from flask import render_template, current_app as app @@ -31,15 +30,14 @@ class Docx: @classmethod def render( cls, + file_like, doc_template="docx/document.xml", file_template="docx/template.docx", **args, ): document = render_template(doc_template, **args) - byte_str = BytesIO() - docx_file = ZipFile(byte_str, mode="w") - docx_template = Docx._template(file_template) - Docx._write(docx_template, docx_file, document) - docx_file.close() - byte_str.seek(0) - return byte_str + 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 diff --git a/tests/utils/test_docx.py b/tests/utils/test_docx.py index e35e2fc4..9b643609 100644 --- a/tests/utils/test_docx.py +++ b/tests/utils/test_docx.py @@ -6,7 +6,8 @@ from atst.utils.docx import Docx def test_render_docx(): data = {"droid_class": "R2"} - docx_file = Docx.render(data=data) + 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 From 60fcd1d42a65233fa276f3b22dc095f6e05fe6db Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 21 Dec 2018 14:16:33 -0500 Subject: [PATCH 7/8] add docstring for clarity in Docx._write --- atst/utils/docx.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/atst/utils/docx.py b/atst/utils/docx.py index bd4b1ddd..f5e644e4 100644 --- a/atst/utils/docx.py +++ b/atst/utils/docx.py @@ -15,13 +15,27 @@ class Docx: return ZipFile(Docx._template_path(docx_file), mode="r") @classmethod - def _write(cls, docx_template, docx_file, document): + 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 = document_content docx_file.writestr(item, content) From 5d7efe6909e85d66fac39e74bcabb8afb28e22ae Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 21 Dec 2018 14:22:05 -0500 Subject: [PATCH 8/8] modify task order submission flash message --- atst/routes/task_orders/invite.py | 2 +- atst/utils/flash.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/atst/routes/task_orders/invite.py b/atst/routes/task_orders/invite.py index 6fab5b04..c43f14b5 100644 --- a/atst/routes/task_orders/invite.py +++ b/atst/routes/task_orders/invite.py @@ -9,7 +9,7 @@ from atst.utils.flash import formatted_flash as flash @task_orders_bp.route("/task_orders/invite/", methods=["POST"]) def invite(task_order_id): task_order = TaskOrders.get(task_order_id) - flash("task_order_complete", task_order=task_order) + flash("task_order_submitted", task_order=task_order) return redirect( url_for("workspaces.workspace_members", workspace_id=task_order.workspace.id) ) diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 70bd0fbd..2d294e8e 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -101,10 +101,10 @@ MESSAGES = { "message_template": "", "category": "success", }, - "task_order_complete": { - "title_template": "Task Order Complete", + "task_order_submitted": { + "title_template": "Task Order Form Submitted", "message_template": """ - Invitations for {{ task_order.portfolio_name }} have been sent. + Your task order form for {{ task_order.portfolio_name }} has been submitted. """, "category": "success", },