You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@trafficcontrol.apache.org by GitBox <gi...@apache.org> on 2018/03/27 15:31:10 UTC

[GitHub] dewrich closed pull request #2027: [Issue 1819/1820] - adds ds request comments to TP

dewrich closed pull request #2027: [Issue 1819/1820] - adds ds request comments to TP
URL: https://github.com/apache/incubator-trafficcontrol/pull/2027
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a694fdd4..a3cc28bbe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 ## [Unreleased]
 ### Added
 - Per-DeliveryService Routing Names: you can now choose a Delivery Service's Routing Name (rather than a hardcoded "tr" or "edge" name). This might require a few pre-upgrade steps detailed [here](http://traffic-control-cdn.readthedocs.io/en/latest/admin/traffic_ops/migration_from_20_to_22.html#per-deliveryservice-routing-names)
+- [Delivery Service Requests](http://traffic-control-cdn.readthedocs.io/en/latest/admin/quick_howto/ds_requests.html#ds-requests): When enabled, delivery service requests are created when ALL users attempt to create, update or delete a delivery service. This allows users with higher level permissions to review delivery service changes for completeness and accuracy before deploying the changes.
 - Traffic Ops Golang Proxy Endpoints (R=REST endpoints for GET, POST, PUT, DELETE)
   - /api/1.3/about
   - /api/1.3/asns (R)
diff --git a/lib/go-tc/deliveryservice_request_comments.go b/lib/go-tc/deliveryservice_request_comments.go
new file mode 100644
index 000000000..55d376194
--- /dev/null
+++ b/lib/go-tc/deliveryservice_request_comments.go
@@ -0,0 +1,44 @@
+package tc
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+type DeliveryServiceRequestCommentsResponse struct {
+	Response []DeliveryServiceRequestComment `json:"response"`
+}
+
+type DeliveryServiceRequestComment struct {
+	AuthorID                 IDNoMod   `json:"authorId" db:"author_id"`
+	Author                   string    `json:"author"`
+	DeliveryServiceRequestID int       `json:"deliveryServiceRequestId" db:"deliveryservice_request_id"`
+	ID                       int       `json:"id" db:"id"`
+	LastUpdated              TimeNoMod `json:"lastUpdated" db:"last_updated"`
+	Value                    string    `json:"value" db:"value"`
+	XMLID                    string    `json:"xmlId" db:"xml_id"`
+}
+
+type DeliveryServiceRequestCommentNullable struct {
+	AuthorID                 *IDNoMod   `json:"authorId" db:"author_id"`
+	Author                   *string    `json:"author"`
+	DeliveryServiceRequestID *int       `json:"deliveryServiceRequestId" db:"deliveryservice_request_id"`
+	ID                       *int       `json:"id" db:"id"`
+	LastUpdated              *TimeNoMod `json:"lastUpdated" db:"last_updated"`
+	Value                    *string    `json:"value" db:"value"`
+	XMLID                    *string    `json:"xmlId" db:"xml_id"`
+}
diff --git a/traffic_ops/app/db/migrations/20180319000000_ds-request-comments.sql b/traffic_ops/app/db/migrations/20180319000000_ds-request-comments.sql
new file mode 100644
index 000000000..787f0551f
--- /dev/null
+++ b/traffic_ops/app/db/migrations/20180319000000_ds-request-comments.sql
@@ -0,0 +1,36 @@
+/*
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+*/
+
+-- +goose Up
+-- SQL in section 'Up' is executed when this migration is applied
+CREATE TABLE deliveryservice_request_comment (
+    author_id bigint NOT NULL,
+    deliveryservice_request_id bigint NOT NULL,
+    id bigserial primary key NOT NULL,
+    last_updated timestamp WITH time zone NOT NULL DEFAULT now(),
+    value text NOT NULL
+);
+
+ALTER TABLE deliveryservice_request_comment
+    ADD CONSTRAINT fk_author FOREIGN KEY (author_id) REFERENCES tm_user(id) ON DELETE CASCADE;
+
+ALTER TABLE deliveryservice_request_comment
+    ADD CONSTRAINT fk_deliveryservice_request FOREIGN KEY (deliveryservice_request_id) REFERENCES deliveryservice_request(id) ON DELETE CASCADE;
+
+CREATE TRIGGER on_update_current_timestamp BEFORE UPDATE ON deliveryservice_request_comment FOR EACH ROW EXECUTE PROCEDURE on_update_current_timestamp_last_updated();
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+DROP TABLE deliveryservice_request_comment;
diff --git a/traffic_ops/client/v13/deliveryservice_request_comments.go b/traffic_ops/client/v13/deliveryservice_request_comments.go
new file mode 100644
index 000000000..c168dc4d3
--- /dev/null
+++ b/traffic_ops/client/v13/deliveryservice_request_comments.go
@@ -0,0 +1,118 @@
+/*
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package v13
+
+import (
+	"encoding/json"
+	"net"
+	"net/http"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+
+	"fmt"
+)
+
+const (
+	API_v13_DeliveryServiceRequestComments = "/api/1.3/deliveryservice_request_comments"
+)
+
+// Create a delivery service request comment
+func (to *Session) CreateDeliveryServiceRequestComment(comment tc.DeliveryServiceRequestComment) (tc.Alerts, ReqInf, error) {
+
+	var remoteAddr net.Addr
+	reqBody, err := json.Marshal(comment)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if err != nil {
+		return tc.Alerts{}, reqInf, err
+	}
+	resp, remoteAddr, err := to.request(http.MethodPost, API_v13_DeliveryServiceRequestComments, reqBody)
+	log.Infof("%s", reqBody)
+	if err != nil {
+		return tc.Alerts{}, reqInf, err
+	}
+	defer resp.Body.Close()
+	var alerts tc.Alerts
+	err = json.NewDecoder(resp.Body).Decode(&alerts)
+	return alerts, reqInf, nil
+}
+
+// Update a delivery service request by ID
+func (to *Session) UpdateDeliveryServiceRequestCommentByID(id int, comment tc.DeliveryServiceRequestComment) (tc.Alerts, ReqInf, error) {
+
+	var remoteAddr net.Addr
+	reqBody, err := json.Marshal(comment)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if err != nil {
+		return tc.Alerts{}, reqInf, err
+	}
+	route := fmt.Sprintf("%s/%d", API_v13_DeliveryServiceRequestComments, id)
+	resp, remoteAddr, err := to.request(http.MethodPut, route, reqBody)
+	if err != nil {
+		return tc.Alerts{}, reqInf, err
+	}
+	defer resp.Body.Close()
+	var alerts tc.Alerts
+	err = json.NewDecoder(resp.Body).Decode(&alerts)
+	return alerts, reqInf, nil
+}
+
+// Returns a list of delivery service request comments
+func (to *Session) GetDeliveryServiceRequestComments() ([]tc.DeliveryServiceRequestComment, ReqInf, error) {
+	resp, remoteAddr, err := to.request(http.MethodGet, API_v13_DeliveryServiceRequestComments, nil)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if err != nil {
+		return nil, reqInf, err
+	}
+	defer resp.Body.Close()
+
+	var data tc.DeliveryServiceRequestCommentsResponse
+	err = json.NewDecoder(resp.Body).Decode(&data)
+	return data.Response, reqInf, nil
+}
+
+// GET a delivery service request comment by ID
+func (to *Session) GetDeliveryServiceRequestCommentByID(id int) ([]tc.DeliveryServiceRequestComment, ReqInf, error) {
+	route := fmt.Sprintf("%s/%d", API_v13_DeliveryServiceRequestComments, id)
+	resp, remoteAddr, err := to.request(http.MethodGet, route, nil)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if err != nil {
+		return nil, reqInf, err
+	}
+	defer resp.Body.Close()
+
+	var data tc.DeliveryServiceRequestCommentsResponse
+	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+		return nil, reqInf, err
+	}
+
+	return data.Response, reqInf, nil
+}
+
+// DELETE a delivery service request comment by ID
+func (to *Session) DeleteDeliveryServiceRequestCommentByID(id int) (tc.Alerts, ReqInf, error) {
+	route := fmt.Sprintf("%s/%d", API_v13_DeliveryServiceRequestComments, id)
+	resp, remoteAddr, err := to.request(http.MethodDelete, route, nil)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if err != nil {
+		return tc.Alerts{}, reqInf, err
+	}
+	defer resp.Body.Close()
+	var alerts tc.Alerts
+	err = json.NewDecoder(resp.Body).Decode(&alerts)
+	return alerts, reqInf, nil
+}
+
diff --git a/traffic_ops/testing/api/v13/deliveryservice_request_comments_test.go b/traffic_ops/testing/api/v13/deliveryservice_request_comments_test.go
new file mode 100644
index 000000000..b5eda0083
--- /dev/null
+++ b/traffic_ops/testing/api/v13/deliveryservice_request_comments_test.go
@@ -0,0 +1,115 @@
+package v13
+
+/*
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+import (
+	"testing"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+)
+
+func TestDeliveryServiceRequestComments(t *testing.T) {
+
+	CreateTestDeliveryServiceRequests(t)
+	CreateTestDeliveryServiceRequestComments(t)
+	UpdateTestDeliveryServiceRequestComments(t)
+	GetTestDeliveryServiceRequestComments(t)
+	DeleteTestDeliveryServiceRequestComments(t)
+	DeleteTestDeliveryServiceRequests(t)
+
+}
+
+func CreateTestDeliveryServiceRequestComments(t *testing.T) {
+
+	// Retrieve a delivery service request by xmlId so we can get the ID needed to create a dsr comment
+	dsr := testData.DeliveryServiceRequests[0].DeliveryService
+
+	resp, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.XMLID)
+	if err != nil {
+		t.Errorf("cannot GET delivery service request by xml id: %v - %v\n", dsr.XMLID, err)
+	}
+	respDSR := resp[0]
+
+	for _, comment := range testData.DeliveryServiceRequestComments {
+		comment.DeliveryServiceRequestID = respDSR.ID
+		resp, _, err := TOSession.CreateDeliveryServiceRequestComment(comment)
+		log.Debugln("Response: ", resp)
+		if err != nil {
+			t.Errorf("could not CREATE delivery service request comment: %v\n", err)
+		}
+	}
+
+}
+
+func UpdateTestDeliveryServiceRequestComments(t *testing.T) {
+
+	comments, _, err := TOSession.GetDeliveryServiceRequestComments()
+
+	firstComment := comments[0]
+	newFirstCommentValue := "new comment value"
+	firstComment.Value = newFirstCommentValue
+
+	var alert tc.Alerts
+	alert, _, err = TOSession.UpdateDeliveryServiceRequestCommentByID(firstComment.ID, firstComment)
+	if err != nil {
+		t.Errorf("cannot UPDATE delivery service request comment by id: %v - %v\n", err, alert)
+	}
+
+	// Retrieve the delivery service request comment to check that the value got updated
+	resp, _, err := TOSession.GetDeliveryServiceRequestCommentByID(firstComment.ID)
+	if err != nil {
+		t.Errorf("cannot GET delivery service request comment by id: '$%s' %v - %v\n", firstComment.ID, err)
+	}
+	respDSRC := resp[0]
+	if respDSRC.Value != newFirstCommentValue {
+		t.Errorf("results do not match actual: %s, expected: %s\n", respDSRC.Value, newFirstCommentValue)
+	}
+
+}
+
+func GetTestDeliveryServiceRequestComments(t *testing.T) {
+
+	comments, _, _ := TOSession.GetDeliveryServiceRequestComments()
+
+	for _, comment := range comments {
+		resp, _, err := TOSession.GetDeliveryServiceRequestCommentByID(comment.ID)
+		if err != nil {
+			t.Errorf("cannot GET delivery service request comment by id: %v - %v\n", err, resp)
+		}
+	}
+}
+
+func DeleteTestDeliveryServiceRequestComments(t *testing.T) {
+
+	comments, _, _ := TOSession.GetDeliveryServiceRequestComments()
+
+	for _, comment := range comments {
+		_, _, err := TOSession.DeleteDeliveryServiceRequestCommentByID(comment.ID)
+		if err != nil {
+			t.Errorf("cannot DELETE delivery service request comment by id: '%s' %v\n", comment.ID, err)
+		}
+
+		// Retrieve the delivery service request comment to see if it got deleted
+		comments, _, err := TOSession.GetDeliveryServiceRequestCommentByID(comment.ID)
+		if err != nil {
+			t.Errorf("error deleting delivery service request comment: %s\n", err.Error())
+		}
+		if len(comments) > 0 {
+			t.Errorf("expected delivery service request comment: %s to be deleted\n", comment.ID)
+		}
+	}
+}
diff --git a/traffic_ops/testing/api/v13/tc-fixtures.json b/traffic_ops/testing/api/v13/tc-fixtures.json
index 783b0fc33..b5be3ec3a 100644
--- a/traffic_ops/testing/api/v13/tc-fixtures.json
+++ b/traffic_ops/testing/api/v13/tc-fixtures.json
@@ -148,6 +148,20 @@
             "status": "draft"
         }
     ],
+    "deliveryServiceRequestComments": [
+        {
+            "value": "this is comment one"
+        },
+        {
+            "value": "this is comment two"
+        },
+        {
+            "value": "this is comment three"
+        },
+        {
+            "value": "this is comment four"
+        }
+    ],
     "deliveryServices": [
         {
             "active": false,
diff --git a/traffic_ops/testing/api/v13/traffic_control.go b/traffic_ops/testing/api/v13/traffic_control.go
index a350cb1fb..d13f9986f 100644
--- a/traffic_ops/testing/api/v13/traffic_control.go
+++ b/traffic_ops/testing/api/v13/traffic_control.go
@@ -22,17 +22,18 @@ import (
 
 // TrafficControl - maps to the tc-fixtures.json file
 type TrafficControl struct {
-	ASNs                    []tcapi.ASN                    `json:"asns"`
-	CDNs                    []v13.CDN                      `json:"cdns"`
-	CacheGroups             []tcapi.CacheGroup             `json:"cachegroups"`
-	DeliveryServiceRequests []tcapi.DeliveryServiceRequest `json:"deliveryServiceRequests"`
-	DeliveryServices        []tcapi.DeliveryService        `json:"deliveryservices"`
-	Divisions               []tcapi.Division               `json:"divisions"`
-	Profiles                []tcapi.Profile                `json:"profiles"`
-	Parameters              []tcapi.Parameter              `json:"parameters"`
-	PhysLocations           []tcapi.PhysLocation           `json:"physLocations"`
-	Regions                 []tcapi.Region                 `json:"regions"`
-	Statuses                []tcapi.Status                 `json:"statuses"`
-	Tenants                 []tcapi.Tenant                 `json:"tenants"`
-	Types                   []tcapi.Type                   `json:"types"`
+	ASNs                           []tcapi.ASN                           `json:"asns"`
+	CDNs                           []v13.CDN                             `json:"cdns"`
+	CacheGroups                    []tcapi.CacheGroup                    `json:"cachegroups"`
+	DeliveryServiceRequests        []tcapi.DeliveryServiceRequest        `json:"deliveryServiceRequests"`
+	DeliveryServiceRequestComments []tcapi.DeliveryServiceRequestComment `json:"deliveryServiceRequestComments"`
+	DeliveryServices               []tcapi.DeliveryService               `json:"deliveryservices"`
+	Divisions                      []tcapi.Division                      `json:"divisions"`
+	Profiles                       []tcapi.Profile                       `json:"profiles"`
+	Parameters                     []tcapi.Parameter                     `json:"parameters"`
+	PhysLocations                  []tcapi.PhysLocation                  `json:"physLocations"`
+	Regions                        []tcapi.Region                        `json:"regions"`
+	Statuses                       []tcapi.Status                        `json:"statuses"`
+	Tenants                        []tcapi.Tenant                        `json:"tenants"`
+	Types                          []tcapi.Type                          `json:"types"`
 }
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments.go
new file mode 100644
index 000000000..c83bfbfa3
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments.go
@@ -0,0 +1,351 @@
+package comment
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tovalidate"
+	"github.com/go-ozzo/ozzo-validation"
+	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
+)
+
+//we need a type alias to define functions on
+type TODeliveryServiceRequestComment tc.DeliveryServiceRequestCommentNullable
+
+//the refType is passed into the handlers where a copy of its type is used to decode the json.
+var refType = TODeliveryServiceRequestComment{}
+
+func GetRefType() *TODeliveryServiceRequestComment {
+	return &refType
+}
+
+//Implementation of the Identifier, Validator interface functions
+func (comment TODeliveryServiceRequestComment) GetID() (int, bool) {
+	if comment.ID == nil {
+		return 0, false
+	}
+	return *comment.ID, true
+}
+
+func (comment TODeliveryServiceRequestComment) GetAuditName() string {
+	return strconv.Itoa(*comment.ID)
+}
+
+func (comment TODeliveryServiceRequestComment) GetType() string {
+	return "deliveryservice_request_comment"
+}
+
+func (comment *TODeliveryServiceRequestComment) SetID(i int) {
+	comment.ID = &i
+}
+
+func (comment TODeliveryServiceRequestComment) Validate(db *sqlx.DB) []error {
+	errs := validation.Errors{
+		"deliveryServiceRequestId": validation.Validate(comment.DeliveryServiceRequestID, validation.NotNil),
+		"value":                    validation.Validate(comment.Value, validation.NotNil),
+	}
+	return tovalidate.ToErrors(errs)
+}
+
+func (comment *TODeliveryServiceRequestComment) Create(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
+	rollbackTransaction := true
+	tx, err := db.Beginx()
+	defer func() {
+		if tx == nil || !rollbackTransaction {
+			return
+		}
+		err := tx.Rollback()
+		if err != nil {
+			log.Errorln(errors.New("rolling back transaction: " + err.Error()))
+		}
+	}()
+
+	if err != nil {
+		log.Error.Printf("could not begin transaction: %v", err)
+		return tc.DBError, tc.SystemError
+	}
+
+	userID := tc.IDNoMod(user.ID)
+	comment.AuthorID = &userID
+
+	resultRows, err := tx.NamedQuery(insertQuery(), comment)
+
+	if err != nil {
+		if pqErr, ok := err.(*pq.Error); ok {
+			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+			if eType == tc.DataConflictError {
+				return errors.New("a comment with " + err.Error()), eType
+			}
+			return err, eType
+		} else {
+			log.Errorf("received non pq error: %++v from create execution", err)
+			return tc.DBError, tc.SystemError
+		}
+	}
+	defer resultRows.Close()
+
+	var id int
+	var lastUpdated tc.TimeNoMod
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.Scan(&id, &lastUpdated); err != nil {
+			log.Error.Printf("could not scan id from insert: %s\n", err)
+			return tc.DBError, tc.SystemError
+		}
+	}
+	if rowsAffected == 0 {
+		err = errors.New("no cdn was inserted, no id was returned")
+		log.Errorln(err)
+		return tc.DBError, tc.SystemError
+	} else if rowsAffected > 1 {
+		err = errors.New("too many ids returned from comment insert")
+		log.Errorln(err)
+		return tc.DBError, tc.SystemError
+	}
+	comment.SetID(id)
+	comment.LastUpdated = &lastUpdated
+	err = tx.Commit()
+	if err != nil {
+		log.Errorln("Could not commit transaction: ", err)
+		return tc.DBError, tc.SystemError
+	}
+	rollbackTransaction = false
+	return nil, tc.NoError
+}
+
+func (comment *TODeliveryServiceRequestComment) Read(db *sqlx.DB, parameters map[string]string, user auth.CurrentUser) ([]interface{}, []error, tc.ApiErrorType) {
+	var rows *sqlx.Rows
+
+	// Query Parameters to Database Query column mappings
+	// see the fields mapped in the SQL query
+	queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+		"authorId":                 dbhelpers.WhereColumnInfo{"dsrc.author_id", nil},
+		"author":                   dbhelpers.WhereColumnInfo{"a.username", nil},
+		"deliveryServiceRequestId": dbhelpers.WhereColumnInfo{"dsrc.deliveryservice_request_id", nil},
+		"id": dbhelpers.WhereColumnInfo{"dsrc.id", api.IsInt},
+	}
+	where, orderBy, queryValues, errs := dbhelpers.BuildWhereAndOrderBy(parameters, queryParamsToQueryCols)
+	if len(errs) > 0 {
+		return nil, errs, tc.DataConflictError
+	}
+
+	query := selectQuery() + where + orderBy
+	log.Debugln("Query is ", query)
+
+	rows, err := db.NamedQuery(query, queryValues)
+	if err != nil {
+		log.Errorf("Error querying delivery service request comments: %v", err)
+		return nil, []error{tc.DBError}, tc.SystemError
+	}
+	defer rows.Close()
+
+	comments := []interface{}{}
+	for rows.Next() {
+		var s tc.DeliveryServiceRequestCommentNullable
+		if err = rows.StructScan(&s); err != nil {
+			log.Errorf("error parsing delivery service request comment rows: %v", err)
+			return nil, []error{tc.DBError}, tc.SystemError
+		}
+		comments = append(comments, s)
+	}
+
+	return comments, []error{}, tc.NoError
+}
+
+func (comment *TODeliveryServiceRequestComment) Update(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
+
+	var current TODeliveryServiceRequestComment
+	err := db.QueryRowx(selectQuery() + `WHERE dsrc.id=` + strconv.Itoa(*comment.ID)).StructScan(&current)
+	if err != nil {
+		log.Errorf("Error querying DeliveryServiceRequestComments: %v", err)
+		return err, tc.SystemError
+	}
+
+	userID := tc.IDNoMod(user.ID)
+	if *current.AuthorID != userID {
+		return errors.New("Comments can only be updated by the author"), tc.DataConflictError
+	}
+
+	rollbackTransaction := true
+	tx, err := db.Beginx()
+	defer func() {
+		if tx == nil || !rollbackTransaction {
+			return
+		}
+		err := tx.Rollback()
+		if err != nil {
+			log.Errorln(errors.New("rolling back transaction: " + err.Error()))
+		}
+	}()
+
+	if err != nil {
+		log.Error.Printf("could not begin transaction: %v", err)
+		return tc.DBError, tc.SystemError
+	}
+	log.Debugf("about to run exec query: %s with comment: %++v", updateQuery(), comment)
+	resultRows, err := tx.NamedQuery(updateQuery(), comment)
+	if err != nil {
+		if pqErr, ok := err.(*pq.Error); ok {
+			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+			if eType == tc.DataConflictError {
+				return errors.New("a comment with " + err.Error()), eType
+			}
+			return err, eType
+		} else {
+			log.Errorf("received error: %++v from update execution", err)
+			return tc.DBError, tc.SystemError
+		}
+	}
+	defer resultRows.Close()
+
+	var lastUpdated tc.TimeNoMod
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.Scan(&lastUpdated); err != nil {
+			log.Error.Printf("could not scan lastUpdated from insert: %s\n", err)
+			return tc.DBError, tc.SystemError
+		}
+	}
+	log.Debugf("lastUpdated: %++v", lastUpdated)
+	comment.LastUpdated = &lastUpdated
+	if rowsAffected != 1 {
+		if rowsAffected < 1 {
+			return errors.New("no cdn found with this id"), tc.DataMissingError
+		} else {
+			return fmt.Errorf("this update affected too many rows: %d", rowsAffected), tc.SystemError
+		}
+	}
+	err = tx.Commit()
+	if err != nil {
+		log.Errorln("Could not commit transaction: ", err)
+		return tc.DBError, tc.SystemError
+	}
+	rollbackTransaction = false
+	return nil, tc.NoError
+}
+
+func (comment *TODeliveryServiceRequestComment) Delete(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
+
+	var current TODeliveryServiceRequestComment
+	err := db.QueryRowx(selectQuery() + `WHERE dsrc.id=` + strconv.Itoa(*comment.ID)).StructScan(&current)
+	if err != nil {
+		log.Errorf("Error querying DeliveryServiceRequestComments: %v", err)
+		return err, tc.SystemError
+	}
+
+	userID := tc.IDNoMod(user.ID)
+	if *current.AuthorID != userID {
+		return errors.New("Comments can only be deleted by the author"), tc.DataConflictError
+	}
+
+	rollbackTransaction := true
+	tx, err := db.Beginx()
+	defer func() {
+		if tx == nil || !rollbackTransaction {
+			return
+		}
+		err := tx.Rollback()
+		if err != nil {
+			log.Errorln(errors.New("rolling back transaction: " + err.Error()))
+		}
+	}()
+
+	if err != nil {
+		log.Error.Printf("could not begin transaction: %v", err)
+		return tc.DBError, tc.SystemError
+	}
+	log.Debugf("about to run exec query: %s with comment: %++v", deleteQuery(), comment)
+	result, err := tx.NamedExec(deleteQuery(), comment)
+	if err != nil {
+		log.Errorf("received error: %++v from delete execution", err)
+		return tc.DBError, tc.SystemError
+	}
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return tc.DBError, tc.SystemError
+	}
+	if rowsAffected != 1 {
+		if rowsAffected < 1 {
+			return errors.New("no comment with that id found"), tc.DataMissingError
+		} else {
+			return fmt.Errorf("this delete affected too many rows: %d", rowsAffected), tc.SystemError
+		}
+	}
+	err = tx.Commit()
+	if err != nil {
+		log.Errorln("Could not commit transaction: ", err)
+		return tc.DBError, tc.SystemError
+	}
+	rollbackTransaction = false
+	return nil, tc.NoError
+}
+
+func insertQuery() string {
+	query := `INSERT INTO deliveryservice_request_comment (
+author_id,
+deliveryservice_request_id,
+value) VALUES (
+:author_id,
+:deliveryservice_request_id,
+:value) RETURNING id,last_updated`
+	return query
+}
+
+func selectQuery() string {
+	query := `SELECT
+a.username AS author,
+dsrc.author_id,
+dsrc.deliveryservice_request_id,
+dsr.deliveryservice->>'xmlId' as xml_id,
+dsrc.id,
+dsrc.last_updated,
+dsrc.value
+FROM deliveryservice_request_comment dsrc
+JOIN tm_user a ON dsrc.author_id = a.id
+JOIN deliveryservice_request dsr ON dsrc.deliveryservice_request_id = dsr.id
+`
+	return query
+}
+
+func updateQuery() string {
+	query := `UPDATE
+deliveryservice_request_comment SET
+deliveryservice_request_id=:deliveryservice_request_id,
+value=:value
+WHERE id=:id RETURNING last_updated`
+	return query
+}
+
+func deleteQuery() string {
+	query := `DELETE FROM deliveryservice_request_comment
+WHERE id=:id`
+	return query
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments_test.go
new file mode 100644
index 000000000..575abdd33
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments_test.go
@@ -0,0 +1,91 @@
+package comment
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/test"
+
+)
+
+func TestFuncs(t *testing.T) {
+	if strings.Index(selectQuery(), "SELECT") != 0 {
+		t.Errorf("expected selectQuery to start with SELECT")
+	}
+	if strings.Index(insertQuery(), "INSERT") != 0 {
+		t.Errorf("expected insertQuery to start with INSERT")
+	}
+	if strings.Index(updateQuery(), "UPDATE") != 0 {
+		t.Errorf("expected updateQuery to start with UPDATE")
+	}
+	if strings.Index(deleteQuery(), "DELETE") != 0 {
+		t.Errorf("expected deleteQuery to start with DELETE")
+	}
+
+}
+func TestInterfaces(t *testing.T) {
+	var i interface{}
+	i = &TODeliveryServiceRequestComment{}
+
+	if _, ok := i.(api.Creator); !ok {
+		t.Errorf("comment must be creator")
+	}
+	if _, ok := i.(api.Reader); !ok {
+		t.Errorf("comment must be reader")
+	}
+	if _, ok := i.(api.Updater); !ok {
+		t.Errorf("comment must be updater")
+	}
+	if _, ok := i.(api.Deleter); !ok {
+		t.Errorf("comment must be deleter")
+	}
+	if _, ok := i.(api.Identifier); !ok {
+		t.Errorf("comment must be Identifier")
+	}
+}
+
+func TestValidate(t *testing.T) {
+	c := TODeliveryServiceRequestComment{}
+	errs := test.SortErrors(c.Validate(nil))
+
+	expectedErrs := []error{
+		errors.New(`'deliveryServiceRequestId' is required`),
+		errors.New(`'value' is required`),
+	}
+
+	if !reflect.DeepEqual(expectedErrs, errs) {
+		t.Errorf("expected %s, got %s", expectedErrs, errs)
+	}
+
+	v := "the comment value"
+	d := 1
+	c = TODeliveryServiceRequestComment{DeliveryServiceRequestID: &d, Value: &v}
+	expectedErrs = []error{}
+	errs = c.Validate(nil)
+	if !reflect.DeepEqual(expectedErrs, errs) {
+		t.Errorf("expected %s, got %s", expectedErrs, errs)
+	}
+
+}
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index 1108eb572..52d7e0107 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -35,6 +35,7 @@ import (
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/cdn"
 	dsrequest "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice/request"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice/request/comment"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/division"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/hwinfo"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/parameter"
@@ -113,6 +114,13 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{1.3, http.MethodPut, `deliveryservice_requests/{id}/assign$`, api.UpdateHandler(dsrequest.GetAssignRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil},
 		{1.3, http.MethodPut, `deliveryservice_requests/{id}/status$`, api.UpdateHandler(dsrequest.GetStatusRefType(), d.DB), auth.PrivLevelPortal, Authenticated, nil},
 
+		//Delivery service request comments
+		{1.3, http.MethodGet, `deliveryservice_request_comments/?(\.json)?$`, api.ReadHandler(comment.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.3, http.MethodGet, `deliveryservice_request_comments/{id}$`, api.ReadHandler(comment.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.3, http.MethodPut, `deliveryservice_request_comments/{id}$`, api.UpdateHandler(comment.GetRefType(), d.DB), auth.PrivLevelPortal, Authenticated, nil},
+		{1.3, http.MethodPost, `deliveryservice_request_comments/?$`, api.CreateHandler(comment.GetRefType(), d.DB), auth.PrivLevelPortal, Authenticated, nil},
+		{1.3, http.MethodDelete, `deliveryservice_request_comments/{id}$`, api.DeleteHandler(comment.GetRefType(), d.DB), auth.PrivLevelPortal, Authenticated, nil},
+
 		{1.3, http.MethodGet, `deliveryservices/{xmlID}/urisignkeys$`, getURIsignkeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
 		{1.3, http.MethodPost, `deliveryservices/{xmlID}/urisignkeys$`, saveDeliveryServiceURIKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
 		{1.3, http.MethodPut, `deliveryservices/{xmlID}/urisignkeys$`, saveDeliveryServiceURIKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
diff --git a/traffic_portal/app/src/app.js b/traffic_portal/app/src/app.js
index d5530d59c..4169d07c3 100644
--- a/traffic_portal/app/src/app.js
+++ b/traffic_portal/app/src/app.js
@@ -81,6 +81,7 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./modules/private/dashboard').name,
         require('./modules/private/dashboard/view').name,
         require('./modules/private/deliveryServiceRequests').name,
+        require('./modules/private/deliveryServiceRequests/comments').name,
         require('./modules/private/deliveryServiceRequests/edit').name,
         require('./modules/private/deliveryServiceRequests/list').name,
         require('./modules/private/deliveryServices').name,
@@ -186,6 +187,7 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./common/modules/dialog/confirm').name,
         require('./common/modules/dialog/confirm/enter').name,
         require('./common/modules/dialog/delete').name,
+        require('./common/modules/dialog/deliveryServiceRequest').name,
         require('./common/modules/dialog/federationResolver').name,
         require('./common/modules/dialog/import').name,
         require('./common/modules/dialog/input').name,
@@ -193,6 +195,7 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./common/modules/dialog/select').name,
         require('./common/modules/dialog/select/status').name,
         require('./common/modules/dialog/text').name,
+        require('./common/modules/dialog/textarea').name,
         require('./common/modules/header').name,
         require('./common/modules/message').name,
         require('./common/modules/navigation').name,
@@ -281,6 +284,7 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./common/modules/table/deliveryServiceJobs').name,
         require('./common/modules/table/deliveryServiceRegexes').name,
         require('./common/modules/table/deliveryServiceRequests').name,
+        require('./common/modules/table/deliveryServiceRequestComments').name,
         require('./common/modules/table/deliveryServiceServers').name,
         require('./common/modules/table/deliveryServiceStaticDnsEntries').name,
         require('./common/modules/table/deliveryServiceTargets').name,
diff --git a/traffic_portal/app/src/common/api/DeliveryServiceRequestService.js b/traffic_portal/app/src/common/api/DeliveryServiceRequestService.js
index d2674871d..edcff0f8b 100644
--- a/traffic_portal/app/src/common/api/DeliveryServiceRequestService.js
+++ b/traffic_portal/app/src/common/api/DeliveryServiceRequestService.js
@@ -23,17 +23,21 @@ var DeliveryServiceRequestService = function(Restangular, $http, $q, locationUti
 		return Restangular.all('deliveryservice_requests').getList(queryParams);
 	};
 
-	this.createDeliveryServiceRequest = function(dsRequest, delay) {
-		return Restangular.service('deliveryservice_requests').post(dsRequest)
+	this.createDeliveryServiceRequest = function(dsRequest) {
+		var request = $q.defer();
+
+		$http.post(ENV.api['root'] + "deliveryservice_requests", dsRequest)
 			.then(
-				function() {
-					messageModel.setMessages([ { level: 'success', text: 'Created request to ' + dsRequest.changeType + ' the ' + dsRequest.deliveryService.xmlId + ' delivery service' } ], delay);
-					locationUtils.navigateToPath('/delivery-service-requests');
+				function(result) {
+					request.resolve(result.data.response);
 				},
 				function(fault) {
 					messageModel.setMessages(fault.data.alerts, false);
+					request.reject(fault);
 				}
 			);
+
+		return request.promise;
 	};
 
 	this.updateDeliveryServiceRequest = function(id, dsRequest) {
@@ -100,6 +104,60 @@ var DeliveryServiceRequestService = function(Restangular, $http, $q, locationUti
 		return request.promise;
 	};
 
+	this.getDeliveryServiceRequestComments = function(queryParams) {
+		return Restangular.all('deliveryservice_request_comments').getList(queryParams);
+	};
+
+	this.createDeliveryServiceRequestComment = function(comment) {
+		var request = $q.defer();
+
+		$http.post(ENV.api['root'] + "deliveryservice_request_comments", comment)
+			.then(
+				function(response) {
+					request.resolve(response);
+				},
+				function(fault) {
+					request.reject(fault);
+				}
+			);
+
+		return request.promise;
+	};
+
+	this.updateDeliveryServiceRequestComment = function(comment) {
+		var request = $q.defer();
+
+		$http.put(ENV.api['root'] + "deliveryservice_request_comments/" + comment.id, comment)
+			.then(
+				function() {
+					request.resolve();
+				},
+				function(fault) {
+					messageModel.setMessages(fault.data.alerts, false);
+					request.reject();
+				}
+			);
+
+		return request.promise;
+	};
+
+	this.deleteDeliveryServiceRequestComment = function(comment) {
+		var deferred = $q.defer();
+
+		$http.delete(ENV.api['root'] + "deliveryservice_request_comments/" + comment.id)
+			.then(
+				function(response) {
+					deferred.resolve(response);
+				},
+				function(fault) {
+					messageModel.setMessages(fault.data.alerts, false);
+					deferred.reject(fault);
+				}
+			);
+
+		return deferred.promise;
+	};
+
 };
 
 DeliveryServiceRequestService.$inject = ['Restangular', '$http', '$q', 'locationUtils', 'messageModel', 'ENV'];
diff --git a/traffic_portal/app/src/common/modules/dialog/deliveryServiceRequest/DialogDeliveryServiceRequestController.js b/traffic_portal/app/src/common/modules/dialog/deliveryServiceRequest/DialogDeliveryServiceRequestController.js
new file mode 100644
index 000000000..5d6bb8c14
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/dialog/deliveryServiceRequest/DialogDeliveryServiceRequestController.js
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var DialogDeliveryServiceRequestController = function(params, statuses, $scope, $uibModalInstance) {
+
+	$scope.params = params;
+
+	$scope.statuses = statuses;
+
+	$scope.selectedStatusId = null;
+
+	$scope.comment = null;
+
+	$scope.select = function() {
+		var selectedStatus = _.find(statuses, function(status){ return parseInt(status.id) == parseInt($scope.selectedStatusId) });
+		$uibModalInstance.close({ status: selectedStatus, comment: $scope.comment });
+	};
+
+	$scope.cancel = function () {
+		$uibModalInstance.dismiss('cancel');
+	};
+
+};
+
+DialogDeliveryServiceRequestController.$inject = ['params', 'statuses', '$scope', '$uibModalInstance'];
+module.exports = DialogDeliveryServiceRequestController;
diff --git a/traffic_portal/app/src/common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html b/traffic_portal/app/src/common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html
new file mode 100644
index 000000000..37b10a3b6
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html
@@ -0,0 +1,36 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div class="modal-header">
+    <button type="button" class="close" ng-click="cancel()"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+    <h4 class="modal-title">{{::params.title}}</h4>
+</div>
+<div class="modal-body">
+    <form name="dsRequestForm" novalidate>
+        <p ng-bind-html="params.message"></p>
+        <select class="form-control" ng-model="selectedStatusId" ng-options="status.id as status.name for status in statuses" required>
+            <option value="">Select request status...</option>
+        </select>
+        <textarea name="comment" rows="5" cols="17" style="margin-top: 10px;" placeholder="Enter request comment..." class="form-control" ng-model="comment" required></textarea>
+    </form>
+</div>
+<div class="modal-footer">
+    <button class="btn btn-link" ng-click="cancel()">Cancel</button>
+    <button class="btn btn-primary" ng-disabled="dsRequestForm.$invalid" ng-click="select()">Submit</button>
+</div>
diff --git a/traffic_portal/app/src/common/modules/dialog/deliveryServiceRequest/index.js b/traffic_portal/app/src/common/modules/dialog/deliveryServiceRequest/index.js
new file mode 100644
index 000000000..d0374d410
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/dialog/deliveryServiceRequest/index.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = angular.module('trafficPortal.dialog.deliveryServiceRequest', [])
+	.controller('DialogDeliveryServiceRequestController', require('./DialogDeliveryServiceRequestController'));
diff --git a/traffic_portal/app/src/common/modules/dialog/textarea/DialogTextareaController.js b/traffic_portal/app/src/common/modules/dialog/textarea/DialogTextareaController.js
new file mode 100644
index 000000000..69a4ba9c1
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/dialog/textarea/DialogTextareaController.js
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var DialogTextareaController = function(params, $scope, $uibModalInstance) {
+
+	$scope.params = params;
+
+	$scope.text = params.text;
+
+	$scope.select = function() {
+		$uibModalInstance.close($scope.text);
+	};
+
+	$scope.cancel = function () {
+		$uibModalInstance.dismiss('cancel');
+	};
+
+};
+
+DialogTextareaController.$inject = ['params', '$scope', '$uibModalInstance'];
+module.exports = DialogTextareaController;
diff --git a/traffic_portal/app/src/common/modules/dialog/textarea/dialog.textarea.tpl.html b/traffic_portal/app/src/common/modules/dialog/textarea/dialog.textarea.tpl.html
new file mode 100644
index 000000000..71801d689
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/dialog/textarea/dialog.textarea.tpl.html
@@ -0,0 +1,33 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div class="modal-header">
+    <button type="button" class="close" ng-click="cancel()"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+    <h4 class="modal-title">{{::params.title}}</h4>
+</div>
+<div class="modal-body">
+    <p ng-bind-html="params.message"></p>
+    <form name="inputForm" novalidate>
+        <textarea name="text" rows="5" cols="17" style="margin-top: 10px;" placeholder="{{::params.placeholder}}" class="form-control" ng-model="text" required></textarea>
+    </form>
+</div>
+<div class="modal-footer">
+    <button class="btn btn-link" ng-click="cancel()">Cancel</button>
+    <button class="btn btn-primary" ng-disabled="inputForm.$invalid" ng-click="select()">Submit</button>
+</div>
diff --git a/traffic_portal/app/src/common/modules/dialog/textarea/index.js b/traffic_portal/app/src/common/modules/dialog/textarea/index.js
new file mode 100644
index 000000000..17347be1c
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/dialog/textarea/index.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = angular.module('trafficPortal.dialog.textarea', [])
+	.controller('DialogTextareaController', require('./DialogTextareaController'));
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js b/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js
index cc9e7f3ab..67a2f849c 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js
@@ -24,26 +24,47 @@ var FormEditDeliveryServiceController = function(deliveryService, type, types, $
 
 	var createDeliveryServiceDeleteRequest = function(deliveryService) {
 		var params = {
-			title: 'Delete Delivery Service: ' + deliveryService.xmlId,
-			key: deliveryService.xmlId
+			title: "Delivery Service Delete Request",
+			message: 'All delivery service deletions must be reviewed.'
 		};
 		var modalInstance = $uibModal.open({
-			templateUrl: 'common/modules/dialog/delete/dialog.delete.tpl.html',
-			controller: 'DialogDeleteController',
+			templateUrl: 'common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html',
+			controller: 'DialogDeliveryServiceRequestController',
 			size: 'md',
 			resolve: {
 				params: function () {
 					return params;
+				},
+				statuses: function() {
+					return [
+						{ id: $scope.DRAFT, name: 'Save as Draft' },
+						{ id: $scope.SUBMITTED, name: 'Submit for Review and Deployment' }
+					];
 				}
 			}
 		});
-		modalInstance.result.then(function() {
+		modalInstance.result.then(function(options) {
 			var dsRequest = {
 				changeType: 'delete',
-				status: 'submitted',
+				status: (options.status.id == $scope.SUBMITTED) ? 'submitted' : 'draft',
 				deliveryService: deliveryService
 			};
-			deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest, true);
+			deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest).
+				then(
+					function(response) {
+						var comment = {
+							deliveryServiceRequestId: response.id,
+							value: options.comment
+						};
+						deliveryServiceRequestService.createDeliveryServiceRequestComment(comment).
+							then(
+								function() {
+									messageModel.setMessages([ { level: 'success', text: 'Created request to ' + dsRequest.changeType + ' the ' + dsRequest.deliveryService.xmlId + ' delivery service' } ], true);
+									locationUtils.navigateToPath('/delivery-service-requests');
+								}
+							);
+					}
+				);
 		}, function () {
 			// do nothing
 		});
@@ -62,17 +83,17 @@ var FormEditDeliveryServiceController = function(deliveryService, type, types, $
 		if ($scope.dsRequestsEnabled) {
 			var params = {
 				title: "Delivery Service Update Request",
-				message: 'All delivery service updates must be reviewed for completeness and accuracy before deployment.<br><br>Please select the status of your delivery service update request.'
+				message: 'All delivery service updates must be reviewed for completeness and accuracy before deployment.'
 			};
 			var modalInstance = $uibModal.open({
-				templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
-				controller: 'DialogSelectController',
+				templateUrl: 'common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html',
+				controller: 'DialogDeliveryServiceRequestController',
 				size: 'md',
 				resolve: {
 					params: function () {
 						return params;
 					},
-					collection: function() {
+					statuses: function() {
 						return [
 							{ id: $scope.DRAFT, name: 'Save as Draft' },
 							{ id: $scope.SUBMITTED, name: 'Submit for Review and Deployment' }
@@ -80,13 +101,28 @@ var FormEditDeliveryServiceController = function(deliveryService, type, types, $
 					}
 				}
 			});
-			modalInstance.result.then(function(action) {
+			modalInstance.result.then(function(options) {
 				var dsRequest = {
 					changeType: 'update',
-					status: (action.id == $scope.SUBMITTED) ? 'submitted' : 'draft',
+					status: (options.status.id == $scope.SUBMITTED) ? 'submitted' : 'draft',
 					deliveryService: deliveryService
 				};
-				deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest, true);
+				deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest).
+					then(
+						function(response) {
+							var comment = {
+								deliveryServiceRequestId: response.id,
+								value: options.comment
+							};
+							deliveryServiceRequestService.createDeliveryServiceRequestComment(comment).
+								then(
+									function() {
+										messageModel.setMessages([ { level: 'success', text: 'Created request to ' + dsRequest.changeType + ' the ' + dsRequest.deliveryService.xmlId + ' delivery service' } ], true);
+										locationUtils.navigateToPath('/delivery-service-requests');
+									}
+								);
+						}
+					);
 			}, function () {
 				// do nothing
 			});
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
index f2c8bd9ae..db85015bb 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
@@ -22,11 +22,21 @@
         <ol class="breadcrumb pull-left">
             <li ng-if="!settings.isRequest"><a ng-click="navigateToPath('/delivery-services')">Delivery Services</a></li>
             <li ng-if="settings.isRequest"><a ng-click="navigateToPath('/delivery-service-requests')">Delivery Service Requests</a></li>
-            <li ng-if="settings.isRequest" class="active">{{changeType}}</li>
+            <li ng-if="settings.isRequest" class="active">{{dsRequest.changeType}}</li>
             <li class="active">{{deliveryServiceName}}</li>
         </ol>
         <div class="pull-right" role="group" ng-if="settings.isRequest">
-            <button class="btn btn-link request-status" title="Edit Status" ng-disabled="!open()" ng-click="editStatus()">[ {{requestStatus}} ]</button>
+            <div class="btn-group" role="group" uib-dropdown is-open="status.isopen">
+                <button ng-disabled="dsRequest.status == 'rejected' || dsRequest.status == 'pending' || dsRequest.status == 'complete'" type="button" class="btn btn-link dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+                    {{dsRequest.status | uppercase }}&nbsp;
+                    <span ng-show="dsRequest.status == 'draft' || dsRequest.status == 'submitted'" class="caret"></span>
+                </button>
+                <ul class="dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+                    <li role="menuitem"><a ng-if="dsRequest.status == 'submitted'" ng-click="editStatus('draft')">Change status to: DRAFT</a></li>
+                    <li role="menuitem"><a ng-if="dsRequest.status == 'draft'" ng-click="editStatus('submitted')">Change status to: SUBMITTED</a></li>
+                </ul>
+            </div>
+            <button class="btn btn-primary" title="View Comments" ng-click="viewComments()">View Comments</button>
         </div>
         <div class="pull-right" role="group" ng-show="!settings.isRequest && !settings.isNew">
             <button type="button" class="btn btn-primary" title="Delivery Service Charts" ng-if="showChartsButton" ng-click="openCharts(deliveryService)"><i class="fa fa-bar-chart fa-fw"></i></button>
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
index f7359e0e9..079dbf273 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
@@ -22,11 +22,21 @@
         <ol class="breadcrumb pull-left">
             <li ng-if="!settings.isRequest"><a ng-click="navigateToPath('/delivery-services')">Delivery Services</a></li>
             <li ng-if="settings.isRequest"><a ng-click="navigateToPath('/delivery-service-requests')">Delivery Service Requests</a></li>
-            <li ng-if="settings.isRequest" class="active">{{changeType}}</li>
+            <li ng-if="settings.isRequest" class="active">{{dsRequest.changeType}}</li>
             <li class="active">{{deliveryServiceName}}</li>
         </ol>
         <div class="pull-right" role="group" ng-if="settings.isRequest">
-            <button class="btn btn-link request-status" title="Edit Status" ng-disabled="!open()" ng-click="editStatus()">[ {{requestStatus}} ]</button>
+            <div class="btn-group" role="group" uib-dropdown is-open="status.isopen">
+                <button ng-disabled="dsRequest.status == 'rejected' || dsRequest.status == 'pending' || dsRequest.status == 'complete'" type="button" class="btn btn-link dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+                    {{dsRequest.status | uppercase }}&nbsp;
+                    <span ng-show="dsRequest.status == 'draft' || dsRequest.status == 'submitted'" class="caret"></span>
+                </button>
+                <ul class="dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+                    <li role="menuitem"><a ng-if="dsRequest.status == 'submitted'" ng-click="editStatus('draft')">Change status to: DRAFT</a></li>
+                    <li role="menuitem"><a ng-if="dsRequest.status == 'draft'" ng-click="editStatus('submitted')">Change status to: SUBMITTED</a></li>
+                </ul>
+            </div>
+            <button class="btn btn-primary" title="View Comments" ng-click="viewComments()">View Comments</button>
         </div>
         <div class="pull-right" role="group" ng-show="!settings.isRequest && !settings.isNew">
             <button type="button" class="btn btn-primary" title="Delivery Service Charts" ng-if="showChartsButton" ng-click="openCharts(deliveryService)"><i class="fa fa-bar-chart fa-fw"></i></button>
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.Steering.tpl.html b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.Steering.tpl.html
index 7ce360ed6..bf4cde0a9 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.Steering.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.Steering.tpl.html
@@ -22,11 +22,21 @@
         <ol class="breadcrumb pull-left">
             <li ng-if="!settings.isRequest"><a ng-click="navigateToPath('/delivery-services')">Delivery Services</a></li>
             <li ng-if="settings.isRequest"><a ng-click="navigateToPath('/delivery-service-requests')">Delivery Service Requests</a></li>
-            <li ng-if="settings.isRequest" class="active">{{changeType}}</li>
+            <li ng-if="settings.isRequest" class="active">{{dsRequest.changeType}}</li>
             <li class="active">{{deliveryServiceName}}</li>
         </ol>
         <div class="pull-right" role="group" ng-if="settings.isRequest">
-            <button class="btn btn-link request-status" title="Edit Status" ng-disabled="!open()" ng-click="editStatus()">[ {{requestStatus}} ]</button>
+            <div class="btn-group" role="group" uib-dropdown is-open="status.isopen">
+                <button ng-disabled="dsRequest.status == 'rejected' || dsRequest.status == 'pending' || dsRequest.status == 'complete'" type="button" class="btn btn-link dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+                    {{dsRequest.status | uppercase }}&nbsp;
+                    <span ng-show="dsRequest.status == 'draft' || dsRequest.status == 'submitted'" class="caret"></span>
+                </button>
+                <ul class="dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+                    <li role="menuitem"><a ng-if="dsRequest.status == 'submitted'" ng-click="editStatus('draft')">Change status to: DRAFT</a></li>
+                    <li role="menuitem"><a ng-if="dsRequest.status == 'draft'" ng-click="editStatus('submitted')">Change status to: SUBMITTED</a></li>
+                </ul>
+            </div>
+            <button class="btn btn-primary" title="View Comments" ng-click="viewComments()">View Comments</button>
         </div>
         <div class="pull-right" role="group" ng-show="!settings.isRequest && !settings.isNew">
             <button type="button" class="btn btn-primary" title="Delivery Service Charts" ng-if="showChartsButton" ng-click="openCharts(deliveryService)"><i class="fa fa-bar-chart fa-fw"></i></button>
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
index 6b1daaf5a..27dc56d2f 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
@@ -22,11 +22,21 @@
         <ol class="breadcrumb pull-left">
             <li ng-if="!settings.isRequest"><a ng-click="navigateToPath('/delivery-services')">Delivery Services</a></li>
             <li ng-if="settings.isRequest"><a ng-click="navigateToPath('/delivery-service-requests')">Delivery Service Requests</a></li>
-            <li ng-if="settings.isRequest" class="active">{{changeType}}</li>
+            <li ng-if="settings.isRequest" class="active">{{dsRequest.changeType}}</li>
             <li class="active">{{deliveryServiceName}}</li>
         </ol>
         <div class="pull-right" role="group" ng-if="settings.isRequest">
-            <button class="btn btn-link request-status" title="Edit Status" ng-disabled="!open()" ng-click="editStatus()">[ {{requestStatus}} ]</button>
+            <div class="btn-group" role="group" uib-dropdown is-open="status.isopen">
+                <button ng-disabled="dsRequest.status == 'rejected' || dsRequest.status == 'pending' || dsRequest.status == 'complete'" type="button" class="btn btn-link dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+                    {{dsRequest.status | uppercase }}&nbsp;
+                    <span ng-show="dsRequest.status == 'draft' || dsRequest.status == 'submitted'" class="caret"></span>
+                </button>
+                <ul class="dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+                    <li role="menuitem"><a ng-if="dsRequest.status == 'submitted'" ng-click="editStatus('draft')">Change status to: DRAFT</a></li>
+                    <li role="menuitem"><a ng-if="dsRequest.status == 'draft'" ng-click="editStatus('submitted')">Change status to: SUBMITTED</a></li>
+                </ul>
+            </div>
+            <button class="btn btn-primary" title="View Comments" ng-click="viewComments()">View Comments</button>
         </div>
         <div class="pull-right" role="group" ng-show="!settings.isRequest && !settings.isNew">
             <button type="button" class="btn btn-primary" title="Delivery Service Charts" ng-if="showChartsButton" ng-click="openCharts(deliveryService)"><i class="fa fa-bar-chart fa-fw"></i></button>
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js b/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js
index 4de616119..00fd8a693 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js
@@ -34,17 +34,17 @@ var FormNewDeliveryServiceController = function(deliveryService, type, types, $s
 		if ($scope.dsRequestsEnabled) {
 			var params = {
 				title: "Delivery Service Create Request",
-				message: 'All new delivery services must be reviewed for completeness and accuracy before deployment.<br><br>Please select the status of your delivery service create request.'
+				message: 'All new delivery services must be reviewed for completeness and accuracy before deployment.'
 			};
 			var modalInstance = $uibModal.open({
-				templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
-				controller: 'DialogSelectController',
+				templateUrl: 'common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html',
+				controller: 'DialogDeliveryServiceRequestController',
 				size: 'md',
 				resolve: {
 					params: function () {
 						return params;
 					},
-					collection: function() {
+					statuses: function() {
 						return [
 							{ id: $scope.DRAFT, name: 'Save as Draft' },
 							{ id: $scope.SUBMITTED, name: 'Submit for Review and Deployment' }
@@ -52,13 +52,28 @@ var FormNewDeliveryServiceController = function(deliveryService, type, types, $s
 					}
 				}
 			});
-			modalInstance.result.then(function(action) {
+			modalInstance.result.then(function(options) {
 				var dsRequest = {
 					changeType: 'create',
-					status: (action.id == $scope.SUBMITTED) ? 'submitted' : 'draft',
+					status: (options.status.id == $scope.SUBMITTED) ? 'submitted' : 'draft',
 					deliveryService: deliveryService
 				};
-				deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest, true);
+				deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest).
+					then(
+						function(response) {
+							var comment = {
+								deliveryServiceRequestId: response.id,
+								value: options.comment
+							};
+							deliveryServiceRequestService.createDeliveryServiceRequestComment(comment).
+							then(
+								function() {
+									messageModel.setMessages([ { level: 'success', text: 'Created request to ' + dsRequest.changeType + ' the ' + dsRequest.deliveryService.xmlId + ' delivery service' } ], true);
+									locationUtils.navigateToPath('/delivery-service-requests');
+								}
+							);
+						}
+					);
 			}, function () {
 				// do nothing
 			});
diff --git a/traffic_portal/app/src/common/modules/table/deliveryServiceRequestComments/TableDeliveryServiceRequestCommentsController.js b/traffic_portal/app/src/common/modules/table/deliveryServiceRequestComments/TableDeliveryServiceRequestCommentsController.js
new file mode 100644
index 000000000..f35bc60fa
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/deliveryServiceRequestComments/TableDeliveryServiceRequestCommentsController.js
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var TableDeliveryServicesRequestsController = function(request, comments, $scope, $state, $stateParams, $uibModal, dateUtils, locationUtils, deliveryServiceRequestService, messageModel) {
+
+	$scope.request = request[0];
+
+	$scope.comments = comments;
+
+	$scope.type = $stateParams.type;
+
+	$scope.refresh = function() {
+		$state.reload(); // reloads all the resolves for the view
+	};
+
+	$scope.createComment = function() {
+		var params = {
+			title: 'Add Comment',
+			placeholder: "Enter comment...",
+			text: null
+		};
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/textarea/dialog.textarea.tpl.html',
+			controller: 'DialogTextareaController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
+				}
+			}
+		});
+		modalInstance.result.then(function(commentValue) {
+			var comment = {
+				deliveryServiceRequestId: $scope.request.id,
+				value: commentValue
+			};
+			deliveryServiceRequestService.createDeliveryServiceRequestComment(comment).
+				then(
+					function() {
+						messageModel.setMessages([ { level: 'success', text: 'Delivery service request comment created' } ], false);
+						$scope.refresh();
+					}
+			);
+		}, function () {
+			// do nothing
+		});
+	};
+
+	$scope.editComment = function(comment) {
+		var params = {
+			title: 'Edit Comment',
+			text: comment.value
+		};
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/textarea/dialog.textarea.tpl.html',
+			controller: 'DialogTextareaController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
+				}
+			}
+		});
+		modalInstance.result.then(function(newValue) {
+			var editedComment = {
+				id: comment.id,
+				deliveryServiceRequestId: comment.deliveryServiceRequestId,
+				value: newValue
+			};
+			deliveryServiceRequestService.updateDeliveryServiceRequestComment(editedComment).
+				then(
+					function() {
+						messageModel.setMessages([ { level: 'success', text: 'Delivery service request comment updated' } ], false);
+						$scope.refresh();
+					}
+				);
+		}, function () {
+			// do nothing
+		});
+	};
+
+	$scope.deleteComment = function(comment, $event) {
+		$event.stopPropagation(); // this kills the click event so it doesn't trigger anything else
+		var params = {
+			title: 'Delete Comment',
+			message: 'Are you sure you want to delete this comment?'
+		};
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+			controller: 'DialogConfirmController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
+				}
+			}
+		});
+		modalInstance.result.then(function() {
+			deliveryServiceRequestService.deleteDeliveryServiceRequestComment(comment).
+				then(
+					function() {
+						messageModel.setMessages([ { level: 'success', text: 'Delivery service request comment deleted' } ], false);
+						$scope.refresh();
+					}
+				);
+		}, function () {
+			// do nothing
+		});
+	};
+
+	$scope.getRelativeTime = dateUtils.getRelativeTime;
+
+	$scope.navigateToPath = locationUtils.navigateToPath;
+
+	angular.element(document).ready(function () {
+		$('#dsRequestCommentsTable').dataTable({
+			"aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
+			"iDisplayLength": -1,
+			"ordering": false,
+			"columnDefs": [
+				{ "width": "3%", "targets": 3 }
+			]
+		});
+	});
+
+};
+
+TableDeliveryServicesRequestsController.$inject = ['request', 'comments', '$scope', '$state', '$stateParams', '$uibModal', 'dateUtils', 'locationUtils', 'deliveryServiceRequestService', 'messageModel'];
+module.exports = TableDeliveryServicesRequestsController;
diff --git a/traffic_portal/app/src/common/modules/table/deliveryServiceRequestComments/index.js b/traffic_portal/app/src/common/modules/table/deliveryServiceRequestComments/index.js
new file mode 100644
index 000000000..48adfbaf2
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/deliveryServiceRequestComments/index.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = angular.module('trafficPortal.table.deliveryServiceRequestComments', [])
+	.controller('TableDeliveryServiceRequestCommentsController', require('./TableDeliveryServiceRequestCommentsController'));
diff --git a/traffic_portal/app/src/common/modules/table/deliveryServiceRequestComments/table.deliveryServiceRequestComments.tpl.html b/traffic_portal/app/src/common/modules/table/deliveryServiceRequestComments/table.deliveryServiceRequestComments.tpl.html
new file mode 100644
index 000000000..5bf0c8f13
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/deliveryServiceRequestComments/table.deliveryServiceRequestComments.tpl.html
@@ -0,0 +1,55 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div class="x_panel">
+    <div class="x_title">
+        <ol class="breadcrumb pull-left">
+            <li><a ng-click="navigateToPath('/delivery-service-requests')">Delivery Service Requests</a></li>
+            <li><a ng-click="navigateToPath('/delivery-service-requests/' + request.id + '?type=' + type)">{{::request.deliveryService.xmlId}}</a></li>
+            <li class="active">Comments</li>
+        </ol>
+        <div class="pull-right" role="group">
+            <button class="btn btn-primary" title="Create Comment" ng-click="createComment()"><i class="fa fa-plus"></i></button>
+            <button class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+        </div>
+        <div class="clearfix"></div>
+    </div>
+    <div class="x_content">
+        <br>
+        <table id="dsRequestCommentsTable" class="table responsive-utilities jambo_table">
+            <thead>
+            <tr class="headings">
+                <th>Author</th>
+                <th>Comment</th>
+                <th>Updated</th>
+                <th></th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr ng-click="editComment(c)" ng-repeat="c in ::comments">
+                <td data-search="^{{::c.author}}$">{{::c.author}}</td>
+                <td data-search="^{{::c.comment}}$">{{::c.value}}</td>
+                <td data-search="^{{::getRelativeTime(c.lastUpdated)}}$" data-order="{{::c.lastUpdated}}">{{::getRelativeTime(c.lastUpdated)}}</td>
+                <td><button type="button" class="btn btn-link" title="Delete Comment" ng-click="deleteComment(c, $event)"><i class="fa fa-trash-o"></i></button></td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+
diff --git a/traffic_portal/app/src/common/modules/table/deliveryServiceRequests/TableDeliveryServiceRequestsController.js b/traffic_portal/app/src/common/modules/table/deliveryServiceRequests/TableDeliveryServiceRequestsController.js
index 9bdb96199..1cf88635c 100644
--- a/traffic_portal/app/src/common/modules/table/deliveryServiceRequests/TableDeliveryServiceRequestsController.js
+++ b/traffic_portal/app/src/common/modules/table/deliveryServiceRequests/TableDeliveryServiceRequestsController.js
@@ -17,51 +17,78 @@
  * under the License.
  */
 
-var TableDeliveryServicesRequestsController = function(dsRequests, $scope, $state, $uibModal, $anchorScroll, $q, dateUtils, locationUtils, typeService, deliveryServiceService, deliveryServiceRequestService, messageModel, userModel) {
+var TableDeliveryServicesRequestsController = function(dsRequests, $scope, $state, $uibModal, $anchorScroll, $q, $location, dateUtils, locationUtils, typeService, deliveryServiceService, deliveryServiceRequestService, messageModel, userModel) {
 
 	var createDeliveryServiceDeleteRequest = function(deliveryService) {
 		var params = {
 			title: "Delivery Service Delete Request",
-			message: 'All delivery service deletions must be reviewed.<br><br>Are you sure you want to submit a request to delete the ' + deliveryService.xmlId + ' delivery service?'
+			message: 'All delivery service deletions must be reviewed.'
 		};
 		var modalInstance = $uibModal.open({
-			templateUrl: 'common/modules/dialog/confirm/dialog.confirm.tpl.html',
-			controller: 'DialogConfirmController',
+			templateUrl: 'common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html',
+			controller: 'DialogDeliveryServiceRequestController',
 			size: 'md',
 			resolve: {
 				params: function () {
 					return params;
+				},
+				statuses: function() {
+					return [
+						{ id: $scope.DRAFT, name: 'Save as Draft' },
+						{ id: $scope.SUBMITTED, name: 'Submit for Review and Deployment' }
+					];
 				}
 			}
 		});
-		modalInstance.result.then(function() {
-			params = {
-				title: 'Delete Delivery Service: ' + deliveryService.xmlId,
-				key: deliveryService.xmlId
+		modalInstance.result.then(function(options) {
+			var dsRequest = {
+				changeType: 'delete',
+				status: (options.status.id == $scope.SUBMITTED) ? 'submitted' : 'draft',
+				deliveryService: deliveryService
 			};
-			modalInstance = $uibModal.open({
-				templateUrl: 'common/modules/dialog/delete/dialog.delete.tpl.html',
-				controller: 'DialogDeleteController',
-				size: 'md',
-				resolve: {
-					params: function () {
-						return params;
+			deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest).
+				then(
+					function(response) {
+						var comment = {
+							deliveryServiceRequestId: response.id,
+							value: options.comment
+						};
+						deliveryServiceRequestService.createDeliveryServiceRequestComment(comment).
+							then(
+								function() {
+									messageModel.setMessages([ { level: 'success', text: 'Created request to ' + dsRequest.changeType + ' the ' + dsRequest.deliveryService.xmlId + ' delivery service' } ], false);
+									$scope.refresh();
+								}
+							);
 					}
+				);
+		}, function () {
+			// do nothing
+		});
+	};
+
+	var createComment = function(request, placeholder) {
+		var params = {
+			title: 'Add Comment',
+			placeholder: placeholder,
+			text: null
+		};
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/textarea/dialog.textarea.tpl.html',
+			controller: 'DialogTextareaController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
 				}
-			});
-			modalInstance.result.then(function() {
-				var dsRequest = {
-					changeType: 'delete',
-					status: 'submitted',
-					deliveryService: deliveryService
-				};
-				deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest, false).
-					then(function() {
-						$scope.refresh();
-					});
-			}, function () {
-				// do nothing
-			});
+			}
+		});
+		modalInstance.result.then(function(commentValue) {
+			var comment = {
+				deliveryServiceRequestId: request.id,
+				value: commentValue
+			};
+			deliveryServiceRequestService.createDeliveryServiceRequestComment(comment);
 		}, function () {
 			// do nothing
 		});
@@ -161,6 +188,11 @@ var TableDeliveryServicesRequestsController = function(dsRequests, $scope, $stat
 		});
 	};
 
+	$scope.viewComments = function(request, $event) {
+		$event.stopPropagation(); // this kills the click event so it doesn't trigger anything else
+		$location.path($location.path() + '/' + request.id + '/comments');
+	};
+
 	$scope.rejectRequest = function(request, $event) {
 		$event.stopPropagation(); // this kills the click event so it doesn't trigger anything else
 
@@ -189,6 +221,7 @@ var TableDeliveryServicesRequestsController = function(dsRequests, $scope, $stat
 				then(
 					function() {
 						$scope.refresh();
+						createComment(request, 'Enter rejection reason...');
 					});
 		}, function () {
 			// do nothing
@@ -222,7 +255,8 @@ var TableDeliveryServicesRequestsController = function(dsRequests, $scope, $stat
 			deliveryServiceRequestService.updateDeliveryServiceRequestStatus(request.id, 'complete').
 				then(function() {
 					$scope.refresh();
-				});
+					createComment(request, 'Enter comment...');
+			});
 		}, function () {
 			// do nothing
 		});
@@ -420,5 +454,5 @@ var TableDeliveryServicesRequestsController = function(dsRequests, $scope, $stat
 
 };
 
-TableDeliveryServicesRequestsController.$inject = ['dsRequests', '$scope', '$state', '$uibModal', '$anchorScroll', '$q', 'dateUtils', 'locationUtils', 'typeService', 'deliveryServiceService', 'deliveryServiceRequestService', 'messageModel', 'userModel'];
+TableDeliveryServicesRequestsController.$inject = ['dsRequests', '$scope', '$state', '$uibModal', '$anchorScroll', '$q', '$location', 'dateUtils', 'locationUtils', 'typeService', 'deliveryServiceService', 'deliveryServiceRequestService', 'messageModel', 'userModel'];
 module.exports = TableDeliveryServicesRequestsController;
diff --git a/traffic_portal/app/src/modules/private/deliveryServiceRequests/comments/index.js b/traffic_portal/app/src/modules/private/deliveryServiceRequests/comments/index.js
new file mode 100644
index 000000000..824a3b160
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/deliveryServiceRequests/comments/index.js
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = angular.module('trafficPortal.private.deliveryServiceRequests.comments', [])
+	.config(function($stateProvider, $urlRouterProvider) {
+		$stateProvider
+			.state('trafficPortal.private.deliveryServiceRequests.comments', {
+				url: '/{deliveryServiceRequestId:[0-9]{1,8}}/comments?type',
+				views: {
+					deliveryServiceRequestsContent: {
+						templateUrl: 'common/modules/table/deliveryServiceRequestComments/table.deliveryServiceRequestComments.tpl.html',
+						controller: 'TableDeliveryServiceRequestCommentsController',
+						resolve: {
+							request: function($stateParams, deliveryServiceRequestService) {
+								return deliveryServiceRequestService.getDeliveryServiceRequests({ id: $stateParams.deliveryServiceRequestId });
+							},
+							comments: function($stateParams, deliveryServiceRequestService) {
+								return deliveryServiceRequestService.getDeliveryServiceRequestComments({ deliveryServiceRequestId: $stateParams.deliveryServiceRequestId, orderby: 'id' });
+							}
+						}
+					}
+				}
+			})
+		;
+		$urlRouterProvider.otherwise('/');
+	});
diff --git a/traffic_portal/app/src/modules/private/deliveryServiceRequests/edit/FormEditDeliveryServiceRequestController.js b/traffic_portal/app/src/modules/private/deliveryServiceRequests/edit/FormEditDeliveryServiceRequestController.js
index 18de2448c..d1c19e33e 100644
--- a/traffic_portal/app/src/modules/private/deliveryServiceRequests/edit/FormEditDeliveryServiceRequestController.js
+++ b/traffic_portal/app/src/modules/private/deliveryServiceRequests/edit/FormEditDeliveryServiceRequestController.js
@@ -17,18 +17,18 @@
  * under the License.
  */
 
-var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest, deliveryService, type, types, $scope, $state, $stateParams, $controller, $uibModal, $anchorScroll, $q, locationUtils, deliveryServiceService, deliveryServiceRequestService, messageModel, userModel) {
+var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest, deliveryService, type, types, $scope, $state, $stateParams, $controller, $uibModal, $anchorScroll, $q, $location, locationUtils, deliveryServiceService, deliveryServiceRequestService, messageModel, userModel) {
 
-	var dsRequest = deliveryServiceRequest[0];
+	$scope.dsRequest = deliveryServiceRequest[0];
 		
 	// extends the FormDeliveryServiceController to inherit common methods
-	angular.extend(this, $controller('FormDeliveryServiceController', { deliveryService: dsRequest.deliveryService, dsCurrent: deliveryService, type: type, types: types, $scope: $scope }));
+	angular.extend(this, $controller('FormDeliveryServiceController', { deliveryService: $scope.dsRequest.deliveryService, dsCurrent: deliveryService, type: type, types: types, $scope: $scope }));
 
-	$scope.changeType = dsRequest.changeType;
+	$scope.changeType = $scope.dsRequest.changeType;
 
-	$scope.requestStatus = dsRequest.status;
+	$scope.requestStatus = $scope.dsRequest.status;
 
-	$scope.deliveryServiceName = angular.copy(dsRequest.deliveryService.xmlId);
+	$scope.deliveryServiceName = angular.copy($scope.dsRequest.deliveryService.xmlId);
 
 	$scope.advancedShowing = true;
 
@@ -40,19 +40,19 @@ var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest,
 	};
 
 	$scope.saveable = function() {
-		return (dsRequest.status == 'draft' || dsRequest.status == 'submitted');
+		return ($scope.dsRequest.status == 'draft' || $scope.dsRequest.status == 'submitted');
 	};
 
 	$scope.deletable = function() {
-		return (dsRequest.status == 'draft' || dsRequest.status == 'submitted');
+		return ($scope.dsRequest.status == 'draft' || $scope.dsRequest.status == 'submitted');
 	};
 
 	$scope.fulfillable = function() {
-		return dsRequest.status == 'submitted';
+		return $scope.dsRequest.status == 'submitted';
 	};
 
 	$scope.open = function() {
-		return (dsRequest.status == 'draft' || dsRequest.status == 'submitted' || dsRequest.status == 'pending');
+		return ($scope.dsRequest.status == 'draft' || $scope.dsRequest.status == 'submitted' || $scope.dsRequest.status == 'pending');
 	};
 
 	$scope.magicNumberLabel = function(collection, magicNumber) {
@@ -60,58 +60,30 @@ var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest,
 		return item.label;
 	};
 
-	$scope.editStatus = function() {
+	$scope.viewComments = function() {
+		$location.path($location.path() + '/comments');
+	};
+
+	$scope.editStatus = function(status) {
 		var params = {
-			title: "Edit Delivery Service Request Status",
-			message: 'Please select the appropriate status for this request.'
+			title: 'Change Delivery Service Request Status',
+			message: "Are you sure you want to change the status of the delivery service request to '" + status + "'?"
 		};
 		var modalInstance = $uibModal.open({
-			templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
-			controller: 'DialogSelectController',
+			templateUrl: 'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+			controller: 'DialogConfirmController',
 			size: 'md',
 			resolve: {
 				params: function () {
 					return params;
-				},
-				collection: function() {
-					var statuses = [];
-					if (dsRequest.status == 'draft' || dsRequest.status == 'submitted') {
-						statuses.push({ id: $scope.DRAFT, name: 'Save as Draft' });
-						statuses.push({ id: $scope.SUBMITTED, name: 'Submit for Review / Deployment' });
-					} else if (dsRequest.status == 'pending') {
-						statuses.push({ id: $scope.COMPLETE, name: 'Complete' });
-					}
-					return statuses;
 				}
 			}
 		});
-		modalInstance.result.then(function(action) {
-			switch (action.id) {
-				case $scope.DRAFT:
-					dsRequest.status = 'draft';
-					deliveryServiceRequestService.updateDeliveryServiceRequestStatus(dsRequest.id, 'draft').
-						then(function() {
-							$state.reload();
-						});
-					break;
-				case $scope.SUBMITTED:
-					dsRequest.status = 'submitted';
-					deliveryServiceRequestService.updateDeliveryServiceRequestStatus(dsRequest.id, 'submitted').
-						then(function() {
-							$state.reload();
-						});
-					break;
-				case $scope.COMPLETE:
-					if (dsRequest.assigneeId != userModel.user.id) {
-						messageModel.setMessages([ { level: 'error', text: 'Only the assignee can mark a delivery service request as complete' } ], false);
-						$anchorScroll(); // scrolls window to top
-						return;
-					}
-					deliveryServiceRequestService.updateDeliveryServiceRequestStatus(dsRequest.id, 'complete').
-						then(function() {
-							$state.reload();
-						});
-			}
+		modalInstance.result.then(function() {
+			deliveryServiceRequestService.updateDeliveryServiceRequestStatus($scope.dsRequest.id, status).
+				then(function() {
+					$state.reload();
+				});
 		}, function () {
 			// do nothing
 		});
@@ -121,12 +93,12 @@ var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest,
 		var promises = [];
 		// update the ds request if the ds request actually changed
 		if ($scope.deliveryServiceForm.$dirty) {
-			promises.push(deliveryServiceRequestService.updateDeliveryServiceRequest(dsRequest.id, dsRequest));
+			promises.push(deliveryServiceRequestService.updateDeliveryServiceRequest($scope.dsRequest.id, $scope.dsRequest));
 		}
 		// make sure the ds request is assigned to the user that is fulfilling the request
-		promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(dsRequest.id, userModel.user.id));
+		promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest($scope.dsRequest.id, userModel.user.id));
 		// set the status to 'pending'
-		promises.push(deliveryServiceRequestService.updateDeliveryServiceRequestStatus(dsRequest.id, 'pending'));
+		promises.push(deliveryServiceRequestService.updateDeliveryServiceRequestStatus($scope.dsRequest.id, 'pending'));
 	};
 
 	$scope.fulfillRequest = function(ds) {
@@ -159,7 +131,7 @@ var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest,
 							$anchorScroll(); // scrolls window to top
 							messageModel.setMessages(fault.data.alerts, false);
 						}
-				);
+					);
 			} else if ($scope.changeType == 'update') {
 				deliveryServiceService.updateDeliveryService(ds).
 					then(
@@ -213,18 +185,19 @@ var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest,
 
 	$scope.save = function(deliveryService) {
 		var params = {
-			title: 'Delivery Service Request Status',
-			message: 'Please select the status of your delivery service request.'
+			title: 'Update Delivery Service Request',
+			statusMessage: 'Please select the status of your delivery service request.',
+			commentMessage: 'Why is this request being changed?'
 		};
 		var modalInstance = $uibModal.open({
-			templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
-			controller: 'DialogSelectController',
+			templateUrl: 'common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html',
+			controller: 'DialogDeliveryServiceRequestController',
 			size: 'md',
 			resolve: {
 				params: function () {
 					return params;
 				},
-				collection: function() {
+				statuses: function() {
 					return [
 						{ id: $scope.DRAFT, name: 'Save as Draft' },
 						{ id: $scope.SUBMITTED, name: 'Submit for Review / Deployment' }
@@ -232,15 +205,27 @@ var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest,
 				}
 			}
 		});
-		modalInstance.result.then(function(action) {
-			dsRequest.status = (action.id == $scope.SUBMITTED) ? 'submitted' : 'draft';
-			dsRequest.deliveryService = deliveryService;
-			deliveryServiceRequestService.updateDeliveryServiceRequest(dsRequest.id, dsRequest).
-				then(function() {
-					messageModel.setMessages([ { level: 'success', text: 'Updated delivery service request for ' + dsRequest.deliveryService.xmlId + ' and set status to ' + dsRequest.status } ], false);
-					$anchorScroll(); // scrolls window to top
-					$state.reload();
-				});
+		modalInstance.result.then(function(options) {
+			$scope.dsRequest.status = (options.status.id == $scope.SUBMITTED) ? 'submitted' : 'draft';
+			$scope.dsRequest.deliveryService = deliveryService;
+
+			deliveryServiceRequestService.updateDeliveryServiceRequest($scope.dsRequest.id, $scope.dsRequest).
+				then(
+					function() {
+						var comment = {
+							deliveryServiceRequestId: $scope.dsRequest.id,
+							value: options.comment
+						};
+						deliveryServiceRequestService.createDeliveryServiceRequestComment(comment).
+							then(
+								function() {
+									messageModel.setMessages([ { level: 'success', text: 'Updated delivery service request for ' + $scope.dsRequest.deliveryService.xmlId + ' and set status to ' + $scope.dsRequest.status } ], false);
+									$anchorScroll(); // scrolls window to top
+									$state.reload();
+								}
+							);
+					}
+				);
 		}, function () {
 			// do nothing
 		});
@@ -248,7 +233,7 @@ var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest,
 
 	$scope.confirmDelete = function(deliveryService) {
 		var params = {
-			title: 'Delete ' + deliveryService.xmlId + ' ' + dsRequest.changeType + ' request?',
+			title: 'Delete ' + deliveryService.xmlId + ' ' + $scope.dsRequest.changeType + ' request?',
 			key: deliveryService.xmlId + ' request'
 		};
 		var modalInstance = $uibModal.open({
@@ -270,5 +255,5 @@ var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest,
 
 };
 
-FormEditDeliveryServiceRequestController.$inject = ['deliveryServiceRequest', 'deliveryService', 'type', 'types', '$scope', '$state', '$stateParams', '$controller', '$uibModal', '$anchorScroll', '$q', 'locationUtils', 'deliveryServiceService', 'deliveryServiceRequestService', 'messageModel', 'userModel'];
+FormEditDeliveryServiceRequestController.$inject = ['deliveryServiceRequest', 'deliveryService', 'type', 'types', '$scope', '$state', '$stateParams', '$controller', '$uibModal', '$anchorScroll', '$q', '$location', 'locationUtils', 'deliveryServiceService', 'deliveryServiceRequestService', 'messageModel', 'userModel'];
 module.exports = FormEditDeliveryServiceRequestController;


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services