From f9f862efb1c1aad0397e1d54a8228ce75cb4a144 Mon Sep 17 00:00:00 2001 From: Raajheer1 Date: Mon, 7 Apr 2025 20:03:24 -0500 Subject: [PATCH 1/3] Training --- cmd/main.go | 44 ++++ go.mod | 4 +- go.sum | 34 ++- internal/training/ots.go | 91 ++++++++ pkg/constants/role.go | 20 ++ pkg/database/models/rating_change.go | 13 +- pkg/database/models/roster.go | 2 +- pkg/database/models/roster_request.go | 85 +++++++- pkg/database/models/setup.go | 6 + pkg/database/models/training_notes.go | 44 ++++ pkg/database/models/training_ots.go | 80 +++++++ pkg/go-chi/middleware/auth/ots.go | 53 +++++ pkg/go-chi/middleware/auth/roster.go | 7 + pkg/go-chi/middleware/auth/training-note.go | 67 ++++++ pkg/utils/context.go | 30 +++ pkg/utils/perms.go | 14 ++ pkg/vatsim/api/base.go | 135 ++++++++++++ pkg/vatusa/user/eligibility.go | 225 ++++++++++++++++++++ pkg/vatusa/user/hour_rule.go | 47 ++++ pkg/vnas/base.go | 73 +++++++ views/docs/docs.go | 173 ++++++++++++++- views/docs/swagger.json | 173 ++++++++++++++- views/docs/swagger.yaml | 115 +++++++++- views/v3/rating-change/rating_change.go | 3 +- views/v3/roster-request/roster_request.go | 21 -- views/v3/router.go | 5 + views/v3/training-records/note.go | 176 +++++++++++++++ views/v3/training-records/ots_record.go | 121 +++++++++++ views/v3/training-records/router.go | 58 +++++ views/v3/training/ots_templates.go | 180 ++++++++++++++++ views/v3/training/router.go | 53 +++++ views/v3/user/router.go | 5 + 32 files changed, 2120 insertions(+), 37 deletions(-) create mode 100644 cmd/main.go create mode 100644 internal/training/ots.go create mode 100644 pkg/database/models/training_notes.go create mode 100644 pkg/database/models/training_ots.go create mode 100644 pkg/go-chi/middleware/auth/ots.go create mode 100644 pkg/go-chi/middleware/auth/training-note.go create mode 100644 pkg/vatsim/api/base.go create mode 100644 pkg/vatusa/user/eligibility.go create mode 100644 pkg/vatusa/user/hour_rule.go create mode 100644 pkg/vnas/base.go create mode 100644 views/v3/training-records/note.go create mode 100644 views/v3/training-records/ots_record.go create mode 100644 views/v3/training-records/router.go create mode 100644 views/v3/training/ots_templates.go create mode 100644 views/v3/training/router.go diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..517fb0c --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "github.com/VATUSA/primary-api/internal/training" + "github.com/VATUSA/primary-api/pkg/config" + "github.com/VATUSA/primary-api/pkg/database" + "github.com/VATUSA/primary-api/pkg/database/models" + "github.com/joho/godotenv" + log "github.com/sirupsen/logrus" + "time" +) + +func main() { + log.SetLevel(log.DebugLevel) + + //check, err := user.FiftyFiftyRuleCheck("ZDV", 811918) + //if err != nil { + // log.Fatal(err) + //} + // + //log.Info("FiftyFiftyRuleCheck: ", check) + _ = godotenv.Load(".env") + config.Cfg = config.New() + database.DB = database.Connect(config.Cfg.Database) + + s2OTS := training.S2OTS + jsonS2OTS, err := json.Marshal(s2OTS) + if err != nil { + log.Fatal(err) + } + + otsTemplate := &models.OTSTemplate{ + Name: "S2 OTS", + Template: jsonS2OTS, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + LastUpdatedBy: 1293257, + } + + if err := otsTemplate.Create(); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod index efb0068..0635d58 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/swaggo/swag v1.16.3 go.uber.org/zap v1.27.0 golang.org/x/oauth2 v0.23.0 + gorm.io/datatypes v1.2.5 gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.25.12 ) @@ -45,6 +46,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -55,7 +57,7 @@ require ( golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 28ca650..5cf1f40 100644 --- a/go.sum +++ b/go.sum @@ -58,12 +58,26 @@ github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaC github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -82,6 +96,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -122,8 +140,8 @@ golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -140,8 +158,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -154,8 +172,16 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= +gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= +gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= +gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/internal/training/ots.go b/internal/training/ots.go new file mode 100644 index 0000000..47068ec --- /dev/null +++ b/internal/training/ots.go @@ -0,0 +1,91 @@ +package training + +type Rating string + +const ( + Empty Rating = "" + Satisfactory Rating = "Satisfactory" + Unsatisfactory Rating = "Unsatisfactory" +) + +// KPI - Key Performance Indicator +type KPI struct { + Order uint `json:"order"` + Title string `json:"title"` + Description string `json:"description"` + Required bool `json:"required"` + Rating `json:"rating"` +} + +// KPA - Key Performance Area +type KPA struct { + Order uint `json:"order"` + Title string `json:"title"` + Description string `json:"description"` + Indicators []KPI `json:"indicators"` +} + +// KPC - Key performance Category +type KPC struct { + Order uint `json:"order"` + Title string `json:"title"` + Areas []KPA `json:"areas"` +} +type OTS struct { + Title string `json:"title"` + Categories []KPC `json:"categories"` +} + +var S2OTS = OTS{ + Title: "S2 (Tower) Rating Form", + Categories: []KPC{ + { + Order: 1, + Title: "Theory", + Areas: []KPA{ + { + Order: 1, + Title: "Clearance Delivery + Ground Control", + Description: "Demonstrates knowledge of Delivery and Ground Controller duties and responsibilities", + Indicators: []KPI{ + { + Order: 1, + Title: "Defines all parts of a clearance", + Description: "...", + Required: true, + Rating: Empty, + }, + { + Order: 2, + Title: "Explains all types of SIDs", + Description: "...", + Required: true, + Rating: Empty, + }, + }, + }, + { + Order: 2, + Title: "Local Control", + Description: "Demonstrates knowledge of Local Controller duties and responsibilities", + Indicators: []KPI{ + { + Order: 1, + Title: "Identifies difference between movement and non-movement areas", + Description: "...", + Required: true, + Rating: Empty, + }, + { + Order: 2, + Title: "Defines all parts of VFR traffic pattern", + Description: "...", + Required: true, + Rating: Empty, + }, + }, + }, + }, + }, + }, +} diff --git a/pkg/constants/role.go b/pkg/constants/role.go index 023b385..0432516 100644 --- a/pkg/constants/role.go +++ b/pkg/constants/role.go @@ -435,6 +435,26 @@ func (r RoleID) IsValidRole() bool { return ok } +func (r RoleID) IsFacilityStaff() bool { + switch r { + case AirTrafficManagerRole, DeputyAirTrafficManagerRole, TrainingAdministratorRole: + return true + case EventCoordinatorRole, FacilityEngineerRole, WebMasterRole: + return true + } + + return false +} + +func (r RoleID) IsAssistant() bool { + switch r { + case AssistantEventCoordinator, AssistantFacilityEngineer, AssistantWebMasterRole: + return true + } + + return false +} + func (r RoleID) DisplayName() string { return Roles[r].Name } diff --git a/pkg/database/models/rating_change.go b/pkg/database/models/rating_change.go index 0bd832b..5d8c936 100644 --- a/pkg/database/models/rating_change.go +++ b/pkg/database/models/rating_change.go @@ -37,7 +37,16 @@ func GetAllRatingChanges() ([]RatingChange, error) { return ratingChanges, database.DB.Find(&ratingChanges).Error } -func GetAllRatingChangesByCID(cid uint) ([]RatingChange, error) { +func GetFilteredRatingChanges(cid uint, dateAfter time.Time) ([]RatingChange, error) { var ratingChanges []RatingChange - return ratingChanges, database.DB.Where("cid = ?", cid).Find(&ratingChanges).Error + + query := database.DB + if cid != 0 { + query = query.Where("cid = ?", cid) + } + if !dateAfter.IsZero() { + query = query.Where("created_at > ?", dateAfter) + } + + return ratingChanges, query.Find(&ratingChanges).Error } diff --git a/pkg/database/models/roster.go b/pkg/database/models/roster.go index d311cdb..6fa4174 100644 --- a/pkg/database/models/roster.go +++ b/pkg/database/models/roster.go @@ -19,7 +19,7 @@ type Roster struct { Roles []UserRole `json:"roles" gorm:"foreignKey:RosterID"` CreatedAt time.Time `json:"created_at" example:"2021-01-01T00:00:00Z"` UpdatedAt time.Time `json:"updated_at" example:"2021-01-01T00:00:00Z"` - DeletedAt gorm.DeletedAt `json:"deleted_at" example:"2021-01-01T00:00:00Z"` // Soft Deletes for logging + DeletedAt time.Time `json:"deleted_at" example:"2021-01-01T00:00:00Z"` } func (r *Roster) BeforeCreate(tx *gorm.DB) error { diff --git a/pkg/database/models/roster_request.go b/pkg/database/models/roster_request.go index b40a023..7dff689 100644 --- a/pkg/database/models/roster_request.go +++ b/pkg/database/models/roster_request.go @@ -4,6 +4,7 @@ import ( "github.com/VATUSA/primary-api/pkg/constants" "github.com/VATUSA/primary-api/pkg/database" "github.com/VATUSA/primary-api/pkg/database/types" + "gorm.io/gorm" "time" ) @@ -18,12 +19,77 @@ type RosterRequest struct { UpdatedAt time.Time `json:"updated_at" example:"2021-01-01T00:00:00Z"` } +func (rr *RosterRequest) BeforeUpdate(tx *gorm.DB) error { + oldRR := &RosterRequest{ID: rr.ID} + if err := oldRR.Get(); err != nil { + return err + } + if oldRR.Status == types.Pending && rr.Status == types.Accepted { + roster := &Roster{ + CID: rr.CID, + Facility: rr.Facility, + OIs: "", + Home: false, + Visiting: false, + Status: "Active", + } + + if rr.RequestType == types.Visiting { + roster.Visiting = true + } else { + roster.Home = true + } + + if err := roster.Create(); err != nil { + return err + } + + // Transfers: + // On accepting a transfer remove the user from their current facility + if rr.RequestType == types.Transferring { + rosters, err := GetRostersByCID(rr.CID) + if err != nil { + return err + } + + for _, r := range rosters { + if r.Facility != rr.Facility && r.Home { + // if the user is an assistant add them to the new facility as a visitor + for _, role := range r.Roles { + if role.RoleID.IsAssistant() { + roster := &Roster{ + CID: r.CID, + Facility: r.Facility, + OIs: r.OIs, + Home: false, + Visiting: true, + Status: "Active", + } + + if err := roster.Create(); err != nil { + return err + } + break + } + } + + if err := r.Delete(); err != nil { + return err + } + } + } + } + } + + return nil +} + func (rr *RosterRequest) Create() error { return database.DB.Create(rr).Error } func (rr *RosterRequest) Update() error { - return database.DB.Save(rr).Error + return database.DB.Updates(rr).Error } func (rr *RosterRequest) Delete() error { @@ -39,6 +105,23 @@ func GetAllRosterRequests() ([]RosterRequest, error) { return rosterRequests, database.DB.Find(&rosterRequests).Error } +func GetFilteredRosterRequests(cid uint, reqType types.RequestType, dateAfter time.Time) ([]RosterRequest, error) { + var rosterRequests []RosterRequest + + query := database.DB + if cid != 0 { + query = query.Where("cid = ?", cid) + } + if reqType != "" { + query = query.Where("request_type = ?", reqType) + } + if !dateAfter.IsZero() { + query = query.Where("created_at > ?", dateAfter) + } + + return rosterRequests, query.Find(&rosterRequests).Error +} + func GetAllRosterRequestsByCID(cid uint) ([]RosterRequest, error) { var rosterRequests []RosterRequest return rosterRequests, database.DB.Where("cid = ?", cid).Find(&rosterRequests).Error diff --git a/pkg/database/models/setup.go b/pkg/database/models/setup.go index d14db72..0b31e72 100644 --- a/pkg/database/models/setup.go +++ b/pkg/database/models/setup.go @@ -28,6 +28,9 @@ func AutoMigrate() { &UserNotification{}, &UserFlag{}, &UserRole{}, + &OTSTemplate{}, + &OTSRecord{}, + &TrainingNotes{}, ) if err != nil { log.Fatal("[Database] Migration Error:", err) @@ -57,6 +60,9 @@ func DropTables() { &UserNotification{}, &UserFlag{}, &UserRole{}, + &OTSTemplate{}, + &OTSRecord{}, + &TrainingNotes{}, ) if err != nil { log.Fatal("[Database] Drop Table Error:", err) diff --git a/pkg/database/models/training_notes.go b/pkg/database/models/training_notes.go new file mode 100644 index 0000000..faf9799 --- /dev/null +++ b/pkg/database/models/training_notes.go @@ -0,0 +1,44 @@ +package models + +import ( + "github.com/VATUSA/primary-api/pkg/constants" + "github.com/VATUSA/primary-api/pkg/database" + "time" +) + +type TrainingNotes struct { + ID uint `json:"id" gorm:"primaryKey" example:"1"` + StudentCID uint `json:"student_cid" example:"1293257" gorm:"index"` + InstructorCID uint `json:"instructor_cid" example:"1293257"` + Facility constants.FacilityID `json:"facility" example:"ZDV"` + Position string `json:"position" example:"DEN_DEL"` + Duration time.Duration `json:"duration" example:"1h30m"` + Score uint `json:"score" example:"100"` + Notes string `json:"notes" example:"Great job!"` + OTSRecordID uint `json:"ots_record_id" example:"1"` + SessionDate time.Time `json:"session_date" example:"2021-01-01T00:00:00Z"` + CreatedAt time.Time `json:"created_at" example:"2021-01-01T00:00:00Z"` + UpdatedAt time.Time `json:"updated_at" example:"2021-01-01T00:00:00Z"` +} + +func (tn *TrainingNotes) Create() error { + return database.DB.Create(tn).Error +} + +func (tn *TrainingNotes) Update() error { + return database.DB.Save(tn).Error +} + +func (tn *TrainingNotes) Delete() error { + return database.DB.Delete(tn).Error +} + +func (tn *TrainingNotes) Get() error { + return database.DB.Where("id = ?", tn.ID).First(tn).Error +} + +func GetFilteredTrainingNotes(filter map[string]interface{}) ([]TrainingNotes, error) { + var notes []TrainingNotes + err := database.DB.Where(filter).Find(¬es).Error + return notes, err +} diff --git a/pkg/database/models/training_ots.go b/pkg/database/models/training_ots.go new file mode 100644 index 0000000..111277e --- /dev/null +++ b/pkg/database/models/training_ots.go @@ -0,0 +1,80 @@ +package models + +import ( + "errors" + "github.com/VATUSA/primary-api/pkg/database" + "gorm.io/datatypes" + "time" +) + +type OTSRecord struct { + ID uint `json:"id" gorm:"primaryKey"` + StudentCID uint `json:"student_cid" gorm:"index"` + InstructorCID uint `json:"instructor_cid" gorm:"index"` + Data datatypes.JSON `json:"data" gorm:"type:jsonb"` // Stores form fields dynamically + Notes string `json:"notes"` + Result bool `json:"result"` // True = Passed, False = Failed + CreatedAt time.Time `json:"created_at"` +} + +func (otsRecord *OTSRecord) BeforeCreate() error { + // check if student CID is valid + if !IsValidUser(otsRecord.StudentCID) { + return errors.New("invalid student CID") + } + + return nil +} + +func (otsRecord *OTSRecord) Create() error { + return database.DB.Create(otsRecord).Error +} + +func (otsRecord *OTSRecord) Update() error { + return database.DB.Updates(otsRecord).Error +} + +func (otsRecord *OTSRecord) Delete() error { + return database.DB.Delete(otsRecord).Error +} + +func (otsRecord *OTSRecord) Get() error { + return database.DB.Where("id = ?", otsRecord.ID).First(otsRecord).Error +} + +func GetFilteredOTSRecords(filter map[string]interface{}) ([]OTSRecord, error) { + var records []OTSRecord + err := database.DB.Where(filter).Find(&records).Error + return records, err +} + +type OTSTemplate struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name"` + Template datatypes.JSON `json:"template" gorm:"type:jsonb"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastUpdatedBy uint `json:"last_updated_by"` +} + +func (otsTemplates *OTSTemplate) Create() error { + return database.DB.Create(otsTemplates).Error +} + +func (otsTemplates *OTSTemplate) Update() error { + return database.DB.Updates(otsTemplates).Error +} + +func (otsTemplates *OTSTemplate) Delete() error { + return database.DB.Delete(otsTemplates).Error +} + +func (otsTemplates *OTSTemplate) Get() error { + return database.DB.Where("id = ?", otsTemplates.ID).First(otsTemplates).Error +} + +func GetAllOTSTemplates() ([]OTSTemplate, error) { + var templates []OTSTemplate + err := database.DB.Find(&templates).Error + return templates, err +} diff --git a/pkg/go-chi/middleware/auth/ots.go b/pkg/go-chi/middleware/auth/ots.go new file mode 100644 index 0000000..da59041 --- /dev/null +++ b/pkg/go-chi/middleware/auth/ots.go @@ -0,0 +1,53 @@ +package middleware + +import ( + "github.com/VATUSA/primary-api/pkg/utils" + log "github.com/sirupsen/logrus" + "net/http" +) + +func CanEditOTSTemplate(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + credentials := GetCredentials(r) + if credentials.User != nil { + if utils.IsVATUSAStaff(credentials.User) { + next.ServeHTTP(w, r) + return + } + + log.Warnf("User %d, attempted to edit an OTS.", credentials.User.CID) + } + + utils.Render(w, r, utils.ErrForbidden) + }) +} + +func CanCROTSRecord(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + targetUser := utils.GetUserCtx(r) + + credentials := GetCredentials(r) + if credentials.User != nil { + if utils.IsVATUSAStaff(credentials.User) { + next.ServeHTTP(w, r) + return + } + + for _, roster := range targetUser.Roster { + if utils.IsFacilitySeniorStaff(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + + if utils.IsInstructor(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + } + + log.Warnf("User %d, attempted to create/read an OTS.", credentials.User.CID) + } + + utils.Render(w, r, utils.ErrForbidden) + }) +} diff --git a/pkg/go-chi/middleware/auth/roster.go b/pkg/go-chi/middleware/auth/roster.go index 345ba23..3943500 100644 --- a/pkg/go-chi/middleware/auth/roster.go +++ b/pkg/go-chi/middleware/auth/roster.go @@ -10,6 +10,8 @@ func CanEditRoster(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { targetFacility := utils.GetFacilityCtx(r) + roster := utils.GetRosterCtx(r) + credentials := GetCredentials(r) if credentials.User != nil { if utils.IsVATUSAStaff(credentials.User) { @@ -22,6 +24,11 @@ func CanEditRoster(next http.Handler) http.Handler { return } + if roster.Visiting && roster.CID == credentials.User.CID { + next.ServeHTTP(w, r) + return + } + log.Warnf("User %d, attempted to edit roster for facility: %s. No permissions.", credentials.User.CID, targetFacility.ID) } diff --git a/pkg/go-chi/middleware/auth/training-note.go b/pkg/go-chi/middleware/auth/training-note.go new file mode 100644 index 0000000..016cfa6 --- /dev/null +++ b/pkg/go-chi/middleware/auth/training-note.go @@ -0,0 +1,67 @@ +package middleware + +import ( + "github.com/VATUSA/primary-api/pkg/utils" + log "github.com/sirupsen/logrus" + "net/http" +) + +func CanCreateReadTrainingNote(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + targetUser := utils.GetUserCtx(r) + + credentials := GetCredentials(r) + if credentials.User != nil { + if utils.IsVATUSAStaff(credentials.User) { + next.ServeHTTP(w, r) + return + } + + for _, roster := range targetUser.Roster { + if utils.IsFacilitySeniorStaff(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + + if utils.IsInstructor(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + + if utils.IsMentor(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + } + + log.Warnf("User %d, attempted to create/read an OTS.", credentials.User.CID) + } + + utils.Render(w, r, utils.ErrForbidden) + }) +} + +func CanDeleteTrainingNote(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + targetUser := utils.GetUserCtx(r) + + credentials := GetCredentials(r) + if credentials.User != nil { + if utils.IsVATUSAStaff(credentials.User) { + next.ServeHTTP(w, r) + return + } + + for _, roster := range targetUser.Roster { + if utils.IsFacilitySeniorStaff(credentials.User, roster.Facility) { + next.ServeHTTP(w, r) + return + } + } + + log.Warnf("User %d, attempted to create/read an OTS.", credentials.User.CID) + } + + utils.Render(w, r, utils.ErrForbidden) + }) +} diff --git a/pkg/utils/context.go b/pkg/utils/context.go index 0404fb5..4deafd8 100644 --- a/pkg/utils/context.go +++ b/pkg/utils/context.go @@ -185,6 +185,36 @@ func GetRosterRequestCtx(r *http.Request) *models.RosterRequest { return rr } +type TrainingNoteKey struct{} + +func GetTrainingNoteCtx(r *http.Request) *models.TrainingNotes { + tn, ok := r.Context().Value(TrainingNoteKey{}).(*models.TrainingNotes) + if !ok { + return nil + } + return tn +} + +type OTSRecord struct{} + +func GetOTSRecordCtx(r *http.Request) *models.OTSRecord { + otsr, ok := r.Context().Value(OTSRecord{}).(*models.OTSRecord) + if !ok { + return nil + } + return otsr +} + +type OTSTemplateKey struct{} + +func GetOTSTemplateCtx(r *http.Request) *models.OTSTemplate { + otst, ok := r.Context().Value(OTSTemplateKey{}).(*models.OTSTemplate) + if !ok { + return nil + } + return otst +} + type UserNotificationKey struct{} func GetUserNotificationCtx(r *http.Request) *models.UserNotification { diff --git a/pkg/utils/perms.go b/pkg/utils/perms.go index dcc309a..708268c 100644 --- a/pkg/utils/perms.go +++ b/pkg/utils/perms.go @@ -109,3 +109,17 @@ func IsInstructor(user *models.User, facility constants.FacilityID) bool { return false } + +func IsMentor(user *models.User, facility constants.FacilityID) bool { + for _, roster := range user.Roster { + if roster.Facility == facility { + for _, roles := range roster.Roles { + if roles.RoleID == constants.MentorRole { + return true + } + } + } + } + + return false +} diff --git a/pkg/vatsim/api/base.go b/pkg/vatsim/api/base.go new file mode 100644 index 0000000..06dae60 --- /dev/null +++ b/pkg/vatsim/api/base.go @@ -0,0 +1,135 @@ +package vatsim_api + +import ( + "encoding/json" + "fmt" + log "github.com/sirupsen/logrus" + "io" + "net/http" + "time" +) + +const ( + baseURL = "https://api.vatsim.net/api" + baseV2URL = "https://api.vatsim.net/v2" + dataURL = "https://data.vatsim.net/v3/vatsim-data.json" +) + +type Location struct { + Region string `json:"region"` + Division string `json:"division"` + Subdivision string `json:"subdivision"` +} + +func GetLocation(cid uint) (Location, error) { + resp, err := http.Get(fmt.Sprintf("%s/ratings/%d/", baseURL, cid)) + if err != nil { + return Location{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return Location{}, err + } + + if resp.StatusCode > 299 { + log.Warnf("Failed to get division for %s: %s", cid, body) + return Location{}, fmt.Errorf("invalid status code: %d", resp.StatusCode) + } + + var division Location + err = json.Unmarshal(body, &division) + if err != nil { + return Location{}, err + } + + return division, nil +} + +type UserStats struct { + Atc float32 `json:"atc"` + Pilot float32 `json:"pilot"` + S1 float32 `json:"s1"` + S2 float32 `json:"s2"` + S3 float32 `json:"s3"` + C1 float32 `json:"c1"` + C2 float32 `json:"c2"` + C3 float32 `json:"c3"` + I1 float32 `json:"i1"` + I2 float32 `json:"i2"` + I3 float32 `json:"i3"` + Sup float32 `json:"sup"` + Adm float32 `json:"adm"` +} + +func GetHours(cid uint) (UserStats, error) { + resp, err := http.Get(fmt.Sprintf("%s/members/%d/stats", baseV2URL, cid)) + if err != nil { + return UserStats{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return UserStats{}, err + } + + if resp.StatusCode > 299 { + log.Warnf("Failed to get stats for %s: %s", cid, body) + return UserStats{}, fmt.Errorf("invalid status code: %d", resp.StatusCode) + } + + var userStats UserStats + err = json.Unmarshal(body, &userStats) + if err != nil { + return UserStats{}, err + } + + return userStats, nil +} + +type ATCConnections struct { + Items []struct { + ConnectionId struct { + Id int `json:"id"` + VatsimId string `json:"vatsim_id"` + Type int `json:"type"` + Rating int `json:"rating"` + Callsign string `json:"callsign"` + Start time.Time `json:"start"` + End time.Time `json:"end"` + Server string `json:"server"` + } `json:"connection_id"` + AircraftTracked int `json:"aircrafttracked"` + AircraftSeen int `json:"aircraftseen"` + } `json:"items"` + Count uint `json:"count"` +} + +// GetATCConnections - for the last 30 days +func GetATCConnections(cid uint) (ATCConnections, error) { + resp, err := http.Get(fmt.Sprintf("%s/members/%d/atc", baseV2URL, cid)) + if err != nil { + return ATCConnections{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return ATCConnections{}, err + } + + if resp.StatusCode > 299 { + log.Warnf("Failed to get stats for %s: %s", cid, body) + return ATCConnections{}, fmt.Errorf("invalid status code: %d", resp.StatusCode) + } + + var atcConnections ATCConnections + err = json.Unmarshal(body, &atcConnections) + if err != nil { + return ATCConnections{}, err + } + + return atcConnections, nil +} diff --git a/pkg/vatusa/user/eligibility.go b/pkg/vatusa/user/eligibility.go new file mode 100644 index 0000000..041491e --- /dev/null +++ b/pkg/vatusa/user/eligibility.go @@ -0,0 +1,225 @@ +package user + +import ( + "github.com/VATUSA/primary-api/pkg/constants" + "github.com/VATUSA/primary-api/pkg/database/models" + "github.com/VATUSA/primary-api/pkg/database/types" + vatsim_api "github.com/VATUSA/primary-api/pkg/vatsim/api" + "time" +) + +// TODO 50/50 Rule + +type TransferEligibility struct { + IsVATUSAMember bool + NeedS1OrRCE bool + NoTransfersWithin90Days bool + NoOtherTransfers bool + NoPromotionsWithin90Days bool + NoStaffAtCurrentFacility bool + NotInstructor bool + UsedTransferWaiver bool +} + +func getTransferEligible(user *models.User) (*TransferEligibility, error) { + eligibility := &TransferEligibility{ + IsVATUSAMember: false, + } + + location, err := vatsim_api.GetLocation(user.CID) + if err != nil { + return nil, err + } + + if location.Division == "USA" { + eligibility.IsVATUSAMember = true + } + + // Needs RCE or S1 Exam + // TODO: Implement + + // No Transfers Within 60 Days + rosterReqs, err := models.GetFilteredRosterRequests(user.CID, types.Transferring, time.Now().AddDate(0, 0, -90)) + if err != nil { + return nil, err + } + + // No Other Transfers + for _, rosterReq := range rosterReqs { + if rosterReq.Status == types.Accepted { + eligibility.NoTransfersWithin90Days = false + } + + if rosterReq.Status == types.Pending { + eligibility.NoOtherTransfers = false + } + } + + // No Promotions Within 90 Days + ratingChanges, err := models.GetFilteredRatingChanges(user.CID, time.Now().Add(-90*24*time.Hour)) + if err != nil { + return nil, err + } + + for _, rc := range ratingChanges { + if rc.NewRating == constants.SeniorControllerRating || rc.OldRating == constants.SeniorControllerRating { + continue + } + if rc.NewRating == constants.InstructorRating || rc.OldRating == constants.InstructorRating { + continue + } + if rc.NewRating == constants.SeniorInstructorRating || rc.OldRating == constants.SeniorInstructorRating { + continue + } + if rc.NewRating == constants.SupervisorRating || rc.OldRating == constants.SupervisorRating { + continue + } + if rc.NewRating == constants.AdministratorRating || rc.OldRating == constants.AdministratorRating { + continue + } + + if rc.NewRating > rc.OldRating { + eligibility.NoPromotionsWithin90Days = false + break + } + } + + // No Staff at Current Facility + roles, err := models.GetAllUserRolesByCID(user.CID) + if err != nil { + return nil, err + } + + for _, role := range roles { + if role.RoleID.IsFacilityStaff() { + eligibility.NoStaffAtCurrentFacility = false + break + } + } + + // Not Instructor + if user.ControllerRating == constants.InstructorRating || user.ControllerRating == constants.SeniorInstructorRating { + eligibility.NotInstructor = false + } + + // Used Transfer Waiver + userFlags := &models.UserFlag{ + CID: user.CID, + } + if err := userFlags.Get(); err != nil { + return nil, err + } + + if userFlags.UsedTransferOverride { + eligibility.UsedTransferWaiver = true + } + + return eligibility, nil +} + +type VisitingEligibility struct { + HasS3 bool + NeedsRCE bool + NoVisitsWithin60Days bool + NoOtherVisitingRequests bool + NoPromotionsWithin90Days bool + Minimum50Hours bool + HasHomeFacility bool + VisitingAllowed bool +} + +func getVisitingEligible(user *models.User) (*VisitingEligibility, error) { + eligibility := &VisitingEligibility{ + HasS3: true, + NeedsRCE: true, + NoVisitsWithin60Days: true, + NoOtherVisitingRequests: true, + NoPromotionsWithin90Days: true, + Minimum50Hours: true, + HasHomeFacility: true, + VisitingAllowed: true, + } + + // Has S3 + if user.ControllerRating < constants.Student3Rating { + eligibility.HasS3 = false + } + + // Needs RCE + // TODO: Implement + + // No Visits Within 60 Days + rosterReqs, err := models.GetFilteredRosterRequests(user.CID, types.Visiting, time.Now().AddDate(0, 0, -60)) + if err != nil { + return nil, err + } + + for _, rosterReq := range rosterReqs { + if rosterReq.Status == types.Pending { + eligibility.NoOtherVisitingRequests = false + } + + if rosterReq.Status == types.Accepted { + eligibility.NoVisitsWithin60Days = false + } + } + + // No Promotions Within 90 Days + ratingChanges, err := models.GetFilteredRatingChanges(user.CID, time.Now().Add(-90*24*time.Hour)) + if err != nil { + return nil, err + } + + for _, rc := range ratingChanges { + if rc.NewRating == constants.SeniorControllerRating || rc.OldRating == constants.SeniorControllerRating { + continue + } + if rc.NewRating == constants.InstructorRating || rc.OldRating == constants.InstructorRating { + continue + } + if rc.NewRating == constants.SeniorInstructorRating || rc.OldRating == constants.SeniorInstructorRating { + continue + } + if rc.NewRating == constants.SupervisorRating || rc.OldRating == constants.SupervisorRating { + continue + } + if rc.NewRating == constants.AdministratorRating || rc.OldRating == constants.AdministratorRating { + continue + } + + if rc.NewRating > rc.OldRating { + eligibility.NoPromotionsWithin90Days = false + break + } + } + + // Minimum 50 Hours + hours, err := vatsim_api.GetHours(user.CID) + if err != nil { + return nil, err + } + + if user.ControllerRating == constants.Student1Rating && hours.S1 < 50 { + eligibility.Minimum50Hours = false + } else if user.ControllerRating == constants.Student2Rating && hours.S2 < 50 { + eligibility.Minimum50Hours = false + } else if user.ControllerRating == constants.Student3Rating && hours.S3 < 50 { + eligibility.Minimum50Hours = false + } else if user.ControllerRating == constants.ControllerRating && hours.C1 < 50 { + eligibility.Minimum50Hours = false + } + + // Visiting Allowed + userFlags := &models.UserFlag{ + CID: user.CID, + } + if err := userFlags.Get(); err != nil { + return nil, err + } + + if userFlags.NoVisiting { + eligibility.VisitingAllowed = false + } + + return eligibility, nil +} diff --git a/pkg/vatusa/user/hour_rule.go b/pkg/vatusa/user/hour_rule.go new file mode 100644 index 0000000..3dfbd37 --- /dev/null +++ b/pkg/vatusa/user/hour_rule.go @@ -0,0 +1,47 @@ +package user + +import ( + vatsim_api "github.com/VATUSA/primary-api/pkg/vatsim/api" + "github.com/VATUSA/primary-api/pkg/vnas" + log "github.com/sirupsen/logrus" + "time" +) + +func FiftyFiftyRuleCheck(homeARTCC string, cid uint) (bool, error) { + artcc, err := vnas.GetARTCC(homeARTCC) + if err != nil { + return false, err + } + + positions := artcc.Positions() + positionsMapped := make(map[string]bool, len(positions)) + for _, pos := range positions { + positionsMapped[pos] = true + } + + connections, err := vatsim_api.GetATCConnections(cid) + if err != nil { + return false, err + } + + var homeHours time.Duration + var otherHours time.Duration + + for _, connection := range connections.Items { + if time.Since(connection.ConnectionId.End) > 30*24*time.Hour { + break + } + hours := connection.ConnectionId.End.Sub(connection.ConnectionId.Start) + + if positionsMapped[connection.ConnectionId.Callsign] { + homeHours += hours + } else { + otherHours += hours + } + } + + log.Debug("Home hours: ", homeHours) + log.Debug("Other hours: ", otherHours) + + return homeHours > otherHours, nil +} diff --git a/pkg/vnas/base.go b/pkg/vnas/base.go new file mode 100644 index 0000000..e975408 --- /dev/null +++ b/pkg/vnas/base.go @@ -0,0 +1,73 @@ +package vnas + +import ( + "encoding/json" + "fmt" + log "github.com/sirupsen/logrus" + "io" + "net/http" + "time" +) + +const ( + baseURL = "https://data-api.vnas.vatsim.net/api" +) + +type ARTCC struct { + ID string `json:"id"` + Facility Facility `json:"facility"` + LastUpdatedAt time.Time `json:"last_updated_at"` +} + +type Facility struct { + ID string `json:"id" example:"ZDV"` + Name string `json:"name" example:"Denver ARTCC"` + Positions []Position `json:"positions"` + ChildFacilities []Facility `json:"childFacilities"` +} + +type Position struct { + ID string `json:"id" example:""` + Name string `json:"name" example:"14 - Hayden High"` + Callsign string `json:"callsign" example:"DEN_14_CTR"` +} + +func GetARTCC(id string) (*ARTCC, error) { + resp, err := http.Get(fmt.Sprintf("%s/artccs/%s", baseURL, id)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode > 299 { + log.Warnf("Failed to get ARTCC for %s: %s", id, body) + return nil, fmt.Errorf("invalid status code: %d", resp.StatusCode) + } + + var artcc ARTCC + if err = json.Unmarshal(body, &artcc); err != nil { + return nil, err + } + + return &artcc, nil +} + +func (artcc ARTCC) Positions() []string { + var positions []string + for _, position := range artcc.Facility.Positions { + positions = append(positions, position.Callsign) + } + + for _, facility := range artcc.Facility.ChildFacilities { + for _, position := range facility.Positions { + positions = append(positions, position.Callsign) + } + } + + return positions +} diff --git a/views/docs/docs.go b/views/docs/docs.go index b27549f..3544d02 100644 --- a/views/docs/docs.go +++ b/views/docs/docs.go @@ -3127,6 +3127,136 @@ const docTemplate = `{ } } }, + "/training/ots/templates": { + "get": { + "description": "List all OTS templates", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List all OTS templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/training.response" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/training/ots/templates/{id}": { + "delete": { + "description": "Delete an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Delete an OTS template", + "parameters": [ + { + "type": "integer", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "patch": { + "description": "Patch an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Patch an OTS template", + "parameters": [ + { + "type": "integer", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Template", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/training.request" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/training.response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, "/user/": { "get": { "description": "Get information for the user logged in", @@ -6389,7 +6519,6 @@ const docTemplate = `{ "example": "2021-01-01T00:00:00Z" }, "deleted_at": { - "description": "Soft Deletes for logging", "type": "string", "example": "2021-01-01T00:00:00Z" }, @@ -6523,6 +6652,46 @@ const docTemplate = `{ } } }, + "training.request": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "template": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "training.response": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_by": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "template": { + "type": "array", + "items": { + "type": "integer" + } + }, + "updated_at": { + "type": "string" + } + } + }, "types.FeedbackRating": { "type": "string", "enum": [ @@ -6851,7 +7020,7 @@ const docTemplate = `{ ], "example": "ZDV" }, - "role_id": { + "role": { "allOf": [ { "$ref": "#/definitions/constants.RoleID" diff --git a/views/docs/swagger.json b/views/docs/swagger.json index f2c2feb..ce043c6 100644 --- a/views/docs/swagger.json +++ b/views/docs/swagger.json @@ -3120,6 +3120,136 @@ } } }, + "/training/ots/templates": { + "get": { + "description": "List all OTS templates", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List all OTS templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/training.response" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/training/ots/templates/{id}": { + "delete": { + "description": "Delete an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Delete an OTS template", + "parameters": [ + { + "type": "integer", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "patch": { + "description": "Patch an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Patch an OTS template", + "parameters": [ + { + "type": "integer", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Template", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/training.request" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/training.response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, "/user/": { "get": { "description": "Get information for the user logged in", @@ -6382,7 +6512,6 @@ "example": "2021-01-01T00:00:00Z" }, "deleted_at": { - "description": "Soft Deletes for logging", "type": "string", "example": "2021-01-01T00:00:00Z" }, @@ -6516,6 +6645,46 @@ } } }, + "training.request": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "template": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "training.response": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_by": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "template": { + "type": "array", + "items": { + "type": "integer" + } + }, + "updated_at": { + "type": "string" + } + } + }, "types.FeedbackRating": { "type": "string", "enum": [ @@ -6844,7 +7013,7 @@ ], "example": "ZDV" }, - "role_id": { + "role": { "allOf": [ { "$ref": "#/definitions/constants.RoleID" diff --git a/views/docs/swagger.yaml b/views/docs/swagger.yaml index 30c619d..a12ef3a 100644 --- a/views/docs/swagger.yaml +++ b/views/docs/swagger.yaml @@ -1083,7 +1083,6 @@ definitions: example: "2021-01-01T00:00:00Z" type: string deleted_at: - description: Soft Deletes for logging example: "2021-01-01T00:00:00Z" type: string facility: @@ -1175,6 +1174,32 @@ definitions: example: "2021-01-01T00:00:00Z" type: string type: object + training.request: + properties: + name: + type: string + template: + items: + type: integer + type: array + type: object + training.response: + properties: + created_at: + type: string + id: + type: integer + last_updated_by: + type: integer + name: + type: string + template: + items: + type: integer + type: array + updated_at: + type: string + type: object types.FeedbackRating: enum: - unsatisfactory @@ -1408,7 +1433,7 @@ definitions: allOf: - $ref: '#/definitions/constants.FacilityID' example: ZDV - role_id: + role: allOf: - $ref: '#/definitions/constants.RoleID' example: ATM @@ -3511,6 +3536,92 @@ paths: summary: Delete a roster tags: - roster + /training/ots/templates: + get: + consumes: + - application/json + description: List all OTS templates + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/training.response' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: List all OTS templates + tags: + - training + /training/ots/templates/{id}: + delete: + consumes: + - application/json + description: Delete an OTS template + parameters: + - description: Template ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "204": + description: No Content + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Delete an OTS template + tags: + - training + patch: + consumes: + - application/json + description: Patch an OTS template + parameters: + - description: Template ID + in: path + name: id + required: true + type: integer + - description: Template + in: body + name: template + required: true + schema: + $ref: '#/definitions/training.request' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/training.response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Patch an OTS template + tags: + - training /user/: get: consumes: diff --git a/views/v3/rating-change/rating_change.go b/views/v3/rating-change/rating_change.go index e514912..c59e935 100644 --- a/views/v3/rating-change/rating_change.go +++ b/views/v3/rating-change/rating_change.go @@ -10,6 +10,7 @@ import ( "github.com/go-playground/validator/v10" "net/http" "sort" + "time" ) type Request struct { @@ -115,7 +116,7 @@ func CreateRatingChange(w http.ResponseWriter, r *http.Request) { // @Failure 500 {object} utils.ErrResponse // @Router /user/{cid}/rating-change [get] func ListRatingChanges(w http.ResponseWriter, r *http.Request) { - rc, err := models.GetAllRatingChangesByCID(utils.GetUserCtx(r).CID) + rc, err := models.GetFilteredRatingChanges(utils.GetUserCtx(r).CID, time.Time{}) if err != nil { utils.Render(w, r, utils.ErrInvalidRequest(err)) return diff --git a/views/v3/roster-request/roster_request.go b/views/v3/roster-request/roster_request.go index cd678e4..18f2042 100644 --- a/views/v3/roster-request/roster_request.go +++ b/views/v3/roster-request/roster_request.go @@ -211,27 +211,6 @@ func PatchRosterRequest(w http.ResponseWriter, r *http.Request) { } if data.Status != "" { - if req.Status == types.Pending && data.Status == types.Accepted { - roster := &models.Roster{ - CID: req.CID, - Facility: req.Facility, - OIs: "", - Home: false, - Visiting: false, - Status: "Active", - } - - if data.RequestType == types.Visiting { - roster.Visiting = true - } else { - roster.Home = true - } - - if err := roster.Create(); err != nil { - utils.Render(w, r, utils.ErrInvalidRequest(err)) - return - } - } req.Status = data.Status } diff --git a/views/v3/router.go b/views/v3/router.go index efcc690..7dfa85d 100644 --- a/views/v3/router.go +++ b/views/v3/router.go @@ -4,6 +4,7 @@ import ( "github.com/VATUSA/primary-api/pkg/config" "github.com/VATUSA/primary-api/views/v3/event" "github.com/VATUSA/primary-api/views/v3/facility" + "github.com/VATUSA/primary-api/views/v3/training" "github.com/VATUSA/primary-api/views/v3/user" "github.com/go-chi/chi/v5" ) @@ -19,5 +20,9 @@ func Router(r chi.Router, cfg *config.Config) { }) r.Get("/events", event.GetAllEvents) + + r.Route("/training", func(r chi.Router) { + training.Router(r) + }) }) } diff --git a/views/v3/training-records/note.go b/views/v3/training-records/note.go new file mode 100644 index 0000000..cf5097b --- /dev/null +++ b/views/v3/training-records/note.go @@ -0,0 +1,176 @@ +package trainingrecords + +import ( + "encoding/json" + "errors" + "github.com/VATUSA/primary-api/pkg/constants" + "github.com/VATUSA/primary-api/pkg/database/models" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/render" + "github.com/go-playground/validator/v10" + "net/http" + "time" +) + +type noteRequest struct { + Facility constants.FacilityID `json:"facility"` + Position string `json:"position"` + Duration time.Duration `json:"duration"` + Score uint `json:"score"` + Notes string `json:"notes"` + OTSRecordID uint `json:"ots_record_id"` + SessionDate time.Time `json:"session_date"` +} + +func (req *noteRequest) Validate() error { + return validator.New().Struct(req) +} + +func (req *noteRequest) Bind(r *http.Request) error { + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + return err + } + return nil +} + +type noteResponse struct { + *models.TrainingNotes +} + +func newNoteResponse(note *models.TrainingNotes) *noteResponse { + return ¬eResponse{note} +} + +func (res *noteResponse) Render(w http.ResponseWriter, r *http.Request) error { + if res.TrainingNotes == nil { + return errors.New("training note not found") + } + return nil +} + +func newNoteListResponse(notes []models.TrainingNotes) []render.Renderer { + list := []render.Renderer{} + for idx := range notes { + list = append(list, newNoteResponse(¬es[idx])) + } + return list +} + +// createNote godoc +// @Summary Create a new training note +// @Description Create a new training note +// @Tags training +// @Accept json +// @Produce json +// @Param cid path string true "CID" +// @Param note body noteRequest true "Training Note" +// @Success 201 {object} noteResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/notes [post] +func createNote(w http.ResponseWriter, r *http.Request) { + data := ¬eRequest{} + if err := render.Bind(r, data); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + note := &models.TrainingNotes{ + Facility: data.Facility, + Position: data.Position, + Duration: data.Duration, + Score: data.Score, + Notes: data.Notes, + OTSRecordID: data.OTSRecordID, + SessionDate: data.SessionDate, + } + + note.StudentCID = utils.GetUserCtx(r).CID + note.InstructorCID = utils.GetXUser(r).CID + + if err := note.Create(); err != nil { + utils.Render(w, r, utils.ErrInternalServerWithErr(err)) + return + } + + render.Status(r, http.StatusCreated) + utils.Render(w, r, newNoteResponse(note)) +} + +// listNotes godoc +// @Summary List training notes for a user +// @Description List all training notes for a specific user +// @Tags training +// @Accept json +// @Produce json +// @Param cid path string true "CID" +// @Success 200 {object} noteResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/notes [get] +func listNotes(w http.ResponseWriter, r *http.Request) { + cid := utils.GetUserCtx(r).CID + + // Fetch the training notes for the user + notes, err := models.GetFilteredTrainingNotes(map[string]interface{}{"student_cid": cid}) + if err != nil { + utils.Render(w, r, utils.ErrInternalServerWithErr(err)) + return + } + + if err := render.RenderList(w, r, newNoteListResponse(notes)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} + +// getNote godoc +// @Summary Get a specific training note +// @Description Retrieve a specific training note by its ID +// @Tags training +// @Accept json +// @Produce json +// @Param note_id path uint true "Note ID" +// @Param cid path string true "CID" +// @Success 200 {object} noteResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 404 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/notes/{note_id} [get] +func getNote(w http.ResponseWriter, r *http.Request) { + note := utils.GetTrainingNoteCtx(r) + + utils.Render(w, r, newNoteResponse(note)) +} + +// deleteNote godoc +// @Summary Delete a specific training note +// @Description Delete a specific training note by its ID +// @Tags training +// @Accept json +// @Produce json +// @Param note_id path uint true "Note ID" +// @Param cid path string true "CID" +// @Success 204 "No Content" +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 404 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/notes/{note_id} [delete] +func deleteNote(w http.ResponseWriter, r *http.Request) { + note := utils.GetTrainingNoteCtx(r) + + if err := note.Delete(); err != nil { + utils.Render(w, r, utils.ErrInternalServerWithErr(err)) + return + } + + render.Status(r, http.StatusNoContent) // HTTP 204 +} diff --git a/views/v3/training-records/ots_record.go b/views/v3/training-records/ots_record.go new file mode 100644 index 0000000..99a2e0d --- /dev/null +++ b/views/v3/training-records/ots_record.go @@ -0,0 +1,121 @@ +package trainingrecords + +import ( + "encoding/json" + "errors" + "github.com/VATUSA/primary-api/pkg/database/models" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/render" + "github.com/go-playground/validator/v10" + "gorm.io/datatypes" + "net/http" +) + +type otsRecordRequest struct { + Data datatypes.JSON `json:"data"` + Notes string `json:"notes"` + Result bool `json:"result"` +} + +func (req *otsRecordRequest) Validate() error { + return validator.New().Struct(req) +} + +func (req *otsRecordRequest) Bind(r *http.Request) error { + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + return err + } + return nil +} + +type otsRecordResponse struct { + *models.OTSRecord +} + +func newOTSRecordResponse(temp *models.OTSRecord) *otsRecordResponse { + return &otsRecordResponse{temp} +} + +func (res *otsRecordResponse) Render(w http.ResponseWriter, r *http.Request) error { + if res.OTSRecord == nil { + return errors.New("OTS Record not found") + } + return nil +} + +func newOTSRecordList(templates []models.OTSRecord) []render.Renderer { + list := []render.Renderer{} + for idx := range templates { + list = append(list, newOTSRecordResponse(&templates[idx])) + } + return list +} + +// createOTSRecord godoc +// @Summary Create a new OTS Record +// @Description Create a new OTS Record +// @Tags training +// @Accept json +// @Produce json +// @Param cid path string true "CID" +// @Param otsRecordRequest body otsRecordRequest true "OTS Record" +// @Success 201 {object} otsRecordResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/ots [post] +func createOTSRecord(w http.ResponseWriter, r *http.Request) { + cid := utils.GetUserCtx(r).CID + data := &otsRecordRequest{} + if err := render.Bind(r, data); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + record := &models.OTSRecord{ + StudentCID: cid, + Data: data.Data, + Notes: data.Notes, + Result: data.Result, + } + + instructorCID := utils.GetXUser(r).CID + record.InstructorCID = instructorCID + + if err := record.Create(); err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + if err := render.Render(w, r, newOTSRecordResponse(record)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} + +// listOTSRecords godoc +// @Summary List all OTS Records for user +// @Description List all OTS Records for user +// @Tags training +// @Accept json +// @Produce json +// @Param cid path string true "CID" +// @Success 200 {object} []otsRecordResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /user/{cid}/training/ots [get] +func listOTSRecords(w http.ResponseWriter, r *http.Request) { + cid := utils.GetUserCtx(r).CID + records, err := models.GetFilteredOTSRecords(map[string]interface{}{"student_cid": cid}) + if err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + if err := render.RenderList(w, r, newOTSRecordList(records)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} diff --git a/views/v3/training-records/router.go b/views/v3/training-records/router.go new file mode 100644 index 0000000..8f17b0e --- /dev/null +++ b/views/v3/training-records/router.go @@ -0,0 +1,58 @@ +package trainingrecords + +import ( + "context" + "github.com/VATUSA/primary-api/pkg/database/models" + middleware "github.com/VATUSA/primary-api/pkg/go-chi/middleware/auth" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/chi/v5" + "net/http" + "strconv" +) + +func Router(r chi.Router) { + r.Use(middleware.NotGuest) + + r.Route("/ots", func(r chi.Router) { + r.Use(middleware.CanCROTSRecord) + r.Get("/", listOTSRecords) + r.Post("/", createOTSRecord) + }) + + r.Route("/notes", func(r chi.Router) { + r.Use(middleware.CanCreateReadTrainingNote) + r.Get("/", listNotes) + r.Post("/", createNote) + + r.Route("/{NoteID}", func(r chi.Router) { + r.Use(noteCtx) + r.Get("/", getNote) + r.With(middleware.CanDeleteTrainingNote).Delete("/", deleteNote) + }) + }) +} + +func noteCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "NoteID") + if id == "" { + http.Error(w, "Invalid training note ID", http.StatusBadRequest) + return + } + + noteID, err := strconv.ParseUint(id, 10, 64) + if err != nil { + http.Error(w, "Invalid training note ID", http.StatusBadRequest) + return + } + + note := &models.TrainingNotes{ID: uint(noteID)} + if err = note.Get(); err != nil { + http.Error(w, "Invalid training note ID", http.StatusBadRequest) + return + } + + ctx := context.WithValue(r.Context(), utils.TrainingNoteKey{}, note) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/views/v3/training/ots_templates.go b/views/v3/training/ots_templates.go new file mode 100644 index 0000000..8280c2e --- /dev/null +++ b/views/v3/training/ots_templates.go @@ -0,0 +1,180 @@ +package training + +import ( + "encoding/json" + "errors" + "github.com/VATUSA/primary-api/pkg/database/models" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/render" + "github.com/go-playground/validator/v10" + "gorm.io/datatypes" + "net/http" +) + +type otsTemplateRequest struct { + Name string `json:"name"` + Template json.RawMessage `json:"template"` +} + +func (req *otsTemplateRequest) Validate() error { + return validator.New().Struct(req) +} + +func (req *otsTemplateRequest) Bind(r *http.Request) error { + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + return err + } + return nil +} + +type otsTemplateResponse struct { + *models.OTSTemplate +} + +func newOTSTemplateResponse(temp *models.OTSTemplate) *otsTemplateResponse { + return &otsTemplateResponse{temp} +} + +func (res *otsTemplateResponse) Render(w http.ResponseWriter, r *http.Request) error { + if res.OTSTemplate == nil { + return errors.New("OTS Template not found") + } + return nil +} + +func newTemplateList(templates []models.OTSTemplate) []render.Renderer { + list := []render.Renderer{} + for idx := range templates { + list = append(list, newOTSTemplateResponse(&templates[idx])) + } + return list +} + +// createTemplate godoc +// @Summary Create an OTS template +// @Description Create an OTS template +// @Tags training +// @Accept json +// @Produce json +// @Param template body otsTemplateRequest true "Template" +// @Success 201 {object} otsTemplateResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /training/ots/templates [post] +func createTemplate(w http.ResponseWriter, r *http.Request) { + data := &otsTemplateRequest{} + if err := data.Bind(r); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + template := &models.OTSTemplate{ + Name: data.Name, + } + + if err := json.Unmarshal(data.Template, &template.Template); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + template.Template = datatypes.JSON(data.Template) + + user := utils.GetXUser(r) + template.LastUpdatedBy = user.CID + + if err := template.Create(); err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + render.Status(r, http.StatusCreated) + utils.Render(w, r, newOTSTemplateResponse(template)) +} + +// listTemplates godoc +// @Summary List all OTS templates +// @Description List all OTS templates +// @Tags training +// @Accept json +// @Produce json +// @Success 200 {object} []otsTemplateResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /training/ots/templates [get] +func listTemplates(w http.ResponseWriter, r *http.Request) { + templates, err := models.GetAllOTSTemplates() + if err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + if err := render.RenderList(w, r, newTemplateList(templates)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} + +// patchTemplate godoc +// @Summary Patch an OTS template +// @Description Patch an OTS template +// @Tags training +// @Accept json +// @Produce json +// @Param id path int true "Template ID" +// @Param template body otsTemplateRequest true "Template" +// @Success 200 {object} otsTemplateResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /training/ots/templates/{id} [patch] +func patchTemplate(w http.ResponseWriter, r *http.Request) { + template := utils.GetOTSTemplateCtx(r) + data := &otsTemplateRequest{} + if err := data.Bind(r); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + if data.Name != "" { + template.Name = data.Name + } + if len(data.Template) != 0 { + // Try to unmarshal the template + if err := json.Unmarshal(data.Template, &template.Template); err != nil { + utils.Render(w, r, utils.ErrInvalidRequest(err)) + return + } + + template.Template = datatypes.JSON(data.Template) + } + + user := utils.GetXUser(r) + template.LastUpdatedBy = user.CID + + if err := template.Update(); err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + utils.Render(w, r, newOTSTemplateResponse(template)) +} + +// deleteTemplate godoc +// @Summary Delete an OTS template +// @Description Delete an OTS template +// @Tags training +// @Accept json +// @Produce json +// @Param id path int true "Template ID" +// @Success 204 +// @Failure 500 {object} utils.ErrResponse +// @Router /training/ots/templates/{id} [delete] +func deleteTemplate(w http.ResponseWriter, r *http.Request) { + template := utils.GetOTSTemplateCtx(r) + if err := template.Delete(); err != nil { + utils.Render(w, r, utils.ErrInternalServer) + return + } + + render.Status(r, http.StatusNoContent) +} diff --git a/views/v3/training/router.go b/views/v3/training/router.go new file mode 100644 index 0000000..28446a4 --- /dev/null +++ b/views/v3/training/router.go @@ -0,0 +1,53 @@ +package training + +import ( + "context" + "github.com/VATUSA/primary-api/pkg/database/models" + middleware "github.com/VATUSA/primary-api/pkg/go-chi/middleware/auth" + "github.com/VATUSA/primary-api/pkg/utils" + "github.com/go-chi/chi/v5" + "net/http" + "strconv" +) + +func Router(r chi.Router) { + r.Use(middleware.NotGuest) + + r.Route("/ots", func(r chi.Router) { + r.Route("/templates", func(r chi.Router) { + r.Get("/", listTemplates) + r.With(middleware.CanEditOTSTemplate).Post("/", createTemplate) + r.Route("/{TemplateID}", func(r chi.Router) { + r.Use(templateCtx) + + r.With(middleware.CanEditOTSTemplate).Patch("/", patchTemplate) + r.With(middleware.CanEditOTSTemplate).Delete("/", deleteTemplate) + }) + }) + }) +} + +func templateCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "TemplateID") + if id == "" { + http.Error(w, "Invalid otsTemplateRequest", http.StatusBadRequest) + return + } + + TemplateID, err := strconv.ParseUint(id, 10, 64) + if err != nil { + http.Error(w, "Invalid otsTemplateRequest", http.StatusBadRequest) + return + } + + template := &models.OTSTemplate{ID: uint(TemplateID)} + if err = template.Get(); err != nil { + http.Error(w, "Invalid otsTemplateRequest", http.StatusBadRequest) + return + } + + ctx := context.WithValue(r.Context(), utils.OTSTemplateKey{}, template) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/views/v3/user/router.go b/views/v3/user/router.go index 62dc247..d365b00 100644 --- a/views/v3/user/router.go +++ b/views/v3/user/router.go @@ -11,6 +11,7 @@ import ( "github.com/VATUSA/primary-api/views/v3/notification" rating_change "github.com/VATUSA/primary-api/views/v3/rating-change" "github.com/VATUSA/primary-api/views/v3/roster" + trainingrecords "github.com/VATUSA/primary-api/views/v3/training-records" user_flag "github.com/VATUSA/primary-api/views/v3/user-flag" user_notification "github.com/VATUSA/primary-api/views/v3/user-notification" user_role "github.com/VATUSA/primary-api/views/v3/user-role" @@ -62,6 +63,10 @@ func Router(r chi.Router) { r.With(middleware.NotGuest, middleware.CanViewUser).Get("/roster", roster.GetUserRosters) + r.Route("/training", func(r chi.Router) { + trainingrecords.Router(r) + }) + r.Route("/user-flag", func(r chi.Router) { user_flag.Router(r) }) From 0026ce279ca2aa36c8364c9e4fdfde3e48755bcb Mon Sep 17 00:00:00 2001 From: Raaj Patel Date: Fri, 11 Apr 2025 15:56:33 -0500 Subject: [PATCH 2/3] Fixes --- cmd/database/migration.go | 4 +- cmd/main.go | 44 -- pkg/database/models/setup.go | 4 +- pkg/database/models/training_notes.go | 10 +- pkg/database/models/training_ots.go | 17 +- pkg/go-chi/middleware/auth/training-note.go | 4 +- pkg/utils/context.go | 4 +- views/docs/docs.go | 741 ++++++++++++++++++-- views/docs/swagger.json | 741 ++++++++++++++++++-- views/docs/swagger.yaml | 430 +++++++++++- views/router.go | 2 +- views/v3/facility/router.go | 3 + views/v3/router.go | 3 +- views/v3/training-records/note.go | 74 +- views/v3/training-records/ots_record.go | 10 +- views/v3/user/login.go | 2 + 16 files changed, 1888 insertions(+), 205 deletions(-) delete mode 100644 cmd/main.go diff --git a/cmd/database/migration.go b/cmd/database/migration.go index 8b36991..cf88a10 100644 --- a/cmd/database/migration.go +++ b/cmd/database/migration.go @@ -200,7 +200,7 @@ func MigrateUsers(oldDbConn *gorm.DB) { Home: true, Visiting: false, Status: "Active", - DeletedAt: nil, + DeletedAt: time.Time{}, } if err := roster.Create(); err != nil { @@ -220,7 +220,7 @@ func MigrateUsers(oldDbConn *gorm.DB) { Home: false, Visiting: true, Status: "Active", - DeletedAt: nil, + DeletedAt: time.Time{}, } if err := roster.Create(); err != nil { diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 517fb0c..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "encoding/json" - "github.com/VATUSA/primary-api/internal/training" - "github.com/VATUSA/primary-api/pkg/config" - "github.com/VATUSA/primary-api/pkg/database" - "github.com/VATUSA/primary-api/pkg/database/models" - "github.com/joho/godotenv" - log "github.com/sirupsen/logrus" - "time" -) - -func main() { - log.SetLevel(log.DebugLevel) - - //check, err := user.FiftyFiftyRuleCheck("ZDV", 811918) - //if err != nil { - // log.Fatal(err) - //} - // - //log.Info("FiftyFiftyRuleCheck: ", check) - _ = godotenv.Load(".env") - config.Cfg = config.New() - database.DB = database.Connect(config.Cfg.Database) - - s2OTS := training.S2OTS - jsonS2OTS, err := json.Marshal(s2OTS) - if err != nil { - log.Fatal(err) - } - - otsTemplate := &models.OTSTemplate{ - Name: "S2 OTS", - Template: jsonS2OTS, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - LastUpdatedBy: 1293257, - } - - if err := otsTemplate.Create(); err != nil { - log.Fatal(err) - } -} diff --git a/pkg/database/models/setup.go b/pkg/database/models/setup.go index 0b31e72..678a71a 100644 --- a/pkg/database/models/setup.go +++ b/pkg/database/models/setup.go @@ -29,7 +29,7 @@ func AutoMigrate() { &UserFlag{}, &UserRole{}, &OTSTemplate{}, - &OTSRecord{}, + &RatingExamRecord{}, &TrainingNotes{}, ) if err != nil { @@ -61,7 +61,7 @@ func DropTables() { &UserFlag{}, &UserRole{}, &OTSTemplate{}, - &OTSRecord{}, + &RatingExamRecord{}, &TrainingNotes{}, ) if err != nil { diff --git a/pkg/database/models/training_notes.go b/pkg/database/models/training_notes.go index faf9799..575c61c 100644 --- a/pkg/database/models/training_notes.go +++ b/pkg/database/models/training_notes.go @@ -9,13 +9,15 @@ import ( type TrainingNotes struct { ID uint `json:"id" gorm:"primaryKey" example:"1"` StudentCID uint `json:"student_cid" example:"1293257" gorm:"index"` + Student *User `json:"-" gorm:"foreignKey:StudentCID"` InstructorCID uint `json:"instructor_cid" example:"1293257"` + Instructor *User `json:"-" gorm:"foreignKey:InstructorCID"` Facility constants.FacilityID `json:"facility" example:"ZDV"` Position string `json:"position" example:"DEN_DEL"` - Duration time.Duration `json:"duration" example:"1h30m"` - Score uint `json:"score" example:"100"` + Duration time.Duration `json:"duration" example:"60"` Notes string `json:"notes" example:"Great job!"` - OTSRecordID uint `json:"ots_record_id" example:"1"` + RatingExamID uint `json:"rating_exam_id" example:"1"` + RatingExam *RatingExamRecord `json:"-" gorm:"foreignKey:RatingExamID"` SessionDate time.Time `json:"session_date" example:"2021-01-01T00:00:00Z"` CreatedAt time.Time `json:"created_at" example:"2021-01-01T00:00:00Z"` UpdatedAt time.Time `json:"updated_at" example:"2021-01-01T00:00:00Z"` @@ -39,6 +41,6 @@ func (tn *TrainingNotes) Get() error { func GetFilteredTrainingNotes(filter map[string]interface{}) ([]TrainingNotes, error) { var notes []TrainingNotes - err := database.DB.Where(filter).Find(¬es).Error + err := database.DB.Preload("Student").Preload("Instructor").Preload("RatingExam").Where(filter).Find(¬es).Error return notes, err } diff --git a/pkg/database/models/training_ots.go b/pkg/database/models/training_ots.go index 111277e..dfaa6c3 100644 --- a/pkg/database/models/training_ots.go +++ b/pkg/database/models/training_ots.go @@ -4,10 +4,11 @@ import ( "errors" "github.com/VATUSA/primary-api/pkg/database" "gorm.io/datatypes" + "gorm.io/gorm" "time" ) -type OTSRecord struct { +type RatingExamRecord struct { ID uint `json:"id" gorm:"primaryKey"` StudentCID uint `json:"student_cid" gorm:"index"` InstructorCID uint `json:"instructor_cid" gorm:"index"` @@ -17,7 +18,7 @@ type OTSRecord struct { CreatedAt time.Time `json:"created_at"` } -func (otsRecord *OTSRecord) BeforeCreate() error { +func (otsRecord *RatingExamRecord) BeforeCreate(tx *gorm.DB) error { // check if student CID is valid if !IsValidUser(otsRecord.StudentCID) { return errors.New("invalid student CID") @@ -26,24 +27,24 @@ func (otsRecord *OTSRecord) BeforeCreate() error { return nil } -func (otsRecord *OTSRecord) Create() error { +func (otsRecord *RatingExamRecord) Create() error { return database.DB.Create(otsRecord).Error } -func (otsRecord *OTSRecord) Update() error { +func (otsRecord *RatingExamRecord) Update() error { return database.DB.Updates(otsRecord).Error } -func (otsRecord *OTSRecord) Delete() error { +func (otsRecord *RatingExamRecord) Delete() error { return database.DB.Delete(otsRecord).Error } -func (otsRecord *OTSRecord) Get() error { +func (otsRecord *RatingExamRecord) Get() error { return database.DB.Where("id = ?", otsRecord.ID).First(otsRecord).Error } -func GetFilteredOTSRecords(filter map[string]interface{}) ([]OTSRecord, error) { - var records []OTSRecord +func GetFilteredOTSRecords(filter map[string]interface{}) ([]RatingExamRecord, error) { + var records []RatingExamRecord err := database.DB.Where(filter).Find(&records).Error return records, err } diff --git a/pkg/go-chi/middleware/auth/training-note.go b/pkg/go-chi/middleware/auth/training-note.go index 016cfa6..a724592 100644 --- a/pkg/go-chi/middleware/auth/training-note.go +++ b/pkg/go-chi/middleware/auth/training-note.go @@ -34,7 +34,7 @@ func CanCreateReadTrainingNote(next http.Handler) http.Handler { } } - log.Warnf("User %d, attempted to create/read an OTS.", credentials.User.CID) + log.Warnf("User %d, attempted to create/read a training note.", credentials.User.CID) } utils.Render(w, r, utils.ErrForbidden) @@ -59,7 +59,7 @@ func CanDeleteTrainingNote(next http.Handler) http.Handler { } } - log.Warnf("User %d, attempted to create/read an OTS.", credentials.User.CID) + log.Warnf("User %d, attempted to delete a training note.", credentials.User.CID) } utils.Render(w, r, utils.ErrForbidden) diff --git a/pkg/utils/context.go b/pkg/utils/context.go index 4deafd8..fb357b5 100644 --- a/pkg/utils/context.go +++ b/pkg/utils/context.go @@ -197,8 +197,8 @@ func GetTrainingNoteCtx(r *http.Request) *models.TrainingNotes { type OTSRecord struct{} -func GetOTSRecordCtx(r *http.Request) *models.OTSRecord { - otsr, ok := r.Context().Value(OTSRecord{}).(*models.OTSRecord) +func GetOTSRecordCtx(r *http.Request) *models.RatingExamRecord { + otsr, ok := r.Context().Value(OTSRecord{}).(*models.RatingExamRecord) if !ok { return nil } diff --git a/views/docs/docs.go b/views/docs/docs.go index 3544d02..24ea479 100644 --- a/views/docs/docs.go +++ b/views/docs/docs.go @@ -3127,6 +3127,65 @@ const docTemplate = `{ } } }, + "/facility/{FacilityID}/training/notes": { + "get": { + "description": "List all training notes for a specific facility", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List training notes for a Facility", + "parameters": [ + { + "type": "string", + "description": "Facility ID", + "name": "FacilityID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, "/training/ots/templates": { "get": { "description": "List all OTS templates", @@ -3146,7 +3205,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/training.response" + "$ref": "#/definitions/training.otsTemplateResponse" } } }, @@ -3169,6 +3228,50 @@ const docTemplate = `{ } } } + }, + "post": { + "description": "Create an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Create an OTS template", + "parameters": [ + { + "description": "Template", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/training.otsTemplateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/training.otsTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } } }, "/training/ots/templates/{id}": { @@ -3231,7 +3334,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/training.request" + "$ref": "#/definitions/training.otsTemplateRequest" } } ], @@ -3239,7 +3342,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/training.response" + "$ref": "#/definitions/training.otsTemplateResponse" } }, "400": { @@ -4668,7 +4771,326 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/user_role.Response" + "$ref": "#/definitions/user_role.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create a new user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user-roles" + ], + "summary": "Create a new user role", + "parameters": [ + { + "type": "integer", + "description": "User CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "description": "User Role", + "name": "user_role", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user_role.Request" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/user_role.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/roles/{role_id}": { + "delete": { + "description": "Remove a user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user-roles" + ], + "summary": "Remove a user role", + "parameters": [ + { + "type": "integer", + "description": "User CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Role ID", + "name": "role_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/roster": { + "get": { + "description": "Get rosters by user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "roster" + ], + "summary": "Get rosters by user", + "parameters": [ + { + "type": "integer", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/roster.Response" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/training/notes": { + "get": { + "description": "List all training notes for a specific user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List training notes for a user", + "parameters": [ + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create a new training note", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Create a new training note", + "parameters": [ + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "description": "Training Note", + "name": "note", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/trainingrecords.noteRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/training/notes/{note_id}": { + "get": { + "description": "Retrieve a specific training note by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Get a specific training note", + "parameters": [ + { + "type": "integer", + "description": "Note ID", + "name": "note_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/trainingrecords.noteResponse" } }, "400": { @@ -4677,6 +5099,24 @@ const docTemplate = `{ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -4685,8 +5125,8 @@ const docTemplate = `{ } } }, - "post": { - "description": "Create a new user role", + "delete": { + "description": "Delete a specific training note by its ID", "consumes": [ "application/json" ], @@ -4694,33 +5134,28 @@ const docTemplate = `{ "application/json" ], "tags": [ - "user-roles" + "training" ], - "summary": "Create a new user role", + "summary": "Delete a specific training note", "parameters": [ { "type": "integer", - "description": "User CID", - "name": "cid", + "description": "Note ID", + "name": "note_id", "in": "path", "required": true }, { - "description": "User Role", - "name": "user_role", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user_role.Request" - } + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true } ], "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/user_role.Response" - } + "204": { + "description": "No Content" }, "400": { "description": "Bad Request", @@ -4728,6 +5163,24 @@ const docTemplate = `{ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -4737,9 +5190,9 @@ const docTemplate = `{ } } }, - "/user/{cid}/roles/{role_id}": { - "delete": { - "description": "Remove a user role", + "/user/{cid}/training/ots": { + "get": { + "description": "List all OTS Records for user", "consumes": [ "application/json" ], @@ -4747,31 +5200,36 @@ const docTemplate = `{ "application/json" ], "tags": [ - "user-roles" + "training" ], - "summary": "Remove a user role", + "summary": "List all OTS Records for user", "parameters": [ - { - "type": "integer", - "description": "User CID", - "name": "cid", - "in": "path", - "required": true - }, { "type": "string", - "description": "Role ID", - "name": "role_id", + "description": "CID", + "name": "cid", "in": "path", "required": true } ], "responses": { - "204": { - "description": "No Content" + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.otsRecordResponse" + } + } }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", "schema": { "$ref": "#/definitions/utils.ErrResponse" } @@ -4783,11 +5241,9 @@ const docTemplate = `{ } } } - } - }, - "/user/{cid}/roster": { - "get": { - "description": "Get rosters by user", + }, + "post": { + "description": "Create a new OTS Record", "consumes": [ "application/json" ], @@ -4795,26 +5251,32 @@ const docTemplate = `{ "application/json" ], "tags": [ - "roster" + "training" ], - "summary": "Get rosters by user", + "summary": "Create a new OTS Record", "parameters": [ { - "type": "integer", + "type": "string", "description": "CID", "name": "cid", "in": "path", "required": true + }, + { + "description": "OTS Record", + "name": "otsRecordRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/trainingrecords.otsRecordRequest" + } } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/roster.Response" - } + "$ref": "#/definitions/trainingrecords.otsRecordResponse" } }, "400": { @@ -4823,6 +5285,18 @@ const docTemplate = `{ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -6652,7 +7126,22 @@ const docTemplate = `{ } } }, - "training.request": { + "time.Duration": { + "type": "integer", + "enum": [ + 1, + 1000, + 1000000, + 1000000000 + ], + "x-enum-varnames": [ + "Nanosecond", + "Microsecond", + "Millisecond", + "Second" + ] + }, + "training.otsTemplateRequest": { "type": "object", "properties": { "name": { @@ -6666,7 +7155,7 @@ const docTemplate = `{ } } }, - "training.response": { + "training.otsTemplateResponse": { "type": "object", "properties": { "created_at": { @@ -6692,6 +7181,146 @@ const docTemplate = `{ } } }, + "trainingrecords.noteRequest": { + "type": "object", + "properties": { + "duration": { + "$ref": "#/definitions/time.Duration" + }, + "facility": { + "$ref": "#/definitions/constants.FacilityID" + }, + "notes": { + "type": "string" + }, + "position": { + "type": "string" + }, + "rating_exam_id": { + "type": "integer" + }, + "session_date": { + "type": "string" + } + } + }, + "trainingrecords.noteResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/time.Duration" + } + ], + "example": 60 + }, + "facility": { + "allOf": [ + { + "$ref": "#/definitions/constants.FacilityID" + } + ], + "example": "ZDV" + }, + "id": { + "type": "integer", + "example": 1 + }, + "instructor_cid": { + "type": "integer", + "example": 1293257 + }, + "instructor_name": { + "type": "string" + }, + "notes": { + "type": "string", + "example": "Great job!" + }, + "position": { + "type": "string", + "example": "DEN_DEL" + }, + "rating_exam_id": { + "type": "integer", + "example": 1 + }, + "rating_exam_notes": { + "type": "string" + }, + "rating_exam_result": { + "type": "boolean" + }, + "session_date": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + }, + "student_cid": { + "type": "integer", + "example": 1293257 + }, + "student_name": { + "type": "string" + }, + "updated_at": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + } + } + }, + "trainingrecords.otsRecordRequest": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "notes": { + "type": "string" + }, + "result": { + "type": "boolean" + } + } + }, + "trainingrecords.otsRecordResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "data": { + "description": "Stores form fields dynamically", + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + }, + "instructor_cid": { + "type": "integer" + }, + "notes": { + "type": "string" + }, + "result": { + "description": "True = Passed, False = Failed", + "type": "boolean" + }, + "student_cid": { + "type": "integer" + } + } + }, "types.FeedbackRating": { "type": "string", "enum": [ diff --git a/views/docs/swagger.json b/views/docs/swagger.json index ce043c6..09252f2 100644 --- a/views/docs/swagger.json +++ b/views/docs/swagger.json @@ -3120,6 +3120,65 @@ } } }, + "/facility/{FacilityID}/training/notes": { + "get": { + "description": "List all training notes for a specific facility", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List training notes for a Facility", + "parameters": [ + { + "type": "string", + "description": "Facility ID", + "name": "FacilityID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, "/training/ots/templates": { "get": { "description": "List all OTS templates", @@ -3139,7 +3198,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/training.response" + "$ref": "#/definitions/training.otsTemplateResponse" } } }, @@ -3162,6 +3221,50 @@ } } } + }, + "post": { + "description": "Create an OTS template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Create an OTS template", + "parameters": [ + { + "description": "Template", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/training.otsTemplateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/training.otsTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } } }, "/training/ots/templates/{id}": { @@ -3224,7 +3327,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/training.request" + "$ref": "#/definitions/training.otsTemplateRequest" } } ], @@ -3232,7 +3335,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/training.response" + "$ref": "#/definitions/training.otsTemplateResponse" } }, "400": { @@ -4661,7 +4764,326 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/user_role.Response" + "$ref": "#/definitions/user_role.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create a new user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user-roles" + ], + "summary": "Create a new user role", + "parameters": [ + { + "type": "integer", + "description": "User CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "description": "User Role", + "name": "user_role", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user_role.Request" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/user_role.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/roles/{role_id}": { + "delete": { + "description": "Remove a user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user-roles" + ], + "summary": "Remove a user role", + "parameters": [ + { + "type": "integer", + "description": "User CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Role ID", + "name": "role_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/roster": { + "get": { + "description": "Get rosters by user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "roster" + ], + "summary": "Get rosters by user", + "parameters": [ + { + "type": "integer", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/roster.Response" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/training/notes": { + "get": { + "description": "List all training notes for a specific user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "List training notes for a user", + "parameters": [ + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + }, + "post": { + "description": "Create a new training note", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Create a new training note", + "parameters": [ + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + }, + { + "description": "Training Note", + "name": "note", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/trainingrecords.noteRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/trainingrecords.noteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + } + } + } + }, + "/user/{cid}/training/notes/{note_id}": { + "get": { + "description": "Retrieve a specific training note by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "training" + ], + "summary": "Get a specific training note", + "parameters": [ + { + "type": "integer", + "description": "Note ID", + "name": "note_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/trainingrecords.noteResponse" } }, "400": { @@ -4670,6 +5092,24 @@ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -4678,8 +5118,8 @@ } } }, - "post": { - "description": "Create a new user role", + "delete": { + "description": "Delete a specific training note by its ID", "consumes": [ "application/json" ], @@ -4687,33 +5127,28 @@ "application/json" ], "tags": [ - "user-roles" + "training" ], - "summary": "Create a new user role", + "summary": "Delete a specific training note", "parameters": [ { "type": "integer", - "description": "User CID", - "name": "cid", + "description": "Note ID", + "name": "note_id", "in": "path", "required": true }, { - "description": "User Role", - "name": "user_role", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user_role.Request" - } + "type": "string", + "description": "CID", + "name": "cid", + "in": "path", + "required": true } ], "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/user_role.Response" - } + "204": { + "description": "No Content" }, "400": { "description": "Bad Request", @@ -4721,6 +5156,24 @@ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -4730,9 +5183,9 @@ } } }, - "/user/{cid}/roles/{role_id}": { - "delete": { - "description": "Remove a user role", + "/user/{cid}/training/ots": { + "get": { + "description": "List all OTS Records for user", "consumes": [ "application/json" ], @@ -4740,31 +5193,36 @@ "application/json" ], "tags": [ - "user-roles" + "training" ], - "summary": "Remove a user role", + "summary": "List all OTS Records for user", "parameters": [ - { - "type": "integer", - "description": "User CID", - "name": "cid", - "in": "path", - "required": true - }, { "type": "string", - "description": "Role ID", - "name": "role_id", + "description": "CID", + "name": "cid", "in": "path", "required": true } ], "responses": { - "204": { - "description": "No Content" + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/trainingrecords.otsRecordResponse" + } + } }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", "schema": { "$ref": "#/definitions/utils.ErrResponse" } @@ -4776,11 +5234,9 @@ } } } - } - }, - "/user/{cid}/roster": { - "get": { - "description": "Get rosters by user", + }, + "post": { + "description": "Create a new OTS Record", "consumes": [ "application/json" ], @@ -4788,26 +5244,32 @@ "application/json" ], "tags": [ - "roster" + "training" ], - "summary": "Get rosters by user", + "summary": "Create a new OTS Record", "parameters": [ { - "type": "integer", + "type": "string", "description": "CID", "name": "cid", "in": "path", "required": true + }, + { + "description": "OTS Record", + "name": "otsRecordRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/trainingrecords.otsRecordRequest" + } } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/roster.Response" - } + "$ref": "#/definitions/trainingrecords.otsRecordResponse" } }, "400": { @@ -4816,6 +5278,18 @@ "$ref": "#/definitions/utils.ErrResponse" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/utils.ErrResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -6645,7 +7119,22 @@ } } }, - "training.request": { + "time.Duration": { + "type": "integer", + "enum": [ + 1, + 1000, + 1000000, + 1000000000 + ], + "x-enum-varnames": [ + "Nanosecond", + "Microsecond", + "Millisecond", + "Second" + ] + }, + "training.otsTemplateRequest": { "type": "object", "properties": { "name": { @@ -6659,7 +7148,7 @@ } } }, - "training.response": { + "training.otsTemplateResponse": { "type": "object", "properties": { "created_at": { @@ -6685,6 +7174,146 @@ } } }, + "trainingrecords.noteRequest": { + "type": "object", + "properties": { + "duration": { + "$ref": "#/definitions/time.Duration" + }, + "facility": { + "$ref": "#/definitions/constants.FacilityID" + }, + "notes": { + "type": "string" + }, + "position": { + "type": "string" + }, + "rating_exam_id": { + "type": "integer" + }, + "session_date": { + "type": "string" + } + } + }, + "trainingrecords.noteResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/time.Duration" + } + ], + "example": 60 + }, + "facility": { + "allOf": [ + { + "$ref": "#/definitions/constants.FacilityID" + } + ], + "example": "ZDV" + }, + "id": { + "type": "integer", + "example": 1 + }, + "instructor_cid": { + "type": "integer", + "example": 1293257 + }, + "instructor_name": { + "type": "string" + }, + "notes": { + "type": "string", + "example": "Great job!" + }, + "position": { + "type": "string", + "example": "DEN_DEL" + }, + "rating_exam_id": { + "type": "integer", + "example": 1 + }, + "rating_exam_notes": { + "type": "string" + }, + "rating_exam_result": { + "type": "boolean" + }, + "session_date": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + }, + "student_cid": { + "type": "integer", + "example": 1293257 + }, + "student_name": { + "type": "string" + }, + "updated_at": { + "type": "string", + "example": "2021-01-01T00:00:00Z" + } + } + }, + "trainingrecords.otsRecordRequest": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "notes": { + "type": "string" + }, + "result": { + "type": "boolean" + } + } + }, + "trainingrecords.otsRecordResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "data": { + "description": "Stores form fields dynamically", + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + }, + "instructor_cid": { + "type": "integer" + }, + "notes": { + "type": "string" + }, + "result": { + "description": "True = Passed, False = Failed", + "type": "boolean" + }, + "student_cid": { + "type": "integer" + } + } + }, "types.FeedbackRating": { "type": "string", "enum": [ diff --git a/views/docs/swagger.yaml b/views/docs/swagger.yaml index a12ef3a..ee6d9c5 100644 --- a/views/docs/swagger.yaml +++ b/views/docs/swagger.yaml @@ -1174,7 +1174,19 @@ definitions: example: "2021-01-01T00:00:00Z" type: string type: object - training.request: + time.Duration: + enum: + - 1 + - 1000 + - 1000000 + - 1000000000 + type: integer + x-enum-varnames: + - Nanosecond + - Microsecond + - Millisecond + - Second + training.otsTemplateRequest: properties: name: type: string @@ -1183,7 +1195,7 @@ definitions: type: integer type: array type: object - training.response: + training.otsTemplateResponse: properties: created_at: type: string @@ -1200,6 +1212,99 @@ definitions: updated_at: type: string type: object + trainingrecords.noteRequest: + properties: + duration: + $ref: '#/definitions/time.Duration' + facility: + $ref: '#/definitions/constants.FacilityID' + notes: + type: string + position: + type: string + rating_exam_id: + type: integer + session_date: + type: string + type: object + trainingrecords.noteResponse: + properties: + created_at: + example: "2021-01-01T00:00:00Z" + type: string + duration: + allOf: + - $ref: '#/definitions/time.Duration' + example: 60 + facility: + allOf: + - $ref: '#/definitions/constants.FacilityID' + example: ZDV + id: + example: 1 + type: integer + instructor_cid: + example: 1293257 + type: integer + instructor_name: + type: string + notes: + example: Great job! + type: string + position: + example: DEN_DEL + type: string + rating_exam_id: + example: 1 + type: integer + rating_exam_notes: + type: string + rating_exam_result: + type: boolean + session_date: + example: "2021-01-01T00:00:00Z" + type: string + student_cid: + example: 1293257 + type: integer + student_name: + type: string + updated_at: + example: "2021-01-01T00:00:00Z" + type: string + type: object + trainingrecords.otsRecordRequest: + properties: + data: + items: + type: integer + type: array + notes: + type: string + result: + type: boolean + type: object + trainingrecords.otsRecordResponse: + properties: + created_at: + type: string + data: + description: Stores form fields dynamically + items: + type: integer + type: array + id: + type: integer + instructor_cid: + type: integer + notes: + type: string + result: + description: True = Passed, False = Failed + type: boolean + student_cid: + type: integer + type: object types.FeedbackRating: enum: - unsatisfactory @@ -3536,6 +3641,45 @@ paths: summary: Delete a roster tags: - roster + /facility/{FacilityID}/training/notes: + get: + consumes: + - application/json + description: List all training notes for a specific facility + parameters: + - description: Facility ID + in: path + name: FacilityID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/trainingrecords.noteResponse' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: List training notes for a Facility + tags: + - training /training/ots/templates: get: consumes: @@ -3548,7 +3692,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/training.response' + $ref: '#/definitions/training.otsTemplateResponse' type: array "401": description: Unauthorized @@ -3565,6 +3709,35 @@ paths: summary: List all OTS templates tags: - training + post: + consumes: + - application/json + description: Create an OTS template + parameters: + - description: Template + in: body + name: template + required: true + schema: + $ref: '#/definitions/training.otsTemplateRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/training.otsTemplateResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Create an OTS template + tags: + - training /training/ots/templates/{id}: delete: consumes: @@ -3603,14 +3776,14 @@ paths: name: template required: true schema: - $ref: '#/definitions/training.request' + $ref: '#/definitions/training.otsTemplateRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/training.response' + $ref: '#/definitions/training.otsTemplateResponse' "400": description: Bad Request schema: @@ -4642,6 +4815,253 @@ paths: summary: Get rosters by user tags: - roster + /user/{cid}/training/notes: + get: + consumes: + - application/json + description: List all training notes for a specific user + parameters: + - description: CID + in: path + name: cid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/trainingrecords.noteResponse' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: List training notes for a user + tags: + - training + post: + consumes: + - application/json + description: Create a new training note + parameters: + - description: CID + in: path + name: cid + required: true + type: string + - description: Training Note + in: body + name: note + required: true + schema: + $ref: '#/definitions/trainingrecords.noteRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/trainingrecords.noteResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Create a new training note + tags: + - training + /user/{cid}/training/notes/{note_id}: + delete: + consumes: + - application/json + description: Delete a specific training note by its ID + parameters: + - description: Note ID + in: path + name: note_id + required: true + type: integer + - description: CID + in: path + name: cid + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Delete a specific training note + tags: + - training + get: + consumes: + - application/json + description: Retrieve a specific training note by its ID + parameters: + - description: Note ID + in: path + name: note_id + required: true + type: integer + - description: CID + in: path + name: cid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/trainingrecords.noteResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Get a specific training note + tags: + - training + /user/{cid}/training/ots: + get: + consumes: + - application/json + description: List all OTS Records for user + parameters: + - description: CID + in: path + name: cid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/trainingrecords.otsRecordResponse' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: List all OTS Records for user + tags: + - training + post: + consumes: + - application/json + description: Create a new OTS Record + parameters: + - description: CID + in: path + name: cid + required: true + type: string + - description: OTS Record + in: body + name: otsRecordRequest + required: true + schema: + $ref: '#/definitions/trainingrecords.otsRecordRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/trainingrecords.otsRecordResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.ErrResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/utils.ErrResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/utils.ErrResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.ErrResponse' + summary: Create a new OTS Record + tags: + - training /user/{cid}/user-flag: delete: consumes: diff --git a/views/router.go b/views/router.go index 37acc27..a6e69ee 100644 --- a/views/router.go +++ b/views/router.go @@ -31,7 +31,7 @@ import ( // @name x-api-key func Router(r chi.Router, cfg *config.Config) { - v3.Router(r, cfg) + v3.Router(r) docs.SwaggerInfo.Host = cfg.API.BaseURL[strings.Index(cfg.API.BaseURL, "://")+3:] diff --git a/views/v3/facility/router.go b/views/v3/facility/router.go index c775e30..2bc16ad 100644 --- a/views/v3/facility/router.go +++ b/views/v3/facility/router.go @@ -13,6 +13,7 @@ import ( "github.com/VATUSA/primary-api/views/v3/news" "github.com/VATUSA/primary-api/views/v3/roster" roster_request "github.com/VATUSA/primary-api/views/v3/roster-request" + trainingrecords "github.com/VATUSA/primary-api/views/v3/training-records" "github.com/go-chi/chi/v5" "net/http" ) @@ -60,6 +61,8 @@ func Router(r chi.Router) { r.Route("/roster-request", func(r chi.Router) { roster_request.Router(r) }) + + r.Get("/training/notes", trainingrecords.ListNotesForFac) }) } diff --git a/views/v3/router.go b/views/v3/router.go index 7dfa85d..3aad5e5 100644 --- a/views/v3/router.go +++ b/views/v3/router.go @@ -1,7 +1,6 @@ package v3 import ( - "github.com/VATUSA/primary-api/pkg/config" "github.com/VATUSA/primary-api/views/v3/event" "github.com/VATUSA/primary-api/views/v3/facility" "github.com/VATUSA/primary-api/views/v3/training" @@ -9,7 +8,7 @@ import ( "github.com/go-chi/chi/v5" ) -func Router(r chi.Router, cfg *config.Config) { +func Router(r chi.Router) { r.Route("/v3", func(r chi.Router) { r.Route("/user", func(r chi.Router) { user.Router(r) diff --git a/views/v3/training-records/note.go b/views/v3/training-records/note.go index cf5097b..6a22a7c 100644 --- a/views/v3/training-records/note.go +++ b/views/v3/training-records/note.go @@ -13,13 +13,12 @@ import ( ) type noteRequest struct { - Facility constants.FacilityID `json:"facility"` - Position string `json:"position"` - Duration time.Duration `json:"duration"` - Score uint `json:"score"` - Notes string `json:"notes"` - OTSRecordID uint `json:"ots_record_id"` - SessionDate time.Time `json:"session_date"` + Facility constants.FacilityID `json:"facility"` + Position string `json:"position"` + Duration time.Duration `json:"duration"` + Notes string `json:"notes"` + RatingExamID uint `json:"rating_exam_id"` + SessionDate time.Time `json:"session_date"` } func (req *noteRequest) Validate() error { @@ -35,10 +34,25 @@ func (req *noteRequest) Bind(r *http.Request) error { type noteResponse struct { *models.TrainingNotes + StudentName string `json:"student_name"` + InstructorName string `json:"instructor_name"` + RatingExamNotes string `json:"rating_exam_notes"` + RatingExamResult bool `json:"rating_exam_result"` } func newNoteResponse(note *models.TrainingNotes) *noteResponse { - return ¬eResponse{note} + resp := ¬eResponse{ + TrainingNotes: note, + StudentName: note.Student.FirstName + " " + note.Student.LastName, + InstructorName: note.Instructor.FirstName + " " + note.Instructor.LastName, + } + + if note.RatingExam != nil { + resp.RatingExamNotes = note.RatingExam.Notes + resp.RatingExamResult = note.RatingExam.Result + } + + return resp } func (res *noteResponse) Render(w http.ResponseWriter, r *http.Request) error { @@ -78,13 +92,12 @@ func createNote(w http.ResponseWriter, r *http.Request) { } note := &models.TrainingNotes{ - Facility: data.Facility, - Position: data.Position, - Duration: data.Duration, - Score: data.Score, - Notes: data.Notes, - OTSRecordID: data.OTSRecordID, - SessionDate: data.SessionDate, + Facility: data.Facility, + Position: data.Position, + Duration: data.Duration, + Notes: data.Notes, + RatingExamID: data.RatingExamID, + SessionDate: data.SessionDate, } note.StudentCID = utils.GetUserCtx(r).CID @@ -106,7 +119,7 @@ func createNote(w http.ResponseWriter, r *http.Request) { // @Accept json // @Produce json // @Param cid path string true "CID" -// @Success 200 {object} noteResponse +// @Success 200 {object} []noteResponse // @Failure 400 {object} utils.ErrResponse // @Failure 401 {object} utils.ErrResponse // @Failure 403 {object} utils.ErrResponse @@ -128,6 +141,35 @@ func listNotes(w http.ResponseWriter, r *http.Request) { } } +// ListNotesForFac godoc +// @Summary List training notes for a Facility +// @Description List all training notes for a specific facility +// @Tags training +// @Accept json +// @Produce json +// @Param FacilityID path string true "Facility ID" +// @Success 200 {object} []noteResponse +// @Failure 400 {object} utils.ErrResponse +// @Failure 401 {object} utils.ErrResponse +// @Failure 403 {object} utils.ErrResponse +// @Failure 500 {object} utils.ErrResponse +// @Router /facility/{FacilityID}/training/notes [get] +func ListNotesForFac(w http.ResponseWriter, r *http.Request) { + fac := utils.GetFacilityCtx(r) + + // Fetch the training notes for the facility + notes, err := models.GetFilteredTrainingNotes(map[string]interface{}{"facility": fac.ID}) + if err != nil { + utils.Render(w, r, utils.ErrInternalServerWithErr(err)) + return + } + + if err := render.RenderList(w, r, newNoteListResponse(notes)); err != nil { + utils.Render(w, r, utils.ErrRender(err)) + return + } +} + // getNote godoc // @Summary Get a specific training note // @Description Retrieve a specific training note by its ID diff --git a/views/v3/training-records/ots_record.go b/views/v3/training-records/ots_record.go index 99a2e0d..2af8175 100644 --- a/views/v3/training-records/ots_record.go +++ b/views/v3/training-records/ots_record.go @@ -29,21 +29,21 @@ func (req *otsRecordRequest) Bind(r *http.Request) error { } type otsRecordResponse struct { - *models.OTSRecord + *models.RatingExamRecord } -func newOTSRecordResponse(temp *models.OTSRecord) *otsRecordResponse { +func newOTSRecordResponse(temp *models.RatingExamRecord) *otsRecordResponse { return &otsRecordResponse{temp} } func (res *otsRecordResponse) Render(w http.ResponseWriter, r *http.Request) error { - if res.OTSRecord == nil { + if res.RatingExamRecord == nil { return errors.New("OTS Record not found") } return nil } -func newOTSRecordList(templates []models.OTSRecord) []render.Renderer { +func newOTSRecordList(templates []models.RatingExamRecord) []render.Renderer { list := []render.Renderer{} for idx := range templates { list = append(list, newOTSRecordResponse(&templates[idx])) @@ -73,7 +73,7 @@ func createOTSRecord(w http.ResponseWriter, r *http.Request) { return } - record := &models.OTSRecord{ + record := &models.RatingExamRecord{ StudentCID: cid, Data: data.Data, Notes: data.Notes, diff --git a/views/v3/user/login.go b/views/v3/user/login.go index e42f1bc..b451fc5 100644 --- a/views/v3/user/login.go +++ b/views/v3/user/login.go @@ -25,6 +25,7 @@ import ( func GetLogin(w http.ResponseWriter, r *http.Request) { state, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 64) if err != nil { + log.WithError(err).Error("Error generating random state") utils.Render(w, r, utils.ErrInternalServer) return } @@ -34,6 +35,7 @@ func GetLogin(w http.ResponseWriter, r *http.Request) { "redirect": r.URL.Query().Get("redirect"), }) if err != nil { + log.WithError(err).Error("Error creating session") utils.Render(w, r, utils.ErrInternalServer) return } From 3858dc2456d40d662cec19822fd050629c8e1e72 Mon Sep 17 00:00:00 2001 From: Raaj Patel Date: Tue, 11 Nov 2025 19:59:45 -0600 Subject: [PATCH 3/3] bug fixes --- pkg/go-chi/middleware/auth/feedback.go | 2 +- pkg/vatsim/api/base.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/go-chi/middleware/auth/feedback.go b/pkg/go-chi/middleware/auth/feedback.go index 62e84a5..b921337 100644 --- a/pkg/go-chi/middleware/auth/feedback.go +++ b/pkg/go-chi/middleware/auth/feedback.go @@ -120,7 +120,7 @@ func CanLeaveFeedback(next http.Handler) http.Handler { if credentials.User != nil { if req.Status != types.Pending { - log.Error("User %d, attempted to create feedback with status: %s. No permissions.", credentials.User.CID, req.Status) + log.Errorf("User %d, attempted to create feedback with status: %s. No permissions.", credentials.User.CID, req.Status) utils.Render(w, r, utils.ErrForbidden) return } diff --git a/pkg/vatsim/api/base.go b/pkg/vatsim/api/base.go index 06dae60..fd04162 100644 --- a/pkg/vatsim/api/base.go +++ b/pkg/vatsim/api/base.go @@ -34,7 +34,7 @@ func GetLocation(cid uint) (Location, error) { } if resp.StatusCode > 299 { - log.Warnf("Failed to get division for %s: %s", cid, body) + log.Warnf("Failed to get division for %d: %s", cid, body) return Location{}, fmt.Errorf("invalid status code: %d", resp.StatusCode) } @@ -76,7 +76,7 @@ func GetHours(cid uint) (UserStats, error) { } if resp.StatusCode > 299 { - log.Warnf("Failed to get stats for %s: %s", cid, body) + log.Warnf("Failed to get stats for %d: %s", cid, body) return UserStats{}, fmt.Errorf("invalid status code: %d", resp.StatusCode) } @@ -121,7 +121,7 @@ func GetATCConnections(cid uint) (ATCConnections, error) { } if resp.StatusCode > 299 { - log.Warnf("Failed to get stats for %s: %s", cid, body) + log.Warnf("Failed to get stats for %d: %s", cid, body) return ATCConnections{}, fmt.Errorf("invalid status code: %d", resp.StatusCode) }