diff --git a/adapters/spaggiari/README.md b/adapters/spaggiari/README.md index 15bc116..305eb97 100644 --- a/adapters/spaggiari/README.md +++ b/adapters/spaggiari/README.md @@ -1,5 +1,7 @@ # Classeviva +## API + Here are the best API docs currently available: - https://github.com/Lioydiano/Classeviva-Official-Endpoints diff --git a/adapters/spaggiari/model.go b/adapters/spaggiari/model.go index 8fe1bb8..89aa098 100644 --- a/adapters/spaggiari/model.go +++ b/adapters/spaggiari/model.go @@ -13,7 +13,7 @@ type Identity struct { type Grade struct { Subject string `json:"subjectDesc"` Date string `json:"evtDate"` - DecimalValue float32 `json:"decimalValue"` + DecimalValue float64 `json:"decimalValue"` DisplaylValue string `json:"displayValue"` Color string `json:"color"` Description string `json:"skillValueDesc,omitempty"` diff --git a/commands/grades.go b/commands/grades.go index 13330ea..c6b5b78 100644 --- a/commands/grades.go +++ b/commands/grades.go @@ -1,7 +1,9 @@ package commands import ( + "fmt" "sort" + "strings" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" @@ -14,7 +16,6 @@ type ListGradesCommand struct { } func (c ListGradesCommand) ExecuteWith(uow UnitOfWork) error { - grades, err := uow.Adapter.List() if err != nil { return err @@ -29,12 +30,6 @@ func (c ListGradesCommand) ExecuteWith(uow UnitOfWork) error { return feedback.PrintResult(GradesResult{Grades: grades}) } -type ByDate []spaggiari.Grade - -func (a ByDate) Len() int { return len(a) } -func (a ByDate) Less(i, j int) bool { return a[i].Date > a[j].Date } -func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] } - type GradesResult struct { Grades []spaggiari.Grade } @@ -69,3 +64,125 @@ func (r GradesResult) String() string { func (r GradesResult) Data() interface{} { return r.Grades } + +type SummarizeGradesCommand struct{} + +func (c SummarizeGradesCommand) ExecuteWith(uow UnitOfWork) error { + grades, err := uow.Adapter.List() + if err != nil { + return err + } + + sort.Sort(ByDateAsc(grades)) + + return feedback.PrintResult( + GradeSummaryResult{Summary: c.summarize(grades)}, + ) +} + +func (c SummarizeGradesCommand) summarize(grades []spaggiari.Grade) []Summary { + gradeBySubject := map[string][]spaggiari.Grade{} + + // group grades by subject + for _, grade := range grades { + if _, exists := gradeBySubject[grade.Subject]; !exists { + gradeBySubject[grade.Subject] = []spaggiari.Grade{} + } + gradeBySubject[grade.Subject] = append(gradeBySubject[grade.Subject], grade) + } + + summaries := []Summary{} + for subject, grades := range gradeBySubject { + gradesDisplay := []string{} + sum := 0.0 + average := 0.0 + trend := "=" + + for _, grade := range grades { + // blue grades do not count for + // grade average + if grade.Color == "blue" { + continue + } + + sum += grade.DecimalValue + + gradesDisplay = append(gradesDisplay, grade.DisplaylValue) + newAverage := sum / float64(len(gradesDisplay)) + + switch { + case newAverage > average: + trend = "+" + case newAverage < average: + trend = "-" + default: + trend = "=" + } + + average = newAverage + } + + summaries = append(summaries, Summary{ + Subject: subject, + Grades: gradesDisplay, + Average: average, + Trend: trend, + }) + + } + + return summaries +} + +type Summary struct { + Subject string `json:"subject"` + Grades []string `json:"grades"` + Average float64 `json:"average"` + Trend string +} + +type GradeSummaryResult struct { + Summary []Summary +} + +func (r GradeSummaryResult) String() string { + t := table.NewWriter() + t.SetColumnConfigs([]table.ColumnConfig{{Number: 1, AutoMerge: true}}) + t.AppendHeader(table.Row{"Subject", "Grades", "Avg", "Trend"}) + + for _, s := range r.Summary { + value := s.Trend + switch value { + case "+": + value = text.FgGreen.Sprint(value) + case "-": + value = text.FgRed.Sprint(value) + } + t.AppendRow(table.Row{ + s.Subject, + strings.Join(s.Grades, ", "), + fmt.Sprintf("%.2f", s.Average), + value, + }) + } + + t.SortBy([]table.SortBy{{Name: "Avg", Mode: table.DscNumeric}}) + + return t.Render() +} + +func (r GradeSummaryResult) Data() interface{} { + return r +} + +type ByDateAsc []spaggiari.Grade + +func (a ByDateAsc) Len() int { return len(a) } +func (a ByDateAsc) Less(i, j int) bool { return a[i].Date < a[j].Date } +func (a ByDateAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type ByDate []spaggiari.Grade + +func (a ByDate) Len() int { return len(a) } +func (a ByDate) Less(i, j int) bool { return a[i].Date > a[j].Date } +func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/commands/grades_test.go b/commands/grades_test.go index b146767..d64f0c0 100644 --- a/commands/grades_test.go +++ b/commands/grades_test.go @@ -36,7 +36,7 @@ func TestListGradesCommand(t *testing.T) { mockAdapter.AssertExpectations(t) }) - t.Run("List 5 agenda entries", func(t *testing.T) { + t.Run("List 1 grade", func(t *testing.T) { entries := []spaggiari.Grade{} if err := UnmarshalFrom("testdata/grades.json", &entries); err != nil { t.Error(err) @@ -67,3 +67,37 @@ func TestListGradesCommand(t *testing.T) { mockAdapter.AssertExpectations(t) }) } + +func TestSummarize(t *testing.T) { + t.Run("Summarize grades", func(t *testing.T) { + entries := []spaggiari.Grade{} + if err := UnmarshalFrom("testdata/grades-summarize.json", &entries); err != nil { + t.Error(err) + } + + mockAdapter := mocks.Adapter{} + mockAdapter.On("List").Return(entries, nil) + + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + fb := feedback.New(&stdout, &stderr, feedback.Text) + feedback.SetDefault(fb) + + uow := commands.UnitOfWork{Adapter: &mockAdapter, Feedback: fb} + cmd := commands.SummarizeGradesCommand{} + + err := cmd.ExecuteWith(uow) + assert.Nil(t, err) + + expected, err := os.ReadFile("testdata/grades-summarize.out.txt") + if err != nil { + t.Errorf("can't read test data from %v: %v", "testdata/grades-summarize.out.txtt", err) + } + + assert.Equal(t, string(expected), stdout.String()) + assert.Equal(t, "", stderr.String()) + + // should I test for this method to be called? + // mockAdapter.AssertExpectations(t) + }) +} diff --git a/commands/testdata/grades-summarize.json b/commands/testdata/grades-summarize.json new file mode 100644 index 0000000..ad43a23 --- /dev/null +++ b/commands/testdata/grades-summarize.json @@ -0,0 +1,176 @@ +[ + { + "subjectId": 396136, + "subjectCode": "ITA", + "subjectDesc": "ITALIANO", + "evtId": 420437, + "evtCode": "GRV0", + "evtDate": "2021-10-11", + "decimalValue": 7.25, + "displayValue": "7+", + "displaPos": 1, + "notesForFamily": "", + "color": "green", + "canceled": false, + "underlined": false, + "periodPos": 1, + "periodDesc": "Quadrimestre", + "componentPos": 1, + "componentDesc": "Unico", + "weightFactor": 1, + "skillId": 0, + "gradeMasterId": 0, + "skillDesc": null, + "skillCode": null, + "skillMasterId": 0, + "skillValueDesc": " Lavoro di gruppo: in gruppi di quattro i ragazzi redigono una storia sul modello del genere fantasy studiato in Antologia", + "skillValueShortDesc": null, + "oldskillId": 0, + "oldskillDesc": "" + }, + { + "subjectId": 396136, + "subjectCode": "ITA", + "subjectDesc": "ITALIANO", + "evtId": 420590, + "evtCode": "GRV0", + "evtDate": "2021-11-09", + "decimalValue": 8, + "displayValue": "8", + "displaPos": 2, + "notesForFamily": "", + "color": "green", + "canceled": false, + "underlined": false, + "periodPos": 1, + "periodDesc": "Quadrimestre", + "componentPos": 1, + "componentDesc": "Unico", + "weightFactor": 1, + "skillId": 0, + "gradeMasterId": 0, + "skillDesc": null, + "skillCode": null, + "skillMasterId": 0, + "skillValueDesc": " Interrogazione Letteratura, unit\u00e0 1: la letteratura italiana in prosa e in versi del 1200.", + "skillValueShortDesc": null, + "oldskillId": 0, + "oldskillDesc": "" + }, + { + "subjectId": 396136, + "subjectCode": "ITA", + "subjectDesc": "ITALIANO", + "evtId": 420404, + "evtCode": "GRV0", + "evtDate": "2021-11-18", + "decimalValue": 7, + "displayValue": "7", + "displaPos": 3, + "notesForFamily": "", + "color": "green", + "canceled": false, + "underlined": false, + "periodPos": 1, + "periodDesc": "Quadrimestre", + "componentPos": 1, + "componentDesc": "Unico", + "weightFactor": 1, + "skillId": 0, + "gradeMasterId": 0, + "skillDesc": null, + "skillCode": null, + "skillMasterId": 0, + "skillValueDesc": " Lavoro di gruppo: in gruppi di quattro i ragazzi redigono una storia sul modello del genere horror studiato in Antologia", + "skillValueShortDesc": null, + "oldskillId": 0, + "oldskillDesc": "" + }, + { + "subjectId": 396136, + "subjectCode": "ITA", + "subjectDesc": "ITALIANO", + "evtId": 1275451, + "evtCode": "GRV0", + "evtDate": "2022-01-21", + "decimalValue": 9.25, + "displayValue": "9+", + "displaPos": 4, + "notesForFamily": "", + "color": "green", + "canceled": false, + "underlined": false, + "periodPos": 1, + "periodDesc": "Quadrimestre", + "componentPos": 1, + "componentDesc": "Unico", + "weightFactor": 1, + "skillId": 0, + "gradeMasterId": 0, + "skillDesc": null, + "skillCode": null, + "skillMasterId": 0, + "skillValueDesc": " ", + "skillValueShortDesc": null, + "oldskillId": 0, + "oldskillDesc": "" + }, + { + "subjectId": 396136, + "subjectCode": "ITA", + "subjectDesc": "ITALIANO", + "evtId": 1899413, + "evtCode": "GRT1", + "evtDate": "2022-02-14", + "decimalValue": null, + "displayValue": "4", + "displaPos": 1, + "notesForFamily": "", + "color": "blue", + "canceled": false, + "underlined": false, + "periodPos": 3, + "periodDesc": "Quadrimestre", + "componentPos": 0, + "componentDesc": "", + "weightFactor": 1, + "skillId": 0, + "gradeMasterId": 0, + "skillDesc": null, + "skillCode": null, + "skillMasterId": 0, + "skillValueDesc": " ", + "skillValueShortDesc": null, + "oldskillId": 0, + "oldskillDesc": "" + }, + { + "subjectId": 396136, + "subjectCode": "ITA", + "subjectDesc": "ITALIANO", + "evtId": 1899670, + "evtCode": "GRV0", + "evtDate": "2022-02-14", + "decimalValue": 8.5, + "displayValue": "8\u00bd", + "displaPos": 1, + "notesForFamily": "", + "color": "green", + "canceled": false, + "underlined": false, + "periodPos": 3, + "periodDesc": "Quadrimestre", + "componentPos": 1, + "componentDesc": "Unico", + "weightFactor": 1, + "skillId": 0, + "gradeMasterId": 0, + "skillDesc": null, + "skillCode": null, + "skillMasterId": 0, + "skillValueDesc": " ", + "skillValueShortDesc": null, + "oldskillId": 0, + "oldskillDesc": "" + } +] \ No newline at end of file diff --git a/commands/testdata/grades-summarize.out.txt b/commands/testdata/grades-summarize.out.txt new file mode 100644 index 0000000..d50e855 --- /dev/null +++ b/commands/testdata/grades-summarize.out.txt @@ -0,0 +1,5 @@ ++----------+------------------+------+-------+ +| SUBJECT | GRADES | AVG | TREND | ++----------+------------------+------+-------+ +| ITALIANO | 7+, 8, 7, 9+, 8½ | 8.00 | + | ++----------+------------------+------+-------+ \ No newline at end of file diff --git a/entrypoints/cli/cmd/grades/grades.go b/entrypoints/cli/cmd/grades/grades.go index a7f767f..e274a2d 100644 --- a/entrypoints/cli/cmd/grades/grades.go +++ b/entrypoints/cli/cmd/grades/grades.go @@ -9,6 +9,7 @@ func NewCommand() *cobra.Command { } cmd.AddCommand(initListCommand()) + cmd.AddCommand(initSummarizeCommand()) return &cmd } diff --git a/entrypoints/cli/cmd/grades/summarize.go b/entrypoints/cli/cmd/grades/summarize.go new file mode 100644 index 0000000..e221aa6 --- /dev/null +++ b/entrypoints/cli/cmd/grades/summarize.go @@ -0,0 +1,32 @@ +package grades + +import ( + "github.com/spf13/cobra" + "github.com/zmoog/classeviva/commands" +) + +func initSummarizeCommand() *cobra.Command { + summarizeCommand := cobra.Command{ + Use: "summarize", + Short: "Summarize the grades on the portal", + RunE: runSummarizeCommand, + } + + return &summarizeCommand +} + +func runSummarizeCommand(cmd *cobra.Command, args []string) error { + command := commands.SummarizeGradesCommand{} + + runner, err := commands.NewRunner() + if err != nil { + return err + } + + err = runner.Run(command) + if err != nil { + return err + } + + return nil +}