[Scouting App] Note scout multiple robots and add keywords
Note scouts often scout multiple robots so the scouting app should support it and
checkboxes were added to select keywords/tags. These tags will likely have to be modified
according to 2023's game.
Mobile UI Preview: https://ibb.co/QdFxwGj
Change-Id: If4fcb3ee97da5f52e428cb0a4b0a8401b4700a02
Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 37c8a6b..82f7fa8 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -67,9 +67,15 @@
}
type NotesData struct {
- ID uint `gorm:"primaryKey"`
- TeamNumber int32
- Notes string
+ ID uint `gorm:"primaryKey"`
+ TeamNumber int32
+ Notes string
+ GoodDriving bool
+ BadDriving bool
+ SketchyClimb bool
+ SolidClimb bool
+ GoodDefense bool
+ BadDefense bool
}
type Ranking struct {
@@ -253,10 +259,16 @@
return rankins, result.Error
}
-func (database *Database) AddNotes(teamNumber int, data string) error {
+func (database *Database) AddNotes(data NotesData) error {
result := database.Create(&NotesData{
- TeamNumber: int32(teamNumber),
- Notes: data,
+ TeamNumber: data.TeamNumber,
+ Notes: data.Notes,
+ GoodDriving: data.GoodDriving,
+ BadDriving: data.BadDriving,
+ SketchyClimb: data.SketchyClimb,
+ SolidClimb: data.SolidClimb,
+ GoodDefense: data.GoodDefense,
+ BadDefense: data.BadDefense,
})
return result.Error
}
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index 77a8bd6..1e11008 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -715,11 +715,11 @@
expected := []string{"Note 1", "Note 3"}
- err := fixture.db.AddNotes(1234, "Note 1")
+ err := fixture.db.AddNotes(NotesData{TeamNumber: 1234, Notes: "Note 1", GoodDriving: true, BadDriving: false, SketchyClimb: false, SolidClimb: true, GoodDefense: false, BadDefense: true})
check(t, err, "Failed to add Note")
- err = fixture.db.AddNotes(1235, "Note 2")
+ err = fixture.db.AddNotes(NotesData{TeamNumber: 1235, Notes: "Note 2", GoodDriving: false, BadDriving: true, SketchyClimb: false, SolidClimb: true, GoodDefense: false, BadDefense: false})
check(t, err, "Failed to add Note")
- err = fixture.db.AddNotes(1234, "Note 3")
+ err = fixture.db.AddNotes(NotesData{TeamNumber: 1234, Notes: "Note 3", GoodDriving: true, BadDriving: false, SketchyClimb: false, SolidClimb: true, GoodDefense: true, BadDefense: false})
check(t, err, "Failed to add Note")
actual, err := fixture.db.QueryNotes(1234)
diff --git a/scouting/webserver/requests/messages/submit_notes.fbs b/scouting/webserver/requests/messages/submit_notes.fbs
index cf111b3..1498e26 100644
--- a/scouting/webserver/requests/messages/submit_notes.fbs
+++ b/scouting/webserver/requests/messages/submit_notes.fbs
@@ -3,6 +3,12 @@
table SubmitNotes {
team:int (id: 0);
notes:string (id: 1);
+ good_driving:bool (id: 2);
+ bad_driving:bool (id: 3);
+ sketchy_climb:bool (id: 4);
+ solid_climb:bool (id: 5);
+ good_defense:bool (id: 6);
+ bad_defense:bool (id: 7);
}
root_type SubmitNotes;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 0ffeaee..e33e82d 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -67,7 +67,7 @@
QueryAllShifts(int) ([]db.Shift, error)
QueryStats(int) ([]db.Stats, error)
QueryNotes(int32) ([]string, error)
- AddNotes(int, string) error
+ AddNotes(db.NotesData) error
}
type ScrapeMatchList func(int32, string) ([]scraping.Match, error)
@@ -470,7 +470,16 @@
return
}
- err = handler.db.AddNotes(int(request.Team()), string(request.Notes()))
+ err = handler.db.AddNotes(db.NotesData{
+ TeamNumber: request.Team(),
+ Notes: string(request.Notes()),
+ GoodDriving: bool(request.GoodDriving()),
+ BadDriving: bool(request.BadDriving()),
+ SketchyClimb: bool(request.SketchyClimb()),
+ SolidClimb: bool(request.SolidClimb()),
+ GoodDefense: bool(request.GoodDefense()),
+ BadDefense: bool(request.BadDefense()),
+ })
if err != nil {
respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert notes: %v", err))
return
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index 24c99f6..85ab916 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -314,8 +314,14 @@
builder := flatbuffers.NewBuilder(1024)
builder.Finish((&submit_notes.SubmitNotesT{
- Team: 971,
- Notes: "Notes",
+ Team: 971,
+ Notes: "Notes",
+ GoodDriving: true,
+ BadDriving: false,
+ SketchyClimb: true,
+ SolidClimb: false,
+ GoodDefense: true,
+ BadDefense: false,
}).Pack(builder))
_, err := debug.SubmitNotes("http://localhost:8080", builder.FinishedBytes())
@@ -324,7 +330,16 @@
}
expected := []db.NotesData{
- {TeamNumber: 971, Notes: "Notes"},
+ {
+ TeamNumber: 971,
+ Notes: "Notes",
+ GoodDriving: true,
+ BadDriving: false,
+ SketchyClimb: true,
+ SolidClimb: false,
+ GoodDefense: true,
+ BadDefense: false,
+ },
}
if !reflect.DeepEqual(database.notes, expected) {
@@ -335,8 +350,14 @@
func TestRequestNotes(t *testing.T) {
database := MockDatabase{
notes: []db.NotesData{{
- TeamNumber: 971,
- Notes: "Notes",
+ TeamNumber: 971,
+ Notes: "Notes",
+ GoodDriving: true,
+ BadDriving: false,
+ SketchyClimb: true,
+ SolidClimb: false,
+ GoodDefense: true,
+ BadDefense: false,
}},
}
scoutingServer := server.NewScoutingServer()
@@ -594,11 +615,8 @@
return results, nil
}
-func (database *MockDatabase) AddNotes(teamNumber int, notes string) error {
- database.notes = append(database.notes, db.NotesData{
- TeamNumber: int32(teamNumber),
- Notes: notes,
- })
+func (database *MockDatabase) AddNotes(data db.NotesData) error {
+ database.notes = append(database.notes, data)
return nil
}
diff --git a/scouting/www/notes/notes.component.css b/scouting/www/notes/notes.component.css
index 869bdab..67b2351 100644
--- a/scouting/www/notes/notes.component.css
+++ b/scouting/www/notes/notes.component.css
@@ -6,7 +6,6 @@
width: calc(100% - 20px);
}
-.buttons {
- display: flex;
- justify-content: space-between;
+.container-main {
+ padding-left: 20px;
}
diff --git a/scouting/www/notes/notes.component.ts b/scouting/www/notes/notes.component.ts
index 0f0eb82..86df425 100644
--- a/scouting/www/notes/notes.component.ts
+++ b/scouting/www/notes/notes.component.ts
@@ -9,88 +9,146 @@
import {SubmitNotes} from 'org_frc971/scouting/webserver/requests/messages/submit_notes_generated';
import {SubmitNotesResponse} from 'org_frc971/scouting/webserver/requests/messages/submit_notes_response_generated';
+/*
+For new games, the keywords being used will likely need to be updated.
+To update the keywords complete the following:
+ 1) Update the Keywords Interface and KEYWORD_CHECKBOX_LABELS in notes.component.ts
+ The keys of Keywords and KEYWORD_CHECKBOX_LABELS should match.
+ 2) In notes.component.ts, update the setTeamNumber() method with the new keywords.
+ 3) Add/Edit the new keywords in /scouting/webserver/requests/messages/submit_notes.fbs.
+ 4) In notes.component.ts, update the submitData() method with the newKeywords
+ so that it matches the updated flatbuffer
+ 5) In db.go, update the NotesData struct and the
+ AddNotes method with the new keywords
+ 6) In db_test.go update the TestNotes method so the test uses the keywords
+ 7) Update the submitNoteScoutingHandler in requests.go with the new keywords
+ 8) Finally, update the corresponding test in requests_test.go (TestSubmitNotes)
+
+ Note: If you change the number of keywords you might need to
+ update how they are displayed in notes.ng.html
+*/
+
+// TeamSelection: Display form to add a team to the teams being scouted.
+// Data: Display the note textbox and keyword selection form
+// for all the teams being scouted.
type Section = 'TeamSelection' | 'Data';
-interface Note {
- readonly data: string;
+// Every keyword checkbox corresponds to a boolean.
+// If the boolean is True, the checkbox is selected
+// and the note scout saw that the robot being scouted
+// displayed said property (ex. Driving really well -> goodDriving)
+interface Keywords {
+ goodDriving: boolean;
+ badDriving: boolean;
+ sketchyClimb: boolean;
+ solidClimb: boolean;
+ goodDefense: boolean;
+ badDefense: boolean;
}
+interface Input {
+ teamNumber: number;
+ notesData: string;
+ keywordsData: Keywords;
+}
+
+const KEYWORD_CHECKBOX_LABELS = {
+ goodDriving: 'Good Driving',
+ badDriving: 'Bad Driving',
+ solidClimb: 'Solid Climb',
+ sketchyClimb: 'Sketchy Climb',
+ goodDefense: 'Good Defense',
+ badDefense: 'Bad Defense',
+} as const;
+
@Component({
selector: 'frc971-notes',
templateUrl: './notes.ng.html',
styleUrls: ['../common.css', './notes.component.css'],
})
export class Notes {
+ // Re-export KEYWORD_CHECKBOX_LABELS so that we can
+ // use it in the checkbox properties.
+ readonly KEYWORD_CHECKBOX_LABELS = KEYWORD_CHECKBOX_LABELS;
+
+ // Necessary in order to iterate the keys of KEYWORD_CHECKBOX_LABELS.
+ Object = Object;
+
section: Section = 'TeamSelection';
- notes: Note[] = [];
errorMessage = '';
+ teamNumberSelection: number = 971;
- teamNumber: number = 971;
- newData = '';
+ // Data inputted by user is stored in this array.
+ // Includes the team number, notes, and keyword selection.
+ newData: Input[] = [];
- async setTeamNumber() {
- const builder = new Builder();
- RequestNotesForTeam.startRequestNotesForTeam(builder);
- RequestNotesForTeam.addTeam(builder, this.teamNumber);
- builder.finish(RequestNotesForTeam.endRequestNotesForTeam(builder));
+ setTeamNumber() {
+ let data: Input = {
+ teamNumber: this.teamNumberSelection,
+ notesData: '',
+ keywordsData: {
+ goodDriving: false,
+ badDriving: false,
+ solidClimb: false,
+ sketchyClimb: false,
+ goodDefense: false,
+ badDefense: false,
+ },
+ };
- const buffer = builder.asUint8Array();
- const res = await fetch('/requests/request/notes_for_team', {
- method: 'POST',
- body: buffer,
- });
+ this.newData.push(data);
+ this.section = 'Data';
+ }
- const resBuffer = await res.arrayBuffer();
- const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
-
- if (res.ok) {
- this.notes = [];
- const parsedResponse =
- RequestNotesForTeamResponse.getRootAsRequestNotesForTeamResponse(
- fbBuffer
- );
- for (let i = 0; i < parsedResponse.notesLength(); i++) {
- const fbNote = parsedResponse.notes(i);
- this.notes.push({data: fbNote.data()});
- }
- this.section = 'Data';
+ removeTeam(index: number) {
+ this.newData.splice(index, 1);
+ if (this.newData.length == 0) {
+ this.section = 'TeamSelection';
} else {
- const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
- const errorMessage = parsedResponse.errorMessage();
- this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ this.section = 'Data';
}
}
- changeTeam() {
+ addTeam() {
this.section = 'TeamSelection';
}
async submitData() {
- const builder = new Builder();
- const dataFb = builder.createString(this.newData);
- builder.finish(
- SubmitNotes.createSubmitNotes(builder, this.teamNumber, dataFb)
- );
+ for (let i = 0; i < this.newData.length; i++) {
+ const builder = new Builder();
+ const dataFb = builder.createString(this.newData[i].notesData);
+ builder.finish(
+ SubmitNotes.createSubmitNotes(
+ builder,
+ this.newData[i].teamNumber,
+ dataFb,
+ this.newData[i].keywordsData.goodDriving,
+ this.newData[i].keywordsData.badDriving,
+ this.newData[i].keywordsData.sketchyClimb,
+ this.newData[i].keywordsData.solidClimb,
+ this.newData[i].keywordsData.goodDefense,
+ this.newData[i].keywordsData.badDefense
+ )
+ );
- const buffer = builder.asUint8Array();
- const res = await fetch('/requests/submit/submit_notes', {
- method: 'POST',
- body: buffer,
- });
+ const buffer = builder.asUint8Array();
+ const res = await fetch('/requests/submit/submit_notes', {
+ method: 'POST',
+ body: buffer,
+ });
- if (res.ok) {
- this.newData = '';
- this.errorMessage = '';
- await this.setTeamNumber();
- } else {
- const resBuffer = await res.arrayBuffer();
- const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
- const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
- const errorMessage = parsedResponse.errorMessage();
- this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ if (!res.ok) {
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
+ const errorMessage = parsedResponse.errorMessage();
+ this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ }
}
+
+ this.newData = [];
+ this.errorMessage = '';
+ this.section = 'TeamSelection';
}
}
diff --git a/scouting/www/notes/notes.ng.html b/scouting/www/notes/notes.ng.html
index a69ba88..af48fd9 100644
--- a/scouting/www/notes/notes.ng.html
+++ b/scouting/www/notes/notes.ng.html
@@ -2,9 +2,9 @@
<ng-container [ngSwitch]="section">
<div *ngSwitchCase="'TeamSelection'">
- <label for="team_number_notes">Team Number</label>
+ <label class="label" for="team_number_notes">Team Number</label>
<input
- [(ngModel)]="teamNumber"
+ [(ngModel)]="teamNumberSelection"
type="number"
id="team_number_notes"
min="1"
@@ -14,17 +14,85 @@
</div>
<div *ngSwitchCase="'Data'">
- <h3>Scouting team: {{teamNumber}}</h3>
- <ul *ngFor="let note of notes">
- <li class="note">{{ note.data }}</li>
- </ul>
- <textarea class="text-input" [(ngModel)]="newData"></textarea>
- <div class="buttons">
- <button class="btn btn-primary" (click)="changeTeam()">
- Change team
- </button>
- <button class="btn btn-primary" (click)="submitData()">Submit</button>
+ <div class="container-main" *ngFor="let team of newData; let i = index">
+ <div class="pt-2 pb-2">
+ <div class="d-flex flex-row">
+ <div>
+ <button
+ class="btn bg-transparent ml-10 md-5"
+ (click)="removeTeam(i)"
+ >
+ ✖
+ <!--X Symbol-->
+ </button>
+ </div>
+ <div><h3>{{team.teamNumber}}</h3></div>
+ </div>
+ <div class="">
+ <textarea
+ class="text-input"
+ [(ngModel)]="newData[i].notesData"
+ ></textarea>
+ </div>
+ <!--Key Word Checkboxes-->
+ <!--Row 1 (Prevent Overflow on mobile by splitting checkboxes into 2 rows)-->
+ <!--Slice KEYWORD_CHECKBOX_LABELS using https://angular.io/api/common/SlicePipe-->
+ <div class="d-flex flex-row justify-content-around">
+ <div
+ *ngFor="let key of Object.keys(KEYWORD_CHECKBOX_LABELS) | slice:0:((Object.keys(KEYWORD_CHECKBOX_LABELS).length)/2); let k = index"
+ >
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ [(ngModel)]="newData[i]['keywordsData'][key]"
+ type="checkbox"
+ id="{{KEYWORD_CHECKBOX_LABELS[key]}}_{{i}}"
+ name="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+ />
+ <label
+ class="form-check-label"
+ for="{{KEYWORD_CHECKBOX_LABELS[key]}}_{{i}}"
+ >
+ {{KEYWORD_CHECKBOX_LABELS[key]}}
+ </label>
+ <br />
+ </div>
+ </div>
+ </div>
+ <!--Row 2 (Prevent Overflow on mobile by splitting checkboxes into 2 rows)-->
+ <div class="d-flex flex-row justify-content-around">
+ <div
+ *ngFor="let key of Object.keys(KEYWORD_CHECKBOX_LABELS) | slice:3:(Object.keys(KEYWORD_CHECKBOX_LABELS).length); let k = index"
+ >
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ [(ngModel)]="newData[i]['keywordsData'][key]"
+ type="checkbox"
+ id="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+ name="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+ />
+ <label
+ class="form-check-label"
+ for="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+ >
+ {{KEYWORD_CHECKBOX_LABELS[key]}}
+ </label>
+ <br />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="d-flex flex-row justify-content-center pt-2">
+ <div>
+ <button class="btn btn-secondary" (click)="addTeam()">Add team</button>
+ </div>
+ <div>
+ <button class="btn btn-success" (click)="submitData()">Submit</button>
+ </div>
</div>
</div>
+
<div class="error">{{errorMessage}}</div>
</ng-container>