Merge branch 'master' into ie/fix-toggler
This commit is contained in:
commit
de04fc39b0
@ -7,7 +7,10 @@ from .validators import Alphabet, PhoneNumber
|
|||||||
|
|
||||||
|
|
||||||
class CCPOReviewForm(ValidatedForm):
|
class CCPOReviewForm(ValidatedForm):
|
||||||
comment = TextAreaField("Comments (optional)")
|
comment = TextAreaField(
|
||||||
|
"Instructions or comments",
|
||||||
|
description="Provide instructions or notes for additional information that is necessary to approve the request here. The requestor may then re-submit the updated request or initiate contact outside of AT-AT if further discussion is required. <strong>This message will be shared with the person making the JEDI request.</strong>.",
|
||||||
|
)
|
||||||
fname_mao = StringField(
|
fname_mao = StringField(
|
||||||
"First Name (optional)", validators=[Optional(), Alphabet()]
|
"First Name (optional)", validators=[Optional(), Alphabet()]
|
||||||
)
|
)
|
||||||
|
@ -5,4 +5,8 @@ from .forms import ValidatedForm
|
|||||||
|
|
||||||
|
|
||||||
class InternalCommentForm(ValidatedForm):
|
class InternalCommentForm(ValidatedForm):
|
||||||
text = TextAreaField(validators=[Optional()])
|
text = TextAreaField(
|
||||||
|
"CCPO Internal Notes",
|
||||||
|
description="You may add additional comments and notes for internal CCPO reference and follow-up here.",
|
||||||
|
validators=[Optional()],
|
||||||
|
)
|
||||||
|
@ -194,3 +194,13 @@ class Request(Base):
|
|||||||
@property
|
@property
|
||||||
def is_approved(self):
|
def is_approved(self):
|
||||||
return self.status == RequestStatus.APPROVED
|
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:
|
||||||
|
return review.comment
|
||||||
|
@ -8,7 +8,7 @@ class RequestReview(Base):
|
|||||||
__tablename__ = "request_reviews"
|
__tablename__ = "request_reviews"
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True)
|
id = Column(BigInteger, primary_key=True)
|
||||||
status = relationship("RequestStatusEvent", back_populates="review")
|
status = relationship("RequestStatusEvent", uselist=False, back_populates="review")
|
||||||
|
|
||||||
user_id = Column(ForeignKey("users.id"), nullable=False)
|
user_id = Column(ForeignKey("users.id"), nullable=False)
|
||||||
reviewer = relationship("User")
|
reviewer = relationship("User")
|
||||||
|
@ -48,6 +48,8 @@ class RequestStatusEvent(Base):
|
|||||||
def log_name(self):
|
def log_name(self):
|
||||||
if self.new_status == RequestStatus.CHANGES_REQUESTED:
|
if self.new_status == RequestStatus.CHANGES_REQUESTED:
|
||||||
return "Denied"
|
return "Denied"
|
||||||
|
if self.new_status == RequestStatus.CHANGES_REQUESTED_TO_FINVER:
|
||||||
|
return "Denied"
|
||||||
elif self.new_status == RequestStatus.PENDING_FINANCIAL_VERIFICATION:
|
elif self.new_status == RequestStatus.PENDING_FINANCIAL_VERIFICATION:
|
||||||
return "Accepted"
|
return "Accepted"
|
||||||
else:
|
else:
|
||||||
|
@ -49,10 +49,9 @@ def login_redirect():
|
|||||||
return redirect(url_for(".home"))
|
return redirect(url_for(".home"))
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_certificate(request):
|
@bp.route("/logout")
|
||||||
cert = request.environ.get("HTTP_X_SSL_CLIENT_CERT")
|
def logout():
|
||||||
if cert:
|
if session.get("user_id"):
|
||||||
result = app.crl_validator.validate(cert.encode())
|
del (session["user_id"])
|
||||||
return result
|
|
||||||
else:
|
return redirect(url_for(".home"))
|
||||||
return False
|
|
||||||
|
@ -12,7 +12,6 @@ from . import requests_bp
|
|||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
from atst.forms.ccpo_review import CCPOReviewForm
|
from atst.forms.ccpo_review import CCPOReviewForm
|
||||||
from atst.forms.internal_comment import InternalCommentForm
|
|
||||||
|
|
||||||
|
|
||||||
def map_ccpo_authorizing(user):
|
def map_ccpo_authorizing(user):
|
||||||
@ -26,8 +25,6 @@ def render_approval(request, form=None):
|
|||||||
if pending_final_approval and request.task_order:
|
if pending_final_approval and request.task_order:
|
||||||
data["task_order"] = request.task_order.to_dictionary()
|
data["task_order"] = request.task_order.to_dictionary()
|
||||||
|
|
||||||
internal_comment_form = InternalCommentForm(text=request.internal_comments_text)
|
|
||||||
|
|
||||||
if not form:
|
if not form:
|
||||||
mo_data = map_ccpo_authorizing(g.current_user)
|
mo_data = map_ccpo_authorizing(g.current_user)
|
||||||
form = CCPOReviewForm(data=mo_data)
|
form = CCPOReviewForm(data=mo_data)
|
||||||
@ -35,13 +32,12 @@ def render_approval(request, form=None):
|
|||||||
return render_template(
|
return render_template(
|
||||||
"requests/approval.html",
|
"requests/approval.html",
|
||||||
data=data,
|
data=data,
|
||||||
status_events=reversed(request.status_events),
|
reviews=list(reversed(request.reviews)),
|
||||||
request=request,
|
request=request,
|
||||||
current_status=request.status.value,
|
current_status=request.status.value,
|
||||||
pending_review=pending_review,
|
pending_review=pending_review,
|
||||||
financial_review=pending_final_approval,
|
financial_review=pending_final_approval,
|
||||||
f=form or CCPOReviewForm(),
|
f=form or CCPOReviewForm(),
|
||||||
internal_comment_form=internal_comment_form,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -58,7 +54,7 @@ def submit_approval(request_id):
|
|||||||
|
|
||||||
form = CCPOReviewForm(http_request.form)
|
form = CCPOReviewForm(http_request.form)
|
||||||
if form.validate():
|
if form.validate():
|
||||||
if http_request.form.get("approved"):
|
if http_request.form.get("review") == "approving":
|
||||||
Requests.advance(g.current_user, request, form.data)
|
Requests.advance(g.current_user, request, form.data)
|
||||||
else:
|
else:
|
||||||
Requests.request_changes(g.current_user, request, form.data)
|
Requests.request_changes(g.current_user, request, form.data)
|
||||||
@ -84,13 +80,3 @@ def task_order_pdf_download(request_id):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
raise NotFoundError("task_order pdf")
|
raise NotFoundError("task_order pdf")
|
||||||
|
|
||||||
|
|
||||||
@requests_bp.route("/requests/internal_comments/<string:request_id>", methods=["POST"])
|
|
||||||
def create_internal_comment(request_id):
|
|
||||||
form = InternalCommentForm(http_request.form)
|
|
||||||
if form.validate():
|
|
||||||
request = Requests.get(g.current_user, request_id)
|
|
||||||
Requests.update_internal_comments(g.current_user, request, form.data["text"])
|
|
||||||
|
|
||||||
return redirect(url_for("requests.approval", request_id=request_id))
|
|
||||||
|
@ -38,7 +38,8 @@ def financial_verification(request_id=None):
|
|||||||
return render_template(
|
return render_template(
|
||||||
"requests/financial_verification.html",
|
"requests/financial_verification.html",
|
||||||
f=form,
|
f=form,
|
||||||
request_id=request_id,
|
request=request,
|
||||||
|
review_comment=request.review_comment,
|
||||||
extended=is_extended(request),
|
extended=is_extended(request),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ def update_financial_verification(request_id):
|
|||||||
existing_request = Requests.get(g.current_user, request_id)
|
existing_request = Requests.get(g.current_user, request_id)
|
||||||
form = financial_form(existing_request, post_data)
|
form = financial_form(existing_request, post_data)
|
||||||
rerender_args = dict(
|
rerender_args = dict(
|
||||||
request_id=request_id, f=form, extended=is_extended(existing_request)
|
request=existing_request, f=form, extended=is_extended(existing_request)
|
||||||
)
|
)
|
||||||
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
|
@ -62,6 +62,7 @@ def requests_form_update(screen=1, request_id=None):
|
|||||||
next_screen=screen + 1,
|
next_screen=screen + 1,
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
jedi_request=jedi_flow.request,
|
jedi_request=jedi_flow.request,
|
||||||
|
review_comment=request.review_comment,
|
||||||
can_submit=jedi_flow.can_submit,
|
can_submit=jedi_flow.can_submit,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -132,6 +133,10 @@ def view_request_details(request_id=None):
|
|||||||
or request.is_approved
|
or request.is_approved
|
||||||
or request.is_pending_financial_verification_changes
|
or request.is_pending_financial_verification_changes
|
||||||
)
|
)
|
||||||
|
requires_fv_action = (
|
||||||
|
request.is_pending_financial_verification
|
||||||
|
or request.is_pending_financial_verification_changes
|
||||||
|
)
|
||||||
|
|
||||||
data = request.body
|
data = request.body
|
||||||
if financial_review and request.task_order:
|
if financial_review and request.task_order:
|
||||||
@ -140,10 +145,6 @@ def view_request_details(request_id=None):
|
|||||||
return render_template(
|
return render_template(
|
||||||
"requests/details.html",
|
"requests/details.html",
|
||||||
data=data,
|
data=data,
|
||||||
request_id=request.id,
|
request=request,
|
||||||
status=request.status_displayname,
|
requires_fv_action=requires_fv_action,
|
||||||
pending_review=request.is_pending_ccpo_action,
|
|
||||||
financial_verification=request.is_pending_financial_verification
|
|
||||||
or request.is_pending_financial_verification_changes,
|
|
||||||
financial_review=financial_review,
|
|
||||||
)
|
)
|
||||||
|
28
js/components/forms/ccpo_approval.js
Normal file
28
js/components/forms/ccpo_approval.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import textinput from '../text_input'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ccpo-approval',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
textinput
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
approving: false,
|
||||||
|
denying: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setReview: function (e) {
|
||||||
|
if (e.target.value === 'approving') {
|
||||||
|
this.approving = true
|
||||||
|
this.denying = false
|
||||||
|
} else {
|
||||||
|
this.approving = false
|
||||||
|
this.denying = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
21
js/components/local_datetime.js
Normal file
21
js/components/local_datetime.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { format } from 'date-fns'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'local-datetime',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
timestamp: String,
|
||||||
|
format: {
|
||||||
|
type: String,
|
||||||
|
default: 'MMM D YYYY H:mm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
displayTime: function () {
|
||||||
|
return format(this.timestamp, this.format)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
template: '<time v-bind:datetime="timestamp">{{ this.displayTime }}</time>'
|
||||||
|
}
|
@ -19,7 +19,8 @@ export default {
|
|||||||
default: () => ''
|
default: () => ''
|
||||||
},
|
},
|
||||||
initialErrors: Array,
|
initialErrors: Array,
|
||||||
paragraph: String
|
paragraph: String,
|
||||||
|
noMaxWidth: String
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
|
@ -17,6 +17,8 @@ import Modal from './mixins/modal'
|
|||||||
import selector from './components/selector'
|
import selector from './components/selector'
|
||||||
import BudgetChart from './components/charts/budget_chart'
|
import BudgetChart from './components/charts/budget_chart'
|
||||||
import SpendTable from './components/tables/spend_table'
|
import SpendTable from './components/tables/spend_table'
|
||||||
|
import CcpoApproval from './components/forms/ccpo_approval'
|
||||||
|
import LocalDatetime from './components/local_datetime'
|
||||||
|
|
||||||
Vue.use(VTooltip)
|
Vue.use(VTooltip)
|
||||||
|
|
||||||
@ -35,7 +37,9 @@ const app = new Vue({
|
|||||||
NewProject,
|
NewProject,
|
||||||
selector,
|
selector,
|
||||||
BudgetChart,
|
BudgetChart,
|
||||||
SpendTable
|
SpendTable,
|
||||||
|
CcpoApproval,
|
||||||
|
LocalDatetime
|
||||||
},
|
},
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
const modalOpen = document.querySelector("#modalOpen")
|
const modalOpen = document.querySelector("#modalOpen")
|
||||||
|
@ -9,6 +9,10 @@
|
|||||||
border-left-width: $gap / 2;
|
border-left-width: $gap / 2;
|
||||||
border-left-style: solid;
|
border-left-style: solid;
|
||||||
@include panel-margin;
|
@include panel-margin;
|
||||||
|
|
||||||
|
@include media($medium-screen) {
|
||||||
|
padding: $gap * 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin alert-level($level) {
|
@mixin alert-level($level) {
|
||||||
@ -53,6 +57,10 @@
|
|||||||
.alert__title {
|
.alert__title {
|
||||||
@include h3;
|
@include h3;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert__content {
|
.alert__content {
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
margin-top: $gap * 4;
|
margin-top: $gap * 4;
|
||||||
margin-bottom: $gap * 4;
|
margin-bottom: $gap * 4;
|
||||||
|
|
||||||
|
&--tight {
|
||||||
|
margin-top: $gap * 2;
|
||||||
|
}
|
||||||
|
|
||||||
.usa-button,
|
.usa-button,
|
||||||
a {
|
a {
|
||||||
margin: 0 0 0 ($gap * 2);
|
margin: 0 0 0 ($gap * 2);
|
||||||
|
@ -246,6 +246,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.no-max-width {
|
||||||
|
padding-right: $gap * 3;
|
||||||
|
|
||||||
|
input, textarea, select, label {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-validation {
|
||||||
|
left: auto;
|
||||||
|
right: - $gap * 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.usa-input--error {
|
&.usa-input--error {
|
||||||
@include input-state('error');
|
@include input-state('error');
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
margin: 0 $gap;
|
margin: 0 $gap;
|
||||||
padding: 0 $gap;
|
padding: 0 $gap;
|
||||||
border-radius: $gap / 2;
|
border-radius: $gap / 2;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&.label--info {
|
&.label--info {
|
||||||
background-color: $color-primary;
|
background-color: $color-primary;
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
|
|
||||||
.panel__heading {
|
.panel__heading {
|
||||||
padding: $gap * 2;
|
padding: $gap * 2;
|
||||||
|
|
||||||
@include media($medium-screen) {
|
@include media($medium-screen) {
|
||||||
padding: $gap * 4;
|
padding: $gap * 4;
|
||||||
}
|
}
|
||||||
@ -71,6 +72,10 @@
|
|||||||
padding: $gap*2;
|
padding: $gap*2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--divider {
|
||||||
|
border-bottom: 1px solid $color-gray-light;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -90,15 +95,16 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px dashed $color-gray-light;
|
||||||
|
margin: ($gap * 4) ($site-margins*-4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel__actions {
|
.panel__actions {
|
||||||
@include panel-actions;
|
@include panel-actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px dashed $color-gray-light;
|
|
||||||
margin: 0px $site-margins*-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@ -32,6 +32,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.request-approval__review {
|
||||||
|
.action-group {
|
||||||
|
margin-bottom: $gap * 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.approval-log {
|
.approval-log {
|
||||||
ol {
|
ol {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -43,7 +49,7 @@
|
|||||||
border-top: 1px dashed $color-gray-light;
|
border-top: 1px dashed $color-gray-light;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-top-style: solid;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media($medium-screen) {
|
@include media($medium-screen) {
|
||||||
@ -94,4 +100,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.internal-notes {
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,35 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/tooltip.html" import Tooltip %}
|
{% from "components/tooltip.html" import Tooltip %}
|
||||||
|
|
||||||
{% macro TextInput(field, tooltip='', placeholder='', validation='anything', paragraph=False, initial_value='') -%}
|
{% macro TextInput(
|
||||||
|
field,
|
||||||
|
label=field.label | striptags,
|
||||||
|
description=field.description,
|
||||||
|
tooltip='',
|
||||||
|
placeholder='',
|
||||||
|
validation='anything',
|
||||||
|
paragraph=False,
|
||||||
|
initial_value='',
|
||||||
|
noMaxWidth=False) -%}
|
||||||
|
|
||||||
<textinput
|
<textinput
|
||||||
name='{{ field.name }}'
|
name='{{ field.name }}'
|
||||||
validation='{{ validation }}'
|
validation='{{ validation }}'
|
||||||
{% if paragraph %}paragraph='true'{% endif %}
|
{% if paragraph %}paragraph='true'{% endif %}
|
||||||
|
{% if noMaxWidth %}no-max-width='true'{% endif %}
|
||||||
{% if initial_value or field.data %}initial-value='{{ initial_value or field.data }}'{% endif %}
|
{% if initial_value or field.data %}initial-value='{{ initial_value or field.data }}'{% endif %}
|
||||||
{% if field.errors %}v-bind:initial-errors='{{ field.errors }}'{% endif %}
|
{% if field.errors %}v-bind:initial-errors='{{ field.errors }}'{% endif %}
|
||||||
key='{{ field.name }}'
|
key='{{ field.name }}'
|
||||||
inline-template>
|
inline-template>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-bind:class="['usa-input usa-input--validation--' + validation, { 'usa-input--error': showError, 'usa-input--success': showValid, 'usa-input--validation--paragraph': paragraph }]">
|
v-bind:class="['usa-input usa-input--validation--' + validation, { 'usa-input--error': showError, 'usa-input--success': showValid, 'usa-input--validation--paragraph': paragraph, 'no-max-width': noMaxWidth }]">
|
||||||
|
|
||||||
<label for={{field.name}}>
|
<label for={{field.name}}>
|
||||||
<div class="usa-input__title">{{ field.label | striptags }} {% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}</div>
|
<div class="usa-input__title">{{ label }} {% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}</div>
|
||||||
|
|
||||||
{% if field.description %}
|
{% if field.description %}
|
||||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
<span class='usa-input__help'>{{ description | safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<span v-show='showError'>{{ Icon('alert',classes="icon-validation") }}</span>
|
<span v-show='showError'>{{ Icon('alert',classes="icon-validation") }}</span>
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
{{ Icon('avatar', classes='topbar__link-icon') }}
|
{{ Icon('avatar', classes='topbar__link-icon') }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/" class="topbar__link" title='Log out of JEDI Cloud'>
|
<a href="{{ url_for('atst.logout') }}" class="topbar__link" title='Log out of JEDI Cloud'>
|
||||||
{{ Icon('logout', classes='topbar__link-icon') }}
|
{{ Icon('logout', classes='topbar__link-icon') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
{% include 'requests/menu.html' %}
|
{% include 'requests/menu.html' %}
|
||||||
|
|
||||||
|
{% if review_comment %}
|
||||||
|
{% include 'requests/comment.html' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block form_action %}
|
{% block form_action %}
|
||||||
{% if request_id %}
|
{% if request_id %}
|
||||||
<form method='POST' action="{{ url_for('requests.requests_form_update', screen=current, request_id=request_id) }}" autocomplete="off">
|
<form method='POST' action="{{ url_for('requests.requests_form_update', screen=current, request_id=request_id) }}" autocomplete="off">
|
||||||
|
@ -6,8 +6,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<dt>{{ title | safe }}</dt>
|
<dt>{{ title | safe }}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if data[section] and data[section][item_name] %}
|
{% set value = data.get(section, {}).get(item_name) %}
|
||||||
{{ data[section][item_name] | findFilter(filter, filter_args) }}
|
{% if value is not none %}
|
||||||
|
{{ value | findFilter(filter, filter_args) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ RequiredLabel() }}
|
{{ RequiredLabel() }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -23,8 +24,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<h2>
|
<h2>
|
||||||
Details of Use
|
Details of Use
|
||||||
{% if editable %}
|
{% if editable %}
|
||||||
|
@ -16,8 +16,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<section class='panel'>
|
<section class='panel'>
|
||||||
<header class='panel__heading request-approval__heading'>
|
<header class='panel__heading panel__heading--divider request-approval__heading'>
|
||||||
<h1 class='h2'>Request #{{ request.id }}</h1>
|
<h1 class='h2'>Request #{{ request.id }}
|
||||||
|
</h1>
|
||||||
<span class='label label--info'>{{ current_status }}</span>
|
<span class='label label--info'>{{ current_status }}</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -32,38 +33,58 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if pending_review %}
|
{% if pending_review %}
|
||||||
|
<section class='request-approval__review'>
|
||||||
<form method="POST" action="{{ url_for("requests.submit_approval", request_id=request.id) }}" autocomplete="off">
|
<form method="POST" action="{{ url_for("requests.submit_approval", request_id=request.id) }}" autocomplete="off">
|
||||||
{{ f.csrf_token }}
|
{{ f.csrf_token }}
|
||||||
<section class='panel'>
|
|
||||||
<header class='panel__heading'>
|
|
||||||
<h2 class='h3'>Approval Notes</h2>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
|
<ccpo-approval inline-template>
|
||||||
|
<div>
|
||||||
|
<div class='panel'>
|
||||||
<div class='panel__content'>
|
<div class='panel__content'>
|
||||||
|
<h2>Review this Request</h2>
|
||||||
|
|
||||||
<div class="form__sub-fields">
|
<div class='usa-input'>
|
||||||
|
<fieldset class='usa-input__choices usa-input__choices--inline'>
|
||||||
<h3>Instructions for the Requestor</h3>
|
<input v-on:change='setReview' type='radio' name='review' id='review-approving' value='approving'/>
|
||||||
|
<label for='review-approving'>Ready for approval</label>
|
||||||
Provide instructions or notes for additional information that is necessary to approve the request here. The requestor may then re-submit the updated request or initiate contact outside of AT-AT if further discussion is required. <b>These notes will be visible to the person making the JEDI Cloud request</b>.
|
|
||||||
|
|
||||||
{{ TextInput(f.comment, paragraph=True, placeholder="Add notes or comments explaining what changes are being requested or why further discussion is needed about this request.") }}
|
|
||||||
|
|
||||||
|
<input v-on:change='setReview' type='radio' name='review' id='review-denying' value='denying'/>
|
||||||
|
<label for='review-denying'>Request revisions</label>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div v-if='approving || denying' class='form__sub-fields' v-cloak>
|
||||||
<div class="form-col">
|
<h3>Message to Requestor <span class='subtitle'>(optional)</span></h3>
|
||||||
<h3>Authorizing Officials</h3>
|
<div v-if='approving' key='approving' v-cloak>
|
||||||
<p>This section is not visible to the person making the request. It is only viewable by CCPO staff.</p>
|
{{ TextInput(
|
||||||
<p>Provide the name of the key officials for both parties that have authorized this request to move forward.</p>
|
f.comment,
|
||||||
|
label='Approval comments or notes',
|
||||||
|
description='Provide any comments or notes regarding the approval of this request. <strong>This message will be shared with the person making the JEDI request.</strong>.',
|
||||||
|
paragraph=True,
|
||||||
|
noMaxWidth=True
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else key='denying' v-cloak>
|
||||||
|
{{ TextInput(
|
||||||
|
f.comment,
|
||||||
|
label='Revision instructions or notes',
|
||||||
|
paragraph=True,
|
||||||
|
noMaxWidth=True
|
||||||
|
) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="h3">Mission Authorizing Official</h4>
|
|
||||||
|
|
||||||
|
<div v-if='approving' class='form__sub-fields' v-cloak>
|
||||||
|
|
||||||
|
<h3>Authorizing Officials <span class='subtitle'>(optional)</span></h3>
|
||||||
|
<p>Provide the name of the key officials for both parties that have authorized this request to move forward. <strong>This section is not visible to the person making the request. It is only viewable by CCPO staff.</strong></p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h4>Mission Authorizing Official</h4>
|
||||||
|
|
||||||
<div class='form-row'>
|
<div class='form-row'>
|
||||||
|
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(f.fname_mao, placeholder="First name of mission authorizing official") }}
|
{{ TextInput(f.fname_mao, placeholder="First name of mission authorizing official") }}
|
||||||
</div>
|
</div>
|
||||||
@ -71,27 +92,23 @@
|
|||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(f.lname_mao, placeholder="Last name of mission authorizing official") }}
|
{{ TextInput(f.lname_mao, placeholder="Last name of mission authorizing official") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='form-row'>
|
<div class='form-row'>
|
||||||
|
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(f.email_mao, placeholder="name@mail.mil") }}
|
{{ TextInput(f.email_mao, placeholder="name@mail.mil", validation='email') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(f.phone_mao, placeholder="(123) 456-7890", validation='usPhone') }}
|
{{ TextInput(f.phone_mao, placeholder="(123) 456-7890", validation='usPhone') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
<h4 class="h3">CCPO Authorizing Official</h4>
|
<h4>CCPO Authorizing Official</h4>
|
||||||
|
|
||||||
<div class='form-row'>
|
<div class='form-row'>
|
||||||
|
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(f.fname_ccpo, placeholder="First name of CCPO authorizing official") }}
|
{{ TextInput(f.fname_ccpo, placeholder="First name of CCPO authorizing official") }}
|
||||||
</div>
|
</div>
|
||||||
@ -99,86 +116,79 @@
|
|||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(f.lname_ccpo, placeholder="Last name of CCPO authorizing official") }}
|
{{ TextInput(f.lname_ccpo, placeholder="Last name of CCPO authorizing official") }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if='approving || denying' class='action-group' v-cloak>
|
||||||
|
<button v-if='approving' type="submit" name="approved" class='usa-button usa-button-big'>Approve Request</button>
|
||||||
</section>
|
<button v-if='denying' type="submit" name="denied" class='usa-button usa-button-big'>Request Revisions</button>
|
||||||
|
|
||||||
<section class='action-group'>
|
|
||||||
<input type="submit" name="approved" class='usa-button usa-button-big' value='Approve Request'>
|
|
||||||
<input type="submit" name="denied" class='usa-button usa-button-big usa-button-secondary' value='Mark as Changes Requested'>
|
|
||||||
<a href='{{ url_for("requests.requests_index") }}' class='icon-link'>
|
<a href='{{ url_for("requests.requests_index") }}' class='icon-link'>
|
||||||
{{ Icon('x') }}
|
{{ Icon('x') }}
|
||||||
<span>Cancel</span>
|
<span>Cancel</span>
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</div>
|
||||||
|
</div>
|
||||||
|
</ccpo-approval>
|
||||||
</form>
|
</form>
|
||||||
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<section class='panel'>
|
<section>
|
||||||
<h4 class="h3">CCPO Internal Notes</h4>
|
<div class='panel'>
|
||||||
<p>You may add additional comments and notes for internal CCPO reference and follow-up here.</p>
|
<header class='panel__heading panel__heading--divider'>
|
||||||
<div class='form-row'>
|
<h2 class='h3 request-approval__columns__heading'>CCPO Activity Log</h2>
|
||||||
<div class='form-col'>
|
|
||||||
<form method="POST" action="{{ url_for('requests.create_internal_comment', request_id=request.id) }}">
|
|
||||||
{{ internal_comment_form.csrf_token }}
|
|
||||||
{{ TextInput(internal_comment_form.text, paragraph=True) }}
|
|
||||||
<button type="submit">Leave comment</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class='panel'>
|
|
||||||
<header class='panel__heading'>
|
|
||||||
<h2 class='h3 request-approval__columns__heading'>Approval Log</h2>
|
|
||||||
</header>
|
</header>
|
||||||
<div>
|
|
||||||
<div class='approval-log'>
|
|
||||||
<ol>
|
|
||||||
|
|
||||||
{% for status_event in status_events %}
|
<div class='approval-log'>
|
||||||
{% if status_event.review %}
|
{% if reviews %}
|
||||||
|
<ol>
|
||||||
|
{% for review in reviews %}
|
||||||
<li>
|
<li>
|
||||||
<article class='approval-log__log-item'>
|
<article class='approval-log__log-item'>
|
||||||
<div>
|
<div>
|
||||||
<h3 class='approval-log__log-item__header'>{{ status_event.log_name }} by {{ status_event.review.full_name_reviewer }}</h3>
|
{{ review.log_name }}
|
||||||
{% if status_event.review.comment %}
|
<h3 class='approval-log__log-item__header'>{{ review.status.log_name }} by {{ review.full_name_reviewer }}</h3>
|
||||||
<p>{{ status_event.review.comment }}</p>
|
{% if review.comment %}
|
||||||
|
<p>{{ review.comment }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class='approval-log__behalfs'>
|
<div class='approval-log__behalfs'>
|
||||||
{% if status_event.review.lname_mao %}
|
{% if review.lname_mao %}
|
||||||
<div class='approval-log__behalf'>
|
<div class='approval-log__behalf'>
|
||||||
<h3 class='approval-log__log-item__header'>Mission Owner approval on behalf of:</h3>
|
<h3 class='approval-log__log-item__header'>Mission Owner approval on behalf of:</h3>
|
||||||
<span>{{ status_event.review.full_name_mao }}</span>
|
<span>{{ review.full_name_mao }}</span>
|
||||||
<span>{{ status_event.review.email_mao }}</span>
|
<span>{{ review.email_mao }}</span>
|
||||||
<span>{{ status_event.review.phone_mao }}</span>
|
<span>{{ review.phone_mao }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if status_event.review.lname_ccpo %}
|
{% if review.lname_ccpo %}
|
||||||
<div class='approval-log__behalf'>
|
<div class='approval-log__behalf'>
|
||||||
<h3 class='approval-log__log-item__header'>CCPO approval on behalf of:</h3>
|
<h3 class='approval-log__log-item__header'>CCPO approval on behalf of:</h3>
|
||||||
<span>{{ status_event.review.full_name_ccpo }}</span>
|
<span>{{ review.full_name_ccpo }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% set timestamp=status_event.time_created | formattedDate("%Y-%m-%d %H:%M:%S %Z") %}
|
{% set timestamp=review.status.time_created | formattedDate("%Y-%m-%d %H:%M:%S %Z") %}
|
||||||
<footer class='approval-log__log-item__timestamp'><time datetime='{{ timestamp }}'>{{ timestamp }}</time></footer>
|
<footer class='approval-log__log-item__timestamp'>
|
||||||
|
<local-datetime timestamp='{{ timestamp }}'></local-datetime>
|
||||||
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</ol>
|
</ol>
|
||||||
|
{% else %}
|
||||||
|
<div class='panel__content'>
|
||||||
|
<p class='h4'>No CCPO approvals or request changes have been recorded yet.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
9
templates/requests/comment.html
Normal file
9
templates/requests/comment.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% from "components/alert.html" import Alert %}
|
||||||
|
|
||||||
|
{% call Alert('Changes Requested', level='warning') %}
|
||||||
|
<p>CCPO has requested changes to your submission with the following notes:
|
||||||
|
<br>
|
||||||
|
{{ review_comment }}
|
||||||
|
<br>
|
||||||
|
Please contact info@jedi.cloud or 123-123-4567 for further discussion.</p>
|
||||||
|
{% endcall %}
|
@ -5,21 +5,28 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
||||||
{% if financial_verification %}
|
{% if request.is_pending_ccpo_acceptance %}
|
||||||
{% include 'requests/review_menu.html' %}
|
|
||||||
{% endif %}
|
{{ Alert('Request submitted. Approval pending.', fragment="fragments/pending_ccpo_acceptance_alert.html") }}
|
||||||
|
|
||||||
|
{% elif request.is_pending_ccpo_approval %}
|
||||||
|
|
||||||
|
{% call Alert('Pending CCPO Approval') %}
|
||||||
|
<p>The CCPO will review and respond to your Financial Verification submission in 3 business days. You will be notified via email or phone.</p>
|
||||||
|
<p>Once the financial verification is approved you will be invited to create your JEDI Workspace and set-up your projects. Click here for more details.</p>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% elif requires_fv_action %}
|
||||||
|
|
||||||
|
{{ Alert('Pending Financial Verification', fragment="fragments/pending_financial_verification.html") }}
|
||||||
|
{% include 'requests/review_menu.html' %}
|
||||||
|
|
||||||
{% if pending_review %}
|
|
||||||
{{ Alert('Your request is being reviewed',
|
|
||||||
message="<p>You cannot edit your submitted request while it is under review. Your request will be reviewed within 3 business days.</p>",
|
|
||||||
level='warning'
|
|
||||||
) }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel__heading">
|
<div class="panel__heading">
|
||||||
<h1>Request Details</h1><br/>
|
<h1>Request Details</h1><br/>
|
||||||
<h2>#{{ request_id }} <span class="label label--info">{{ status }}</span></h2>
|
<h2>#{{ request.id }} <span class="label label--info">{{ request.status_displayname }}</span></h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
|
@ -8,6 +8,14 @@
|
|||||||
|
|
||||||
{% include 'requests/review_menu.html' %}
|
{% include 'requests/review_menu.html' %}
|
||||||
|
|
||||||
|
{% if request.is_pending_financial_verification %}
|
||||||
|
{{ Alert('Pending Financial Verification', fragment="fragments/pending_financial_verification.html") }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if review_comment %}
|
||||||
|
{% include 'requests/comment.html' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<financial inline-template v-bind:initial-data='{{ f.data|mixedContentToJson }}'>
|
<financial inline-template v-bind:initial-data='{{ f.data|mixedContentToJson }}'>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
||||||
@ -19,21 +27,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if f.is_missing_task_order_number %}
|
{% if f.is_missing_task_order_number %}
|
||||||
{% set extended_url = url_for('requests.financial_verification', request_id=request_id, extended=True) %}
|
{% set extended_url = url_for('requests.financial_verification', request_id=request.id, extended=True) %}
|
||||||
{{ Alert('Task Order not found in EDA',
|
{% call Alert('Task Order not found in EDA', level='warning') %}
|
||||||
message="We could not find your Task Order in our system of record, EDA.
|
We could not find your Task Order in our system of record, EDA. Please confirm that you have entered it correctly.<br>
|
||||||
Please confirm that you have entered it correctly.<br>
|
<a class="usa-button" href="{{ extended_url }}">Enter Task Order information manually</a>
|
||||||
<a class=\"usa-button\" href=\"%s\">Enter Task Order information manually</a>
|
{% endcall %}
|
||||||
"|format(extended_url),
|
|
||||||
level='warning'
|
|
||||||
) }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% block form_action %}
|
{% block form_action %}
|
||||||
{% if extended %}
|
{% if extended %}
|
||||||
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id, extended=True) }}" autocomplete="off" enctype="multipart/form-data">
|
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request.id, extended=True) }}" autocomplete="off" enctype="multipart/form-data">
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id) }}" autocomplete="off">
|
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request.id) }}" autocomplete="off">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -52,7 +57,7 @@
|
|||||||
|
|
||||||
<div class="panel__heading">
|
<div class="panel__heading">
|
||||||
<h1>Financial Verification</h1>
|
<h1>Financial Verification</h1>
|
||||||
<div class="subtitle" id="financial-verification"><h2>Order #{{ request_id }}</h2></div>
|
<div class="subtitle" id="financial-verification"><h2>Order #{{ request.id }}</h2></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
|
@ -45,18 +45,6 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{% if pending_financial_verification %}
|
|
||||||
|
|
||||||
{{ Alert('Pending Financial Verification', fragment="fragments/pending_financial_verification.html") }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if pending_ccpo_approval %}
|
|
||||||
|
|
||||||
{{ Alert('Request submitted. Approval pending.', fragment="fragments/pending_ccpo_approval_alert.html") }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if extended_view %}
|
{% if extended_view %}
|
||||||
<div class="row kpi">
|
<div class="row kpi">
|
||||||
<div class="kpi__item col col--grow">
|
<div class="kpi__item col col--grow">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% set pending_url=url_for('requests.view_request_details', request_id=request_id) %}
|
{% set pending_url=url_for('requests.view_request_details', request_id=request.id) %}
|
||||||
{% set financial_url=url_for('requests.financial_verification', request_id=request_id) %}
|
{% set financial_url=url_for('requests.financial_verification', request_id=request.id) %}
|
||||||
<div class="progress-menu progress-menu--four">
|
<div class="progress-menu progress-menu--four">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="progress-menu__item progress-menu__item--complete">
|
<li class="progress-menu__item progress-menu__item--complete">
|
||||||
|
@ -92,3 +92,26 @@ def test_reviews():
|
|||||||
RequestStatusEventFactory.create(revision=request.latest_revision),
|
RequestStatusEventFactory.create(revision=request.latest_revision),
|
||||||
]
|
]
|
||||||
assert len(request.reviews) == 2
|
assert len(request.reviews) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_comment():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
ccpo = UserFactory.from_atat_role("ccpo")
|
||||||
|
request.status_events = [
|
||||||
|
RequestStatusEventFactory.create(
|
||||||
|
revision=request.latest_revision,
|
||||||
|
new_status=RequestStatus.CHANGES_REQUESTED,
|
||||||
|
review=RequestReviewFactory.create(reviewer=ccpo, comment="do better"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
assert request.review_comment == "do better"
|
||||||
|
|
||||||
|
request.status_events = [
|
||||||
|
RequestStatusEventFactory.create(
|
||||||
|
revision=request.latest_revision,
|
||||||
|
new_status=RequestStatus.APPROVED,
|
||||||
|
review=RequestReviewFactory.create(reviewer=ccpo, comment="much better"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
assert not request.review_comment
|
||||||
|
@ -3,9 +3,16 @@ import pytest
|
|||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from atst.eda_client import MockEDAClient
|
from atst.eda_client import MockEDAClient
|
||||||
|
from atst.models.request_status_event import RequestStatus
|
||||||
|
|
||||||
from tests.mocks import MOCK_REQUEST, MOCK_USER
|
from tests.mocks import MOCK_REQUEST, MOCK_USER
|
||||||
from tests.factories import PENumberFactory, RequestFactory, UserFactory
|
from tests.factories import (
|
||||||
|
PENumberFactory,
|
||||||
|
RequestFactory,
|
||||||
|
UserFactory,
|
||||||
|
RequestStatusEventFactory,
|
||||||
|
RequestReviewFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestPENumberInForm:
|
class TestPENumberInForm:
|
||||||
@ -148,3 +155,21 @@ class TestPENumberInForm:
|
|||||||
response = self.submit_data(client, user, data, extended=True)
|
response = self.submit_data(client, user, data, extended=True)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
review_comment = "add all of the correct info, instead of the incorrect info"
|
||||||
|
request.status_events = [
|
||||||
|
RequestStatusEventFactory.create(
|
||||||
|
revision=request.latest_revision,
|
||||||
|
new_status=RequestStatus.CHANGES_REQUESTED_TO_FINVER,
|
||||||
|
review=RequestReviewFactory.create(reviewer=ccpo, comment=review_comment),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
response = client.get("/requests/verify/{}".format(request.id))
|
||||||
|
body = response.data.decode()
|
||||||
|
assert review_comment in body
|
||||||
|
@ -87,7 +87,7 @@ def test_can_submit_request_approval(client, user_session):
|
|||||||
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
|
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
|
||||||
)
|
)
|
||||||
review_data = RequestReviewFactory.dictionary()
|
review_data = RequestReviewFactory.dictionary()
|
||||||
review_data["approved"] = True
|
review_data["review"] = "approving"
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url_for("requests.submit_approval", request_id=request.id), data=review_data
|
url_for("requests.submit_approval", request_id=request.id), data=review_data
|
||||||
)
|
)
|
||||||
@ -102,7 +102,7 @@ def test_can_submit_request_denial(client, user_session):
|
|||||||
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
|
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
|
||||||
)
|
)
|
||||||
review_data = RequestReviewFactory.dictionary()
|
review_data = RequestReviewFactory.dictionary()
|
||||||
review_data["denied"] = True
|
review_data["review"] = "denying"
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url_for("requests.submit_approval", request_id=request.id), data=review_data
|
url_for("requests.submit_approval", request_id=request.id), data=review_data
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import re
|
import re
|
||||||
from tests.factories import RequestFactory, UserFactory, RequestRevisionFactory
|
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.roles import Roles
|
||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
@ -213,3 +220,21 @@ def test_can_review_data(user_session, client):
|
|||||||
# assert a sampling of the request data is on the review page
|
# assert a sampling of the request data is on the review page
|
||||||
assert request.body["primary_poc"]["fname_poc"] in body
|
assert request.body["primary_poc"]["fname_poc"] in body
|
||||||
assert request.body["information_about_you"]["email_request"] 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)
|
||||||
|
review_comment = "add all of the correct info, instead of the incorrect info"
|
||||||
|
request.status_events = [
|
||||||
|
RequestStatusEventFactory.create(
|
||||||
|
revision=request.latest_revision,
|
||||||
|
new_status=RequestStatus.CHANGES_REQUESTED,
|
||||||
|
review=RequestReviewFactory.create(reviewer=ccpo, comment=review_comment),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
response = client.get("/requests/new/1/{}".format(request.id))
|
||||||
|
body = response.data.decode()
|
||||||
|
assert review_comment in body
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import session, url_for
|
from flask import session, url_for
|
||||||
from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
|
from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
|
||||||
@ -14,6 +16,17 @@ def _fetch_user_info(c, t):
|
|||||||
return MOCK_USER
|
return MOCK_USER
|
||||||
|
|
||||||
|
|
||||||
|
def _login(client, verify="SUCCESS", sdn=DOD_SDN, cert=""):
|
||||||
|
return client.get(
|
||||||
|
url_for("atst.login_redirect"),
|
||||||
|
environ_base={
|
||||||
|
"HTTP_X_SSL_CLIENT_VERIFY": verify,
|
||||||
|
"HTTP_X_SSL_CLIENT_S_DN": sdn,
|
||||||
|
"HTTP_X_SSL_CLIENT_CERT": cert,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_successful_login_redirect_non_ccpo(client, monkeypatch):
|
def test_successful_login_redirect_non_ccpo(client, monkeypatch):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True
|
"atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True
|
||||||
@ -23,14 +36,7 @@ def test_successful_login_redirect_non_ccpo(client, monkeypatch):
|
|||||||
lambda *args: UserFactory.create(),
|
lambda *args: UserFactory.create(),
|
||||||
)
|
)
|
||||||
|
|
||||||
resp = client.get(
|
resp = _login(client)
|
||||||
"/login-redirect",
|
|
||||||
environ_base={
|
|
||||||
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
|
||||||
"HTTP_X_SSL_CLIENT_S_DN": "",
|
|
||||||
"HTTP_X_SSL_CLIENT_CERT": "",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
assert "home" in resp.headers["Location"]
|
assert "home" in resp.headers["Location"]
|
||||||
@ -47,14 +53,7 @@ def test_successful_login_redirect_ccpo(client, monkeypatch):
|
|||||||
lambda *args: UserFactory.create(atat_role=role),
|
lambda *args: UserFactory.create(atat_role=role),
|
||||||
)
|
)
|
||||||
|
|
||||||
resp = client.get(
|
resp = _login(client)
|
||||||
"/login-redirect",
|
|
||||||
environ_base={
|
|
||||||
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
|
||||||
"HTTP_X_SSL_CLIENT_S_DN": "",
|
|
||||||
"HTTP_X_SSL_CLIENT_CERT": "",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
assert "home" in resp.headers["Location"]
|
assert "home" in resp.headers["Location"]
|
||||||
@ -62,7 +61,7 @@ def test_successful_login_redirect_ccpo(client, monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_unsuccessful_login_redirect(client, monkeypatch):
|
def test_unsuccessful_login_redirect(client, monkeypatch):
|
||||||
resp = client.get("/login-redirect")
|
resp = client.get(url_for("atst.login_redirect"))
|
||||||
|
|
||||||
assert resp.status_code == 401
|
assert resp.status_code == 401
|
||||||
assert "user_id" not in session
|
assert "user_id" not in session
|
||||||
@ -99,26 +98,12 @@ def test_crl_validation_on_login(client):
|
|||||||
bad_cert = open("ssl/client-certs/bad-atat.mil.crt").read()
|
bad_cert = open("ssl/client-certs/bad-atat.mil.crt").read()
|
||||||
|
|
||||||
# bad cert is on the test CRL
|
# bad cert is on the test CRL
|
||||||
resp = client.get(
|
resp = _login(client, cert=bad_cert)
|
||||||
"/login-redirect",
|
|
||||||
environ_base={
|
|
||||||
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
|
||||||
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
|
||||||
"HTTP_X_SSL_CLIENT_CERT": bad_cert,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert resp.status_code == 401
|
assert resp.status_code == 401
|
||||||
assert "user_id" not in session
|
assert "user_id" not in session
|
||||||
|
|
||||||
# good cert is not on the test CRL, passes
|
# good cert is not on the test CRL, passes
|
||||||
resp = client.get(
|
resp = _login(client, cert=good_cert)
|
||||||
"/login-redirect",
|
|
||||||
environ_base={
|
|
||||||
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
|
||||||
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
|
||||||
"HTTP_X_SSL_CLIENT_CERT": good_cert,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert session["user_id"]
|
assert session["user_id"]
|
||||||
|
|
||||||
|
|
||||||
@ -132,14 +117,7 @@ def test_creates_new_user_on_login(monkeypatch, client):
|
|||||||
with pytest.raises(NotFoundError):
|
with pytest.raises(NotFoundError):
|
||||||
Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
||||||
|
|
||||||
resp = client.get(
|
resp = _login(client, cert=cert_file)
|
||||||
"/login-redirect",
|
|
||||||
environ_base={
|
|
||||||
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
|
||||||
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
|
||||||
"HTTP_X_SSL_CLIENT_CERT": cert_file,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
user = Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
user = Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
||||||
assert user.first_name == DOD_SDN_INFO["first_name"]
|
assert user.first_name == DOD_SDN_INFO["first_name"]
|
||||||
@ -148,23 +126,36 @@ def test_creates_new_user_on_login(monkeypatch, client):
|
|||||||
|
|
||||||
|
|
||||||
def test_creates_new_user_without_email_on_login(monkeypatch, client):
|
def test_creates_new_user_without_email_on_login(monkeypatch, client):
|
||||||
monkeypatch.setattr("atst.routes._is_valid_certificate", lambda *args: True)
|
|
||||||
cert_file = open("ssl/client-certs/atat.mil.crt").read()
|
cert_file = open("ssl/client-certs/atat.mil.crt").read()
|
||||||
|
|
||||||
# ensure user does not exist
|
# ensure user does not exist
|
||||||
with pytest.raises(NotFoundError):
|
with pytest.raises(NotFoundError):
|
||||||
Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
||||||
|
|
||||||
resp = client.get(
|
resp = _login(client, cert=cert_file)
|
||||||
"/login-redirect",
|
|
||||||
environ_base={
|
|
||||||
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
|
||||||
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
|
||||||
"HTTP_X_SSL_CLIENT_CERT": cert_file,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
user = Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
user = Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
||||||
assert user.first_name == DOD_SDN_INFO["first_name"]
|
assert user.first_name == DOD_SDN_INFO["first_name"]
|
||||||
assert user.last_name == DOD_SDN_INFO["last_name"]
|
assert user.last_name == DOD_SDN_INFO["last_name"]
|
||||||
assert user.email == None
|
assert user.email == None
|
||||||
|
|
||||||
|
|
||||||
|
def test_logout(app, client, monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"atst.domain.authnid.AuthenticationContext.authenticate", lambda s: True
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"atst.domain.authnid.AuthenticationContext.get_user",
|
||||||
|
lambda s: UserFactory.create(),
|
||||||
|
)
|
||||||
|
# create a real session
|
||||||
|
resp = _login(client)
|
||||||
|
resp_success = client.get(url_for("requests.requests_index"))
|
||||||
|
# verify session is valid
|
||||||
|
assert resp_success.status_code == 200
|
||||||
|
client.get(url_for("atst.logout"))
|
||||||
|
resp_failure = client.get(url_for("requests.requests_index"))
|
||||||
|
# verify that logging out has cleared the session
|
||||||
|
assert resp_failure.status_code == 302
|
||||||
|
destination = urlparse(resp_failure.headers["Location"]).path
|
||||||
|
assert destination == url_for("atst.root")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user