Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions adapters/spaggiari/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Classeviva

## API

Here are the best API docs currently available:

- https://github.com/Lioydiano/Classeviva-Official-Endpoints
2 changes: 1 addition & 1 deletion adapters/spaggiari/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
131 changes: 124 additions & 7 deletions commands/grades.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package commands

import (
"fmt"
"sort"
"strings"

"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
Expand All @@ -14,7 +16,6 @@ type ListGradesCommand struct {
}

func (c ListGradesCommand) ExecuteWith(uow UnitOfWork) error {

grades, err := uow.Adapter.List()
if err != nil {
return err
Expand All @@ -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
}
Expand Down Expand Up @@ -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] }
36 changes: 35 additions & 1 deletion commands/grades_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
}
176 changes: 176 additions & 0 deletions commands/testdata/grades-summarize.json
Original file line number Diff line number Diff line change
@@ -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": ""
}
]
Loading