This is an automated email from the ASF dual-hosted git repository.

srijeet0406 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 451827fc29 Fixes Phys_Location V5 apis to respond with RFC3339 
date/time Format (#7631)
451827fc29 is described below

commit 451827fc29eed72d99b17176cd9a94c30948dbe1
Author: Jagan Parthiban <[email protected]>
AuthorDate: Fri Jul 21 23:36:36 2023 +0530

    Fixes Phys_Location V5 apis to respond with RFC3339 date/time Format (#7631)
    
    * Fixes Phys_location RFC3339 issue 
https://github.com/apache/trafficcontrol/issues/7630
    
    * Integration Test Case updates for  
https://github.com/apache/trafficcontrol/issues/7630
    
    * Documentation updates for  
https://github.com/apache/trafficcontrol/issues/7630
    
    * CHANGELOG.md updates for  
https://github.com/apache/trafficcontrol/issues/7630
    
    * PR Review comment Updates for  
https://github.com/apache/trafficcontrol/issues/7630
    
    * PR Review comment Updates for  
https://github.com/apache/trafficcontrol/issues/7630
    
    * PR Review comment Updates for  
https://github.com/apache/trafficcontrol/issues/7630
    
    * PR Review comment Updates for  
https://github.com/apache/trafficcontrol/issues/7630
    
    * PR Review comment Updates for  
https://github.com/apache/trafficcontrol/issues/7630
---
 CHANGELOG.md                                       |   1 +
 docs/source/api/v5/phys_locations.rst              |   8 +-
 docs/source/api/v5/phys_locations_id.rst           |   4 +-
 lib/go-tc/physlocations.go                         |  63 ++++
 traffic_ops/testing/api/v5/phys_locations_test.go  |  22 +-
 traffic_ops/testing/api/v5/traffic_control_test.go |   2 +-
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |  15 +
 .../physlocation/phys_locations.go                 | 339 +++++++++++++++++++++
 traffic_ops/traffic_ops_golang/routing/routes.go   |   8 +-
 traffic_ops/v5-client/phys_location.go             |   8 +-
 10 files changed, 444 insertions(+), 26 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51cc40ac2c..55f97ce1e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ### Fixed
 - [#4393](https://github.com/apache/trafficcontrol/issues/4393) *Traffic Ops* 
Fixed the error code and alert structure when TO is queried for a delivery 
service with no ssl keys.
+- [#7631] (https://github.com/apache/trafficcontrol/pull/7631) *Traffic Ops* 
Fixes Phys_Location V5 apis to respond with RFC3339 date/time Format
 - [#7623] (https://github.com/apache/trafficcontrol/pull/7623) *Traffic Ops* 
Removed TryIfModifiedSinceQuery from servicecategories.go and reused from ims.go
 - [#7608](https://github.com/apache/trafficcontrol/pull/7608) *Traffic 
Monitor* Use stats_over_http(plugin.system_stats.timestamp_ms) timestamp field 
to calculate bandwidth for TM's caches.
 - [#7612](https://github.com/apache/trafficcontrol/pull/7612) *Traffic Ops* 
Fixes Divisions V5 apis to respond with RFC3339 date/time Format
diff --git a/docs/source/api/v5/phys_locations.rst 
b/docs/source/api/v5/phys_locations.rst
index 9aae8be2b8..08cb486d04 100644
--- a/docs/source/api/v5/phys_locations.rst
+++ b/docs/source/api/v5/phys_locations.rst
@@ -71,7 +71,7 @@ Response Structure
 :comments:    Any and all human-readable comments
 :email:       The email address of the physical location's ``poc``
 :id:          An integral, unique identifier for the physical location
-:lastUpdated: The date and time at which the physical location was last 
updated, in :ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which the physical location was last 
updated, in :rfc:`3339` Format
 :name:        The name of the physical location
 :phone:       A phone number where the the physical location's ``poc`` might 
be reached
 :poc:         The name of a "point of contact" for the physical location
@@ -103,7 +103,7 @@ Response Structure
                        "comments": "",
                        "email": "",
                        "id": 2,
-                       "lastUpdated": "2018-12-05 17:50:58+00",
+                       "lastUpdated": "2018-12-05T18:56:27.057163+05:30",
                        "name": "CDN_in_a_Box",
                        "phone": "",
                        "poc": "",
@@ -173,7 +173,7 @@ Response Structure
 :comments:    Any and all human-readable comments
 :email:       The email address of the physical location's ``poc``
 :id:          An integral, unique identifier for the physical location
-:lastUpdated: The date and time at which the physical location was last 
updated, in :ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which the physical location was last 
updated, in :rfc:`3339`
 :name:        The name of the physical location
 :phone:       A phone number where the the physical location's ``poc`` might 
be reached
 :poc:         The name of a "point of contact" for the physical location
@@ -210,7 +210,7 @@ Response Structure
                "comments": "Buckingham Palace",
                "email": "[email protected]",
                "id": 3,
-               "lastUpdated": "2018-12-06 00:14:47+00",
+               "lastUpdated": "2018-12-06T18:56:27.057163+05:30",
                "name": "Great_Britain",
                "phone": "0-843-816-6276",
                "poc": "Her Majesty The Queen Elizabeth Alexandra Mary Windsor 
II",
diff --git a/docs/source/api/v5/phys_locations_id.rst 
b/docs/source/api/v5/phys_locations_id.rst
index b945bf2177..bdeda10bb0 100644
--- a/docs/source/api/v5/phys_locations_id.rst
+++ b/docs/source/api/v5/phys_locations_id.rst
@@ -85,7 +85,7 @@ Response Structure
 :comments:    Any and all human-readable comments
 :email:       The email address of the physical location's ``poc``
 :id:          An integral, unique identifier for the physical location
-:lastUpdated: The date and time at which the physical location was last 
updated, in :ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which the physical location was last 
updated, in :rfc:`3339`
 :name:        The name of the physical location
 :phone:       A phone number where the the physical location's ``poc`` might 
be reached
 :poc:         The name of a "point of contact" for the physical location
@@ -122,7 +122,7 @@ Response Structure
                "comments": "The White House",
                "email": "[email protected]",
                "id": 2,
-               "lastUpdated": "2018-12-05 23:39:17+00",
+               "lastUpdated": "2018-12-05T18:56:27.057163+05:30",
                "name": "CDN_in_a_Box",
                "phone": "1-202-456-1414",
                "poc": "Donald J. Trump",
diff --git a/lib/go-tc/physlocations.go b/lib/go-tc/physlocations.go
index a238067709..3dcd218cba 100644
--- a/lib/go-tc/physlocations.go
+++ b/lib/go-tc/physlocations.go
@@ -19,6 +19,8 @@ package tc
  * under the License.
  */
 
+import "time"
+
 // PhysLocationsResponse is a list of PhysLocations as a response.
 type PhysLocationsResponse struct {
        Response []PhysLocation `json:"response"`
@@ -201,3 +203,64 @@ type PhysLocationNullable struct {
 type PhysLocationTrimmed struct {
        Name string `json:"name"`
 }
+
+// PhysLocationV5 is an alias for the latest minor version for the major 
version 5.
+type PhysLocationV5 PhysLocationV50
+
+// PhysLocationV50 contains the physical location of a cache group.
+type PhysLocationV50 struct {
+       Address     string    `json:"address" db:"address"`
+       City        string    `json:"city" db:"city"`
+       Comments    string    `json:"comments" db:"comments"`
+       Email       string    `json:"email" db:"email"`
+       ID          int       `json:"id" db:"id"`
+       LastUpdated time.Time `json:"lastUpdated" db:"last_updated"`
+       Name        string    `json:"name" db:"name"`
+       Phone       string    `json:"phone" db:"phone"`
+       POC         string    `json:"poc" db:"poc"`
+       RegionID    int       `json:"regionId" db:"region"`
+       RegionName  string    `json:"region" db:"region_name"`
+       ShortName   string    `json:"shortName" db:"short_name"`
+       State       string    `json:"state" db:"state"`
+       Zip         string    `json:"zip" db:"zip"`
+}
+
+// PhysLocationsResponseV5 is an alias for the latest minor version for the 
major version 5.
+type PhysLocationsResponseV5 PhysLocationsResponseV50
+
+// PhysLocationsResponseV50 is a list of PhysLocations as a response.
+type PhysLocationsResponseV50 struct {
+       Response []PhysLocationV5 `json:"response"`
+       Alerts
+}
+
+// PhysLocationNullableV5 is an alias for the latest minor version for the 
major version 5.
+type PhysLocationNullableV5 PhysLocationNullableV50
+
+// PhysLocationNullableV50 contains the physical location of a cache group. It
+// allows for all fields to be null.
+type PhysLocationNullableV50 struct {
+       Address     *string   `json:"address" db:"address"`
+       City        *string   `json:"city" db:"city"`
+       Comments    *string   `json:"comments" db:"comments"`
+       Email       *string   `json:"email" db:"email"`
+       ID          *int      `json:"id" db:"id"`
+       LastUpdated time.Time `json:"lastUpdated" db:"last_updated"`
+       Name        *string   `json:"name" db:"name"`
+       Phone       *string   `json:"phone" db:"phone"`
+       POC         *string   `json:"poc" db:"poc"`
+       RegionID    *int      `json:"regionId" db:"region"`
+       RegionName  *string   `json:"region" db:"region_name"`
+       ShortName   *string   `json:"shortName" db:"short_name"`
+       State       *string   `json:"state" db:"state"`
+       Zip         *string   `json:"zip" db:"zip"`
+}
+
+// PhysLocationResponseV5 is an alias for the latest minor version for the 
major version 5.
+type PhysLocationResponseV5 PhysLocationResponseV50
+
+// PhysLocationResponseV50 is a single PhysLocationNullable as a response.
+type PhysLocationResponseV50 struct {
+       Response PhysLocationNullableV5 `json:"response"`
+       Alerts
+}
diff --git a/traffic_ops/testing/api/v5/phys_locations_test.go 
b/traffic_ops/testing/api/v5/phys_locations_test.go
index f1f70ddab7..ae6f3b03a8 100644
--- a/traffic_ops/testing/api/v5/phys_locations_test.go
+++ b/traffic_ops/testing/api/v5/phys_locations_test.go
@@ -38,7 +38,7 @@ func TestPhysLocations(t *testing.T) {
                currentTimeRFC := currentTime.Format(time.RFC1123)
                tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
 
-               methodTests := utils.TestCase[client.Session, 
client.RequestOptions, tc.PhysLocation]{
+               methodTests := utils.TestCase[client.Session, 
client.RequestOptions, tc.PhysLocationV5]{
                        "GET": {
                                "NOT MODIFIED when NO CHANGES made": {
                                        ClientSession: TOSession,
@@ -99,7 +99,7 @@ func TestPhysLocations(t *testing.T) {
                        "POST": {
                                "OK when VALID request": {
                                        ClientSession: TOSession,
-                                       RequestBody: tc.PhysLocation{
+                                       RequestBody: tc.PhysLocationV5{
                                                Address:    "100 blah lane",
                                                City:       "foo",
                                                Comments:   "comment",
@@ -112,12 +112,12 @@ func TestPhysLocations(t *testing.T) {
                                                State:      "CO",
                                                Zip:        "80602",
                                        },
-                                       Expectations: 
utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
+                                       Expectations: 
utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusCreated),
                                                
validatePhysicalLocationUpdateCreateFields("testPhysicalLocation", 
map[string]interface{}{"Name": "testPhysicalLocation"})),
                                },
                                "BAD REQUEST when REGION ID does NOT MATCH 
REGION NAME": {
                                        ClientSession: TOSession,
-                                       RequestBody: tc.PhysLocation{
+                                       RequestBody: tc.PhysLocationV5{
                                                Address:    "1234 southern way",
                                                City:       "Atlanta",
                                                Name:       "HotAtlanta",
@@ -135,7 +135,7 @@ func TestPhysLocations(t *testing.T) {
                                "OK when VALID request": {
                                        EndpointID:    GetPhysicalLocationID(t, 
"HotAtlanta"),
                                        ClientSession: TOSession,
-                                       RequestBody: tc.PhysLocation{
+                                       RequestBody: tc.PhysLocationV5{
                                                Address:   "1234 southern way",
                                                City:      "NewCity",
                                                Name:      "HotAtlanta",
@@ -152,7 +152,7 @@ func TestPhysLocations(t *testing.T) {
                                        EndpointID:    GetPhysicalLocationID(t, 
"HotAtlanta"),
                                        ClientSession: TOSession,
                                        RequestOpts:   
client.RequestOptions{Header: http.Header{rfc.IfUnmodifiedSince: 
{currentTimeRFC}}},
-                                       RequestBody: tc.PhysLocation{
+                                       RequestBody: tc.PhysLocationV5{
                                                Address:   "1234 southern way",
                                                City:      "Atlanta",
                                                RegionID:  GetRegionID(t, 
"region1")(),
@@ -166,7 +166,7 @@ func TestPhysLocations(t *testing.T) {
                                "PRECONDITION FAILED when updating with IFMATCH 
ETAG Header": {
                                        EndpointID:    GetPhysicalLocationID(t, 
"HotAtlanta"),
                                        ClientSession: TOSession,
-                                       RequestBody: tc.PhysLocation{
+                                       RequestBody: tc.PhysLocationV5{
                                                Address:   "1234 southern way",
                                                City:      "Atlanta",
                                                RegionID:  GetRegionID(t, 
"region1")(),
@@ -230,7 +230,7 @@ func TestPhysLocations(t *testing.T) {
 func validatePhysicalLocationFields(expectedResp map[string]interface{}) 
utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ 
tc.Alerts, _ error) {
                assert.RequireNotNil(t, resp, "Expected Physical Location 
response to not be nil.")
-               plResp := resp.([]tc.PhysLocation)
+               plResp := resp.([]tc.PhysLocationV5)
                for field, expected := range expectedResp {
                        for _, pl := range plResp {
                                switch field {
@@ -259,7 +259,7 @@ func validatePhysicalLocationUpdateCreateFields(name 
string, expectedResp map[st
 
 func validatePhysicalLocationPagination(paginationParam string) 
utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ 
tc.Alerts, _ error) {
-               paginationResp := resp.([]tc.PhysLocation)
+               paginationResp := resp.([]tc.PhysLocationV5)
 
                opts := client.NewRequestOptions()
                opts.QueryParameters.Set("orderby", "id")
@@ -283,7 +283,7 @@ func validatePhysicalLocationSort() utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, 
alerts tc.Alerts, _ error) {
                assert.RequireNotNil(t, resp, "Expected Physical Location 
response to not be nil.")
                var physLocNames []string
-               physLocResp := resp.([]tc.PhysLocation)
+               physLocResp := resp.([]tc.PhysLocationV5)
                for _, pl := range physLocResp {
                        physLocNames = append(physLocNames, pl.Name)
                }
@@ -295,7 +295,7 @@ func validatePhysicalLocationIDSort() utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, 
alerts tc.Alerts, _ error) {
                assert.RequireNotNil(t, resp, "Expected Physical Location 
response to not be nil.")
                var physLocIDs []int
-               physLocResp := resp.([]tc.PhysLocation)
+               physLocResp := resp.([]tc.PhysLocationV5)
                for _, pl := range physLocResp {
                        physLocIDs = append(physLocIDs, pl.ID)
                }
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go 
b/traffic_ops/testing/api/v5/traffic_control_test.go
index c99480ebdb..2d514b594b 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -42,7 +42,7 @@ type TrafficControl struct {
        Profiles                                          []tc.Profile          
                  `json:"profiles"`
        Parameters                                        []tc.Parameter        
                  `json:"parameters"`
        ProfileParameters                                 []tc.ProfileParameter 
                  `json:"profileParameters"`
-       PhysLocations                                     []tc.PhysLocation     
                  `json:"physLocations"`
+       PhysLocations                                     []tc.PhysLocationV5   
                  `json:"physLocations"`
        Regions                                           []tc.Region           
                  `json:"regions"`
        Roles                                             []tc.RoleV4           
                  `json:"roles"`
        Servers                                           []tc.ServerV4         
                  `json:"servers"`
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go 
b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index dde6815757..94f026b800 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -2232,3 +2232,18 @@ func DivisionExists(tx *sql.Tx, id string) (bool, error) 
{
        }
        return true, nil
 }
+
+// PhysLocationExists confirms whether the PhysLocationExists exists, and an 
error (if one occurs).
+func PhysLocationExists(tx *sql.Tx, id string) (bool, error) {
+       var count int
+       if err := tx.QueryRow("SELECT count(name) FROM phys_location WHERE 
id=$1", id).Scan(&count); err != nil {
+               return false, fmt.Errorf("error getting PhysLocation info: %w", 
err)
+       }
+       if count == 0 {
+               return false, nil
+       }
+       if count != 1 {
+               return false, fmt.Errorf("getting PhysLocation info - expected 
row count: 1, actual: %d", count)
+       }
+       return true, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/physlocation/phys_locations.go 
b/traffic_ops/traffic_ops_golang/physlocation/phys_locations.go
index 81e315931f..08770fb52e 100644
--- a/traffic_ops/traffic_ops_golang/physlocation/phys_locations.go
+++ b/traffic_ops/traffic_ops_golang/physlocation/phys_locations.go
@@ -20,17 +20,21 @@ package physlocation
  */
 
 import (
+       "database/sql"
+       "encoding/json"
        "errors"
        "fmt"
        "net/http"
        "strconv"
        "time"
 
+       "github.com/apache/trafficcontrol/lib/go-log"
        "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
        "github.com/apache/trafficcontrol/lib/go-util"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
 
        validation "github.com/go-ozzo/ozzo-validation"
 )
@@ -215,3 +219,338 @@ func deleteQuery() string {
 WHERE id=:id`
        return query
 }
+
+func GetPhysLocation(w http.ResponseWriter, r *http.Request) {
+       var runSecond bool
+       var maxTime time.Time
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       tx := inf.Tx
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       // Query Parameters to Database Query column mappings
+       queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+               "name":   {Column: "pl.name", Checker: nil},
+               "id":     {Column: "pl.id", Checker: api.IsInt},
+               "region": {Column: "pl.region", Checker: api.IsInt},
+       }
+       if _, ok := inf.Params["orderby"]; !ok {
+               inf.Params["orderby"] = "name"
+       }
+       where, orderBy, pagination, queryValues, errs := 
dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols)
+       if len(errs) > 0 {
+               api.HandleErr(w, r, tx.Tx, http.StatusBadRequest, 
util.JoinErrs(errs), nil)
+               return
+       }
+
+       if inf.Config.UseIMS {
+               runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, r.Header, 
queryValues, selectMaxLastUpdatedQuery(where))
+               if !runSecond {
+                       log.Debugln("IMS HIT")
+                       api.AddLastModifiedHdr(w, maxTime)
+                       w.WriteHeader(http.StatusNotModified)
+                       return
+               }
+               log.Debugln("IMS MISS")
+       } else {
+               log.Debugln("Non IMS request")
+       }
+
+       query := selectQuery() + where + orderBy + pagination
+       rows, err := tx.NamedQuery(query, queryValues)
+       if err != nil {
+               api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, 
fmt.Errorf("Phy_Location read: error getting Phy_Location(s): %w", err))
+               return
+       }
+       defer log.Close(rows, "unable to close DB connection")
+
+       physLocation := tc.PhysLocationNullableV5{}
+       physLocationList := []tc.PhysLocationNullableV5{}
+       for rows.Next() {
+               if err = rows.Scan(&physLocation.Address, &physLocation.City, 
&physLocation.Comments, &physLocation.Email, &physLocation.ID, 
&physLocation.LastUpdated, &physLocation.Name, &physLocation.Phone, 
&physLocation.POC, &physLocation.RegionID, &physLocation.RegionName, 
&physLocation.ShortName, &physLocation.State, &physLocation.Zip); err != nil {
+                       api.HandleErr(w, r, tx.Tx, 
http.StatusInternalServerError, nil, fmt.Errorf("error getting physLocation(s): 
%w", err))
+                       return
+               }
+               physLocationList = append(physLocationList, physLocation)
+       }
+
+       api.WriteResp(w, r, physLocationList)
+       return
+}
+
+func CreatePhysLocation(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+       tx := inf.Tx.Tx
+
+       physLocation, readValErr := readAndValidateJsonStruct(r)
+       if readValErr != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+               return
+       }
+
+       // checks to see if the supplied region name and ID in the 
phys_location body correspond to each other.
+       if physLocation.RegionName != "" {
+               regionName, ok, err := dbhelpers.GetRegionNameFromID(tx, 
physLocation.RegionID)
+               if err != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("error fetching name from region ID: %w", err), nil)
+                       return
+               } else if !ok {
+                       api.HandleErr(w, r, tx, http.StatusNotFound, 
errors.New("no such region"), nil)
+                       return
+               }
+               if regionName != physLocation.RegionName {
+                       api.HandleErr(w, r, tx, http.StatusBadRequest, 
errors.New("region name and ID do not match"), nil)
+                       return
+               }
+       }
+
+       // check if phys_location already exists
+       var count int
+       err := tx.QueryRow("SELECT count(*) from phys_location where name = 
$1", physLocation.Name).Scan(&count)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
fmt.Errorf("error: %w, when checking if physLocation with name %s exists", err, 
physLocation.Name))
+               return
+       }
+       if count == 1 {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, 
fmt.Errorf("physLocation name '%s' already exists", physLocation.Name), nil)
+               return
+       }
+
+       // create phys_location
+       query := `
+       INSERT INTO phys_location (
+               address, 
+               city, 
+               comments, 
+               email, 
+               name, 
+               phone, 
+               poc, 
+               region, 
+               short_name, 
+               state, 
+               zip
+       ) VALUES (
+               $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
+       ) RETURNING id, last_updated
+`
+       err = tx.QueryRow(
+               query,
+               physLocation.Address,
+               physLocation.City,
+               physLocation.Comments,
+               physLocation.Email,
+               physLocation.Name,
+               physLocation.Phone,
+               physLocation.POC,
+               physLocation.RegionID,
+               physLocation.ShortName,
+               physLocation.State,
+               physLocation.Zip,
+       ).Scan(
+               &physLocation.ID,
+               &physLocation.LastUpdated,
+       )
+
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("error: %w in physLocation  with name: %s", err, physLocation.Name), 
nil)
+                       return
+               }
+               usrErr, sysErr, code := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, code, usrErr, sysErr)
+               return
+       }
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "physLocation was created.")
+       w.Header().Set("Location", 
fmt.Sprintf("/api/%d.%d/phys_locations?name=%s", inf.Version.Major, 
inf.Version.Minor, physLocation.Name))
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, physLocation)
+       return
+}
+
+func UpdatePhysLocation(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       tx := inf.Tx.Tx
+       physLocation, readValErr := readAndValidateJsonStruct(r)
+       if readValErr != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+               return
+       }
+
+       // checks to see if the supplied region name and ID in the 
phys_location body correspond to each other.
+       if physLocation.RegionName != "" {
+               regionName, ok, err := dbhelpers.GetRegionNameFromID(tx, 
physLocation.RegionID)
+               if err != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("error fetching name from region ID: %w", err), nil)
+                       return
+               } else if !ok {
+                       api.HandleErr(w, r, tx, http.StatusNotFound, 
errors.New("no such region"), nil)
+                       return
+               }
+               if regionName != physLocation.RegionName {
+                       api.HandleErr(w, r, tx, http.StatusBadRequest, 
errors.New("region name and ID do not match"), nil)
+                       return
+               }
+       }
+
+       requestedID := inf.Params["id"]
+
+       intRequestId, convErr := strconv.Atoi(requestedID)
+       if convErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, 
fmt.Errorf("physLocation update error: %w, while converting from string to 
int", convErr), nil)
+       }
+       // check if the entity was already updated
+       userErr, sysErr, errCode = api.CheckIfUnModified(r.Header, inf.Tx, 
intRequestId, "phys_location")
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+
+       //update name and description of a phys_location
+       query := `
+       UPDATE phys_location pl
+       SET
+               address = $1,
+               city = $2,
+               comments = $3,
+               email = $4,
+               name = $5,
+               phone = $6,
+               poc = $7,
+               region = $8,
+               short_name = $9,
+               state = $10,
+               zip = $11
+       WHERE
+               pl.id = $12
+       RETURNING
+               pl.id, pl.last_updated
+`
+
+       err := tx.QueryRow(
+               query,
+               physLocation.Address,
+               physLocation.City,
+               physLocation.Comments,
+               physLocation.Email,
+               physLocation.Name,
+               physLocation.Phone,
+               physLocation.POC,
+               physLocation.RegionID,
+               physLocation.ShortName,
+               physLocation.State,
+               physLocation.Zip,
+               requestedID,
+       ).Scan(
+               &physLocation.ID,
+               &physLocation.LastUpdated,
+       )
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       api.HandleErr(w, r, tx, http.StatusBadRequest, 
fmt.Errorf("physLocation with ID: %v not found", physLocation.ID), nil)
+                       return
+               }
+               usrErr, sysErr, code := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, code, usrErr, sysErr)
+               return
+       }
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "physLocation was updated")
+       api.WriteAlertsObj(w, r, http.StatusOK, alerts, physLocation)
+       return
+}
+
+func DeletePhysLocation(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       tx := inf.Tx.Tx
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       id := inf.Params["id"]
+       if id == "" {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, 
fmt.Errorf("couldn't delete Phys_Location. Invalid ID. Id Cannot be empty for 
Delete Operation"), nil)
+               return
+       }
+       exists, err := dbhelpers.PhysLocationExists(tx, id)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
err)
+               return
+       }
+       if !exists {
+               api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("no 
PhysLocation exists by id: %s", id), nil)
+               return
+       }
+
+       assignedServer := 0
+       if err := inf.Tx.Get(&assignedServer, "SELECT count(id) FROM server sv 
WHERE sv.phys_location=$1", id); err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
fmt.Errorf("phys_location delete error, could not count assigned servers: %w", 
err))
+               return
+       } else if assignedServer != 0 {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("can 
not delete a phys_location with %d assigned servers", assignedServer), nil)
+               return
+       }
+
+       res, err := tx.Exec("DELETE FROM phys_location AS pl WHERE pl.id=$1", 
id)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
err)
+               return
+       }
+       rowsAffected, err := res.RowsAffected()
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
fmt.Errorf("determining rows affected for delete phys_location: %w", err))
+               return
+       }
+       if rowsAffected == 0 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("no rows deleted for phys_location"), nil)
+               return
+       }
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "phys_location was deleted.")
+       api.WriteAlerts(w, r, http.StatusOK, alerts)
+       return
+}
+
+func readAndValidateJsonStruct(r *http.Request) (tc.PhysLocationV5, error) {
+       var physLocation tc.PhysLocationV5
+       if err := json.NewDecoder(r.Body).Decode(&physLocation); err != nil {
+               userErr := fmt.Errorf("error decoding POST request body into 
PhysLocationV5 struct %w", err)
+               return physLocation, userErr
+       }
+
+       // validate JSON body
+       errs := tovalidate.ToErrors(validation.Errors{
+               "address":   validation.Validate(physLocation.Address, 
validation.Required),
+               "city":      validation.Validate(physLocation.City, 
validation.Required),
+               "name":      validation.Validate(physLocation.Name, 
validation.Required),
+               "regionId":  validation.Validate(physLocation.RegionID, 
validation.Required, validation.Min(0)),
+               "shortName": validation.Validate(physLocation.ShortName, 
validation.Required),
+               "state":     validation.Validate(physLocation.State, 
validation.Required),
+               "zip":       validation.Validate(physLocation.Zip, 
validation.Required)})
+       if len(errs) > 0 {
+               userErr := util.JoinErrs(errs)
+               return physLocation, userErr
+       }
+       return physLocation, nil
+}
+
+// selectMaxLastUpdatedQuery used for TryIfModifiedSinceQuery()
+func selectMaxLastUpdatedQuery(where string) string {
+       return `SELECT max(t) from (
+               SELECT max(a.last_updated) as t from phys_location a` + where +
+               ` UNION ALL
+       select max(last_updated) as t from last_deleted l where 
l.table_name='phys_location') as res`
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index ba2f12d167..306169ad94 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -256,10 +256,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
                {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodDelete, Path: `parameters/{id}$`, Handler: 
api.DeleteHandler(&parameter.TOParameter{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"PARAMETER:DELETE", 
"PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 
42627711831},
 
                //Phys_Location: CRUD
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodGet, Path: `phys_locations/?$`, Handler: 
api.ReadHandler(&physlocation.TOPhysLocation{}), RequiredPrivLevel: 
auth.PrivLevelReadOnly, RequiredPermissions: 
[]string{"PHYSICAL-LOCATION:READ"}, Authenticated: Authenticated, Middlewares: 
nil, ID: 42040518231},
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPut, Path: `phys_locations/{id}$`, Handler: 
api.UpdateHandler(&physlocation.TOPhysLocation{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: 
[]string{"PHYSICAL-LOCATION:UPDATE", "PHYSICAL-LOCATION:READ"}, Authenticated: 
Authenticated, Middlewares: nil, ID: 42279502131},
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `phys_locations/?$`, Handler: 
api.CreateHandler(&physlocation.TOPhysLocation{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: 
[]string{"PHYSICAL-LOCATION:CREATE", "PHYSICAL-LOCATION:READ"}, Authenticated: 
Authenticated, Middlewares: nil, ID: 424645664831},
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodDelete, Path: `phys_locations/{id}$`, Handler: 
api.DeleteHandler(&physlocation.TOPhysLocation{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: 
[]string{"PHYSICAL-LOCATION:DELETE", "PHYSICAL-LOCATION:READ"}, Authenticated: 
Authenticated, Middlewares: nil, ID: 4561422131},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodGet, Path: `phys_locations/?$`, Handler: 
physlocation.GetPhysLocation, RequiredPrivLevel: auth.PrivLevelReadOnly, 
RequiredPermissions: []string{"PHYSICAL-LOCATION:READ"}, Authenticated: 
Authenticated, Middlewares: nil, ID: 42040518231},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPut, Path: `phys_locations/{id}$`, Handler: 
physlocation.UpdatePhysLocation, RequiredPrivLevel: auth.PrivLevelOperations, 
RequiredPermissions: []string{"PHYSICAL-LOCATION:UPDATE", 
"PHYSICAL-LOCATION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 
42279502131},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `phys_locations/?$`, Handler: 
physlocation.CreatePhysLocation, RequiredPrivLevel: auth.PrivLevelOperations, 
RequiredPermissions: []string{"PHYSICAL-LOCATION:CREATE", 
"PHYSICAL-LOCATION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 
424645664831},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodDelete, Path: `phys_locations/{id}$`, Handler: 
physlocation.DeletePhysLocation, RequiredPrivLevel: auth.PrivLevelOperations, 
RequiredPermissions: []string{"PHYSICAL-LOCATION:DELETE", 
"PHYSICAL-LOCATION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 
4561422131},
 
                //Ping
                {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodGet, Path: `ping$`, Handler: ping.Handler, RequiredPrivLevel: 
auth.PrivLevelUnauthenticated, RequiredPermissions: nil, Authenticated: NoAuth, 
Middlewares: nil, ID: 455566159731},
diff --git a/traffic_ops/v5-client/phys_location.go 
b/traffic_ops/v5-client/phys_location.go
index c9c94f3f76..ce50b45a63 100644
--- a/traffic_ops/v5-client/phys_location.go
+++ b/traffic_ops/v5-client/phys_location.go
@@ -26,7 +26,7 @@ import (
 const apiPhysLocations = "/phys_locations"
 
 // CreatePhysLocation creates the passed Physical Location.
-func (to *Session) CreatePhysLocation(pl tc.PhysLocation, opts RequestOptions) 
(tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) CreatePhysLocation(pl tc.PhysLocationV5, opts 
RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
        if pl.RegionID == 0 && pl.RegionName != "" {
                regionOpts := NewRequestOptions()
                regionOpts.QueryParameters.Set("name", pl.RegionName)
@@ -47,7 +47,7 @@ func (to *Session) CreatePhysLocation(pl tc.PhysLocation, 
opts RequestOptions) (
 
 // UpdatePhysLocation replaces the Physical Location identified by 'id' with
 // the given Physical Location structure.
-func (to *Session) UpdatePhysLocation(id int, pl tc.PhysLocation, opts 
RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) UpdatePhysLocation(id int, pl tc.PhysLocationV5, opts 
RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
        route := fmt.Sprintf("%s/%d", apiPhysLocations, id)
        var alerts tc.Alerts
        reqInf, err := to.put(route, opts, pl, &alerts)
@@ -55,8 +55,8 @@ func (to *Session) UpdatePhysLocation(id int, pl 
tc.PhysLocation, opts RequestOp
 }
 
 // GetPhysLocations retrieves Physical Locations from Traffic Ops.
-func (to *Session) GetPhysLocations(opts RequestOptions) 
(tc.PhysLocationsResponse, toclientlib.ReqInf, error) {
-       var data tc.PhysLocationsResponse
+func (to *Session) GetPhysLocations(opts RequestOptions) 
(tc.PhysLocationsResponseV5, toclientlib.ReqInf, error) {
+       var data tc.PhysLocationsResponseV5
        reqInf, err := to.get(apiPhysLocations, opts, &data)
        return data, reqInf, err
 }

Reply via email to