diff --git a/alembic/versions/2572be7fb7fc_add_request_internal_comments.py b/alembic/versions/2572be7fb7fc_add_request_internal_comments.py new file mode 100644 index 00000000..f17ee18e --- /dev/null +++ b/alembic/versions/2572be7fb7fc_add_request_internal_comments.py @@ -0,0 +1,36 @@ +"""add request.internal_comments + +Revision ID: 2572be7fb7fc +Revises: dea6b8e09d63 +Create Date: 2018-09-11 15:28:27.252248 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '2572be7fb7fc' +down_revision = 'dea6b8e09d63' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('request_internal_comments', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('text', sa.String(), nullable=True), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('request_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint(['request_id'], ['requests.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('request_internal_comments') + # ### end Alembic commands ### diff --git a/atst/domain/requests.py b/atst/domain/requests.py index 47629ebc..808f2553 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -13,6 +13,7 @@ from atst.models.request import Request from atst.models.request_revision import RequestRevision from atst.models.request_status_event import RequestStatusEvent, RequestStatus from atst.models.request_review import RequestReview +from atst.models.request_internal_comment import RequestInternalComment from atst.utils import deep_merge from .exceptions import NotFoundError, UnauthorizedError @@ -311,3 +312,13 @@ WHERE requests_with_status.status = :status Requests.set_status(request, RequestStatus.CHANGES_REQUESTED_TO_FINVER) return Requests._add_review(user, request, review_data) + + @classmethod + def update_internal_comments(cls, user, request, comment_text): + Authorization.check_can_approve_request(user) + + request.internal_comments = RequestInternalComment(text=comment_text, user=user) + db.session.add(request) + db.session.commit() + + return request diff --git a/atst/forms/internal_comment.py b/atst/forms/internal_comment.py new file mode 100644 index 00000000..a64833c8 --- /dev/null +++ b/atst/forms/internal_comment.py @@ -0,0 +1,8 @@ +from wtforms.fields import TextAreaField +from wtforms.validators import Optional + +from .forms import ValidatedForm + + +class InternalCommentForm(ValidatedForm): + text = TextAreaField(validators=[Optional()]) diff --git a/atst/models/__init__.py b/atst/models/__init__.py index 245e3806..0b8a95df 100644 --- a/atst/models/__init__.py +++ b/atst/models/__init__.py @@ -16,3 +16,4 @@ from .environment import Environment from .attachment import Attachment from .request_revision import RequestRevision from .request_review import RequestReview +from .request_internal_comment import RequestInternalComment diff --git a/atst/models/request.py b/atst/models/request.py index 8205410c..0b90db6d 100644 --- a/atst/models/request.py +++ b/atst/models/request.py @@ -46,6 +46,8 @@ class Request(Base): "RequestRevision", back_populates="request", order_by="RequestRevision.sequence" ) + internal_comments = relationship("RequestInternalComment", uselist=False) + @property def latest_revision(self): if self.revisions: @@ -163,3 +165,7 @@ class Request(Base): @property def reviews(self): return [status.review for status in self.status_events if status.review] + + @property + def internal_comments_text(self): + return self.internal_comments.text if self.internal_comments else "" diff --git a/atst/models/request_internal_comment.py b/atst/models/request_internal_comment.py new file mode 100644 index 00000000..f3633910 --- /dev/null +++ b/atst/models/request_internal_comment.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, BigInteger, String, ForeignKey +from sqlalchemy.orm import relationship + +from atst.models import Base + + +class RequestInternalComment(Base): + __tablename__ = "request_internal_comments" + + id = Column(BigInteger, primary_key=True) + text = Column(String()) + + user_id = Column(ForeignKey("users.id"), nullable=False) + user = relationship("User") + + request_id = Column(ForeignKey("requests.id", ondelete="CASCADE")) diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py index de67fba4..fcb0ca66 100644 --- a/atst/routes/requests/approval.py +++ b/atst/routes/requests/approval.py @@ -12,6 +12,7 @@ from . import requests_bp from atst.domain.requests import Requests from atst.domain.exceptions import NotFoundError from atst.forms.ccpo_review import CCPOReviewForm +from atst.forms.internal_comment import InternalCommentForm def task_order_dictionary(task_order): @@ -31,16 +32,19 @@ def render_approval(request, form=None): if pending_final_approval and request.task_order: data["task_order"] = task_order_dictionary(request.task_order) + internal_comment_form = InternalCommentForm(text=request.internal_comments_text) + return render_template( "requests/approval.html", data=data, status_events=reversed(request.status_events), - request_id=request.id, + request=request, current_status=request.status.value, pending_review=pending_review, financial_review=pending_final_approval, pdf_available=request.task_order and request.task_order.pdf, f=form or CCPOReviewForm(), + internal_comment_form=internal_comment_form, ) @@ -83,3 +87,13 @@ def task_order_pdf_download(request_id): else: raise NotFoundError("task_order pdf") + + +@requests_bp.route("/requests/internal_comments/", 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)) diff --git a/templates/requests/approval.html b/templates/requests/approval.html index f29814fc..a8e7f450 100644 --- a/templates/requests/approval.html +++ b/templates/requests/approval.html @@ -15,17 +15,15 @@ ) }} {% endif %} -
- {{ f.csrf_token }}
-

Request #{{ request_id }}

+

Request #{{ request.id }}

{{ current_status }}
- {% with data=data, request_id=request_id %} + {% with data=data, request_id=request.id %} {% include "requests/_review.html" %} {% endwith %} @@ -34,6 +32,8 @@
{% if pending_review %} + + {{ f.csrf_token }}

Approval Notes

@@ -104,25 +104,6 @@ -

CCPO Internal Notes

- -

You may add additional comments and notes for internal CCPO reference and follow-up here.

- -
- -
- -
- - -
- -
- -
- - -
@@ -133,8 +114,24 @@ Cancel
+
{% endif %} +
+

CCPO Internal Notes

+

You may add additional comments and notes for internal CCPO reference and follow-up here.

+
+
+
+ {{ internal_comment_form.csrf_token }} + {{ TextInput(internal_comment_form.text, paragraph=True) }} + +
+
+
+ +
+

Approval Log

@@ -182,8 +179,6 @@
- - {% endblock %} diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index bb4d8949..c9f03d8e 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -221,3 +221,12 @@ def test_request_changes_to_financial_verification_info(): assert request.status == RequestStatus.CHANGES_REQUESTED_TO_FINVER current_review = request.latest_status.review assert current_review.fname_mao == review_data["fname_mao"] + + +def test_update_internal_comments(): + request = RequestFactory.create() + ccpo = UserFactory.from_atat_role("ccpo") + + request = Requests.update_internal_comments(ccpo, request, "this is my comment") + + assert request.internal_comments.text == "this is my comment"