Scouting app: Add "actions" to database for 2023 scouting changes

Signed-off-by: Sabina Leaver <100027607@mvla.net>
Change-Id: If37799307e6e7e41c3936d6de336d01bb20f2802
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 8a28db4..121ba52 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -66,6 +66,34 @@
 	CollectedBy string
 }
 
+type Stats2023 struct {
+	TeamNumber                                                     string `gorm:"primaryKey"`
+	MatchNumber                                                    int32  `gorm:"primaryKey"`
+	SetNumber                                                      int32  `gorm:"primaryKey"`
+	CompLevel                                                      string `gorm:"primaryKey"`
+	StartingQuadrant                                               int32
+	LowCubesAuto, MiddleCubesAuto, HighCubesAuto, CubesDroppedAuto int32
+	LowConesAuto, MiddleConesAuto, HighConesAuto, ConesDroppedAuto int32
+	LowCubes, MiddleCubes, HighCubes, CubesDropped                 int32
+	LowCones, MiddleCones, HighCones, ConesDropped                 int32
+	AvgCycle                                                       int32
+	// The username of the person who collected these statistics.
+	// "unknown" if submitted without logging in.
+	// Empty if the stats have not yet been collected.
+	CollectedBy string
+}
+
+type Action struct {
+	TeamNumber      string `gorm:"primaryKey"`
+	MatchNumber     int32  `gorm:"primaryKey"`
+	SetNumber       int32  `gorm:"primaryKey"`
+	CompLevel       string `gorm:"primaryKey"`
+	CompletedAction []byte
+	// This contains a serialized scouting.webserver.requests.ActionType flatbuffer.
+	TimeStamp   int32 `gorm:"primaryKey"`
+	CollectedBy string
+}
+
 type NotesData struct {
 	ID           uint `gorm:"primaryKey"`
 	TeamNumber   int32
@@ -113,7 +141,7 @@
 		return nil, errors.New(fmt.Sprint("Failed to connect to postgres: ", err))
 	}
 
-	err = database.AutoMigrate(&Match{}, &Shift{}, &Stats{}, &NotesData{}, &Ranking{}, &DriverRankingData{})
+	err = database.AutoMigrate(&Match{}, &Shift{}, &Stats{}, &Stats2023{}, &Action{}, &NotesData{}, &Ranking{}, &DriverRankingData{})
 	if err != nil {
 		database.Delete()
 		return nil, errors.New(fmt.Sprint("Failed to create/migrate tables: ", err))
@@ -148,6 +176,13 @@
 	return result.Error
 }
 
+func (database *Database) AddAction(a Action) error {
+	result := database.Clauses(clause.OnConflict{
+		UpdateAll: true,
+	}).Create(&a)
+	return result.Error
+}
+
 func (database *Database) AddToStats(s Stats) error {
 	matches, err := database.QueryMatches(s.TeamNumber)
 	if err != nil {
@@ -176,6 +211,28 @@
 	return result.Error
 }
 
+func (database *Database) AddToStats2023(s Stats2023) error {
+	matches, err := database.QueryMatchesString(s.TeamNumber)
+	if err != nil {
+		return err
+	}
+	foundMatch := false
+	for _, match := range matches {
+		if match.MatchNumber == s.MatchNumber {
+			foundMatch = true
+			break
+		}
+	}
+	if !foundMatch {
+		return errors.New(fmt.Sprint(
+			"Failed to find team ", s.TeamNumber,
+			" in match ", s.MatchNumber, " in the schedule."))
+	}
+
+	result := database.Create(&s)
+	return result.Error
+}
+
 func (database *Database) AddOrUpdateRankings(r Ranking) error {
 	result := database.Clauses(clause.OnConflict{
 		UpdateAll: true,
@@ -207,6 +264,12 @@
 	return shifts, result.Error
 }
 
+func (database *Database) ReturnActions() ([]Action, error) {
+	var actions []Action
+	result := database.Find(&actions)
+	return actions, result.Error
+}
+
 // Packs the stats. This really just consists of taking the individual auto
 // ball booleans and turning them into an array. The individual booleans are
 // cleared so that they don't affect struct comparisons.
@@ -249,6 +312,14 @@
 	return matches, result.Error
 }
 
+func (database *Database) QueryMatchesString(teamNumber_ string) ([]Match, error) {
+	var matches []Match
+	result := database.
+		Where("r1 = $1 OR r2 = $1 OR r3 = $1 OR b1 = $1 OR b2 = $1 OR b3 = $1", teamNumber_).
+		Find(&matches)
+	return matches, result.Error
+}
+
 func (database *Database) QueryAllShifts(matchNumber_ int) ([]Shift, error) {
 	var shifts []Shift
 	result := database.Where("match_number = ?", matchNumber_).Find(&shifts)
@@ -265,6 +336,13 @@
 	return stats, result.Error
 }
 
+func (database *Database) QueryActions(teamNumber_ int) ([]Action, error) {
+	var actions []Action
+	result := database.
+		Where("team_number = ?", teamNumber_).Find(&actions)
+	return actions, result.Error
+}
+
 func (database *Database) QueryNotes(TeamNumber int32) ([]string, error) {
 	var rawNotes []NotesData
 	result := database.Where("team_number = ?", TeamNumber).Find(&rawNotes)
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index 460b177..642fb6d 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -658,6 +658,54 @@
 	}
 }
 
+func TestReturnActionsDB(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+	correct := []Action{
+		Action{
+			TeamNumber: "1235", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), TimeStamp: 0000, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1236", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), TimeStamp: 0321, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1237", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), TimeStamp: 0222, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1238", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), TimeStamp: 0110, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1239", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), TimeStamp: 0004, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1233", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), TimeStamp: 0004, CollectedBy: "",
+		},
+	}
+
+	err := fixture.db.AddToMatch(Match{
+		MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+		R1: 1235, R2: 1236, R3: 1237, B1: 1238, B2: 1239, B3: 1233})
+	check(t, err, "Failed to add match")
+
+	for i := 0; i < len(correct); i++ {
+		err = fixture.db.AddAction(correct[i])
+		check(t, err, fmt.Sprint("Failed to add to actions ", i))
+	}
+
+	got, err := fixture.db.ReturnActions()
+	check(t, err, "Failed ReturnActions()")
+
+	if !reflect.DeepEqual(correct, got) {
+		t.Errorf("Got %#v,\nbut expected %#v.", got, correct)
+	}
+}
+
 func TestRankingsDbUpdate(t *testing.T) {
 	fixture := createDatabase(t)
 	defer fixture.TearDown()
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
index 53bc385..1d90a78 100644
--- a/scouting/webserver/requests/BUILD
+++ b/scouting/webserver/requests/BUILD
@@ -26,6 +26,8 @@
         "//scouting/webserver/requests/messages:request_notes_for_team_response_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_actions_go_fbs",
+        "//scouting/webserver/requests/messages:submit_actions_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_driver_ranking_go_fbs",
diff --git a/scouting/webserver/requests/messages/BUILD b/scouting/webserver/requests/messages/BUILD
index 392ee80..fcda246 100644
--- a/scouting/webserver/requests/messages/BUILD
+++ b/scouting/webserver/requests/messages/BUILD
@@ -27,6 +27,8 @@
     "submit_shift_schedule_response",
     "submit_driver_ranking",
     "submit_driver_ranking_response",
+    "submit_actions",
+    "submit_actions_response",
 )
 
 filegroup(
diff --git a/scouting/webserver/requests/messages/request_data_scouting_response.fbs b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
index 071a848..6048210 100644
--- a/scouting/webserver/requests/messages/request_data_scouting_response.fbs
+++ b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
@@ -49,4 +49,4 @@
     stats_list:[Stats] (id:0);
 }
 
-root_type RequestDataScoutingResponse;
+root_type RequestDataScoutingResponse;
\ No newline at end of file
diff --git a/scouting/webserver/requests/messages/submit_actions.fbs b/scouting/webserver/requests/messages/submit_actions.fbs
new file mode 100644
index 0000000..9d9efa4
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_actions.fbs
@@ -0,0 +1,53 @@
+namespace scouting.webserver.requests;
+
+table StartMatchAction {
+    position:int (id:0);
+}
+
+enum ObjectType: short {
+    kCube,
+    kCone
+}
+
+enum ScoreLevel: short {
+    kLow,
+    kMiddle,
+    kHigh
+}
+
+table PickupObjectAction {
+    object_type:ObjectType (id:0);
+    auto:bool (id:1);
+}
+
+table PlaceObjectAction {
+    object_type:ObjectType (id:0);
+    score_level:ScoreLevel (id:1);
+    auto:bool (id:2);
+}
+
+table RobotDeathAction {
+    robot_on:bool (id:0);
+}
+
+table EndMatchAction {
+}
+
+union ActionType {
+    StartMatchAction,
+    PickupObjectAction,
+    PlaceObjectAction,
+    RobotDeathAction,
+    EndMatchAction
+}
+
+table Action {
+    timestamp:int (id:0);
+    action_taken:ActionType (id:2);
+}
+
+table SubmitActions {
+    actions_list:[Action] (id:0);
+}
+
+root_type SubmitActions;
\ No newline at end of file
diff --git a/scouting/webserver/requests/messages/submit_actions_response.fbs b/scouting/webserver/requests/messages/submit_actions_response.fbs
new file mode 100644
index 0000000..4079914
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_actions_response.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table SubmitActionsResponse {
+    // empty response
+}
+
+root_type SubmitActionsResponse;
\ No newline at end of file
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index a7f1a77..11fd317 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -29,6 +29,8 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
@@ -65,6 +67,8 @@
 type SubmitShiftScheduleResponseT = submit_shift_schedule_response.SubmitShiftScheduleResponseT
 type SubmitDriverRanking = submit_driver_ranking.SubmitDriverRanking
 type SubmitDriverRankingResponseT = submit_driver_ranking_response.SubmitDriverRankingResponseT
+type SubmitActions = submit_actions.SubmitActions
+type SubmitActionsResponseT = submit_actions_response.SubmitActionsResponseT
 
 // The interface we expect the database abstraction to conform to.
 // We use an interface here because it makes unit testing easier.