diff --git a/docs/spec.md b/docs/spec.md index e84d17d..f386da2 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -185,13 +185,13 @@ Flag aliases: - `gog classroom teachers add ` - `gog classroom teachers remove ` - `gog classroom roster [--students] [--teachers]` -- `gog classroom coursework [--state ...] [--max N] [--page TOKEN]` +- `gog classroom coursework [--state ...] [--topic TOPIC_ID] [--max N] [--page TOKEN]` - `gog classroom coursework get ` - `gog classroom coursework create --title TITLE [--type ASSIGNMENT|...]` - `gog classroom coursework update [--title ...]` - `gog classroom coursework delete ` - `gog classroom coursework assignees [--mode ...] [--add-student ...]` -- `gog classroom materials [--state ...] [--max N] [--page TOKEN]` +- `gog classroom materials [--state ...] [--topic TOPIC_ID] [--max N] [--page TOKEN]` - `gog classroom materials get ` - `gog classroom materials create --title TITLE` - `gog classroom materials update [--title ...]` diff --git a/internal/cmd/classroom_courses.go b/internal/cmd/classroom_courses.go index 31f7d94..6d8c071 100644 --- a/internal/cmd/classroom_courses.go +++ b/internal/cmd/classroom_courses.go @@ -423,7 +423,7 @@ func (c *ClassroomCoursesJoinCmd) Run(ctx context.Context, flags *RootFlags) err return outfmt.WriteJSON(os.Stdout, map[string]any{"student": created}) } u.Out().Printf("user_id\t%s", created.UserId) - u.Out().Printf("email\t%s", created.Profile.EmailAddress) + u.Out().Printf("email\t%s", profileEmail(created.Profile)) u.Out().Printf("name\t%s", profileName(created.Profile)) return nil case "teacher": @@ -436,7 +436,7 @@ func (c *ClassroomCoursesJoinCmd) Run(ctx context.Context, flags *RootFlags) err return outfmt.WriteJSON(os.Stdout, map[string]any{"teacher": created}) } u.Out().Printf("user_id\t%s", created.UserId) - u.Out().Printf("email\t%s", created.Profile.EmailAddress) + u.Out().Printf("email\t%s", profileEmail(created.Profile)) u.Out().Printf("name\t%s", profileName(created.Profile)) return nil default: diff --git a/internal/cmd/classroom_coursework.go b/internal/cmd/classroom_coursework.go index ee38834..2832523 100644 --- a/internal/cmd/classroom_coursework.go +++ b/internal/cmd/classroom_coursework.go @@ -24,6 +24,7 @@ type ClassroomCourseworkCmd struct { type ClassroomCourseworkListCmd struct { CourseID string `arg:"" name:"courseId" help:"Course ID or alias"` States string `name:"state" help:"Coursework states filter (comma-separated: DRAFT,PUBLISHED,DELETED)"` + Topic string `name:"topic" help:"Filter by topic ID"` OrderBy string `name:"order-by" help:"Order by (e.g., updateTime desc, dueDate desc)"` Max int64 `name:"max" aliases:"limit" help:"Max results" default:"100"` Page string `name:"page" help:"Page token"` @@ -62,14 +63,27 @@ func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags) return wrapClassroomError(err) } + // Client-side filter by topic (API doesn't support server-side topic filter) + topicFilter := strings.TrimSpace(c.Topic) + coursework := resp.CourseWork + if topicFilter != "" { + filtered := make([]*classroom.CourseWork, 0, len(coursework)) + for _, work := range coursework { + if work != nil && work.TopicId == topicFilter { + filtered = append(filtered, work) + } + } + coursework = filtered + } + if outfmt.IsJSON(ctx) { return outfmt.WriteJSON(os.Stdout, map[string]any{ - "coursework": resp.CourseWork, + "coursework": coursework, "nextPageToken": resp.NextPageToken, }) } - if len(resp.CourseWork) == 0 { + if len(coursework) == 0 { u.Err().Println("No coursework") return nil } @@ -77,7 +91,7 @@ func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags) w, flush := tableWriter(ctx) defer flush() fmt.Fprintln(w, "ID\tTITLE\tSTATE\tDUE\tTYPE\tMAX_POINTS") - for _, work := range resp.CourseWork { + for _, work := range coursework { if work == nil { continue } diff --git a/internal/cmd/classroom_guardians.go b/internal/cmd/classroom_guardians.go index faa8a33..f29baa1 100644 --- a/internal/cmd/classroom_guardians.go +++ b/internal/cmd/classroom_guardians.go @@ -72,7 +72,7 @@ func (c *ClassroomGuardiansListCmd) Run(ctx context.Context, flags *RootFlags) e } fmt.Fprintf(w, "%s\t%s\t%s\n", sanitizeTab(guardian.GuardianId), - sanitizeTab(guardian.GuardianProfile.EmailAddress), + sanitizeTab(profileEmail(guardian.GuardianProfile)), sanitizeTab(profileName(guardian.GuardianProfile)), ) } @@ -116,7 +116,7 @@ func (c *ClassroomGuardiansGetCmd) Run(ctx context.Context, flags *RootFlags) er u.Out().Printf("id\t%s", guardian.GuardianId) u.Out().Printf("student_id\t%s", guardian.StudentId) - u.Out().Printf("email\t%s", guardian.GuardianProfile.EmailAddress) + u.Out().Printf("email\t%s", profileEmail(guardian.GuardianProfile)) u.Out().Printf("name\t%s", profileName(guardian.GuardianProfile)) return nil } diff --git a/internal/cmd/classroom_helpers.go b/internal/cmd/classroom_helpers.go index 6ae4871..9669bbb 100644 --- a/internal/cmd/classroom_helpers.go +++ b/internal/cmd/classroom_helpers.go @@ -140,6 +140,13 @@ func profileName(profile *classroom.UserProfile) string { return strings.TrimSpace(strings.TrimSpace(profile.Name.GivenName + " " + profile.Name.FamilyName)) } +func profileEmail(profile *classroom.UserProfile) string { + if profile == nil { + return "" + } + return profile.EmailAddress +} + func formatFloatValue(v float64) string { return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", v), "0"), ".") } diff --git a/internal/cmd/classroom_helpers_test.go b/internal/cmd/classroom_helpers_test.go new file mode 100644 index 0000000..dd3bfc6 --- /dev/null +++ b/internal/cmd/classroom_helpers_test.go @@ -0,0 +1,612 @@ +package cmd + +import ( + "errors" + "strings" + "testing" + + "google.golang.org/api/classroom/v1" +) + +func TestWrapClassroomError(t *testing.T) { + tests := []struct { + name string + err error + wantNil bool + contains string + }{ + { + name: "nil error returns nil", + err: nil, + wantNil: true, + }, + { + name: "accessNotConfigured wraps with enable link", + err: errors.New("accessNotConfigured: Classroom API has not been used"), + contains: "console.developers.google.com", + }, + { + name: "Classroom API has not been used wraps with enable link", + err: errors.New("Classroom API has not been used in project"), + contains: "classroom.googleapis.com", + }, + { + name: "insufficientPermissions wraps with re-auth hint", + err: errors.New("insufficientPermissions: Request had insufficient auth"), + contains: "gog auth add", + }, + { + name: "insufficient authentication scopes wraps with re-auth hint", + err: errors.New("insufficient authentication scopes"), + contains: "--services classroom", + }, + { + name: "other errors pass through", + err: errors.New("some other error"), + contains: "some other error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := wrapClassroomError(tt.err) + if tt.wantNil { + if got != nil { + t.Errorf("expected nil, got %v", got) + } + return + } + if got == nil { + t.Fatal("expected non-nil error") + } + if tt.contains != "" && !strings.Contains(got.Error(), tt.contains) { + t.Errorf("error %q does not contain %q", got.Error(), tt.contains) + } + }) + } +} + +func TestFormatClassroomDate(t *testing.T) { + tests := []struct { + name string + date *classroom.Date + want string + }{ + { + name: "nil date returns empty", + date: nil, + want: "", + }, + { + name: "zero values return empty", + date: &classroom.Date{Year: 0, Month: 0, Day: 0}, + want: "", + }, + { + name: "partial values return empty", + date: &classroom.Date{Year: 2024, Month: 0, Day: 15}, + want: "", + }, + { + name: "valid date formats correctly", + date: &classroom.Date{Year: 2024, Month: 3, Day: 15}, + want: "2024-03-15", + }, + { + name: "single digit month and day get padded", + date: &classroom.Date{Year: 2024, Month: 1, Day: 5}, + want: "2024-01-05", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatClassroomDate(tt.date) + if got != tt.want { + t.Errorf("formatClassroomDate() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestFormatClassroomTime(t *testing.T) { + tests := []struct { + name string + time *classroom.TimeOfDay + want string + }{ + { + name: "nil time returns empty", + time: nil, + want: "", + }, + { + name: "hours and minutes only", + time: &classroom.TimeOfDay{Hours: 14, Minutes: 30}, + want: "14:30", + }, + { + name: "with seconds", + time: &classroom.TimeOfDay{Hours: 14, Minutes: 30, Seconds: 45}, + want: "14:30:45", + }, + { + name: "with nanos (shows seconds)", + time: &classroom.TimeOfDay{Hours: 14, Minutes: 30, Nanos: 1}, + want: "14:30:00", + }, + { + name: "single digit hours and minutes get padded", + time: &classroom.TimeOfDay{Hours: 9, Minutes: 5}, + want: "09:05", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatClassroomTime(tt.time) + if got != tt.want { + t.Errorf("formatClassroomTime() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestFormatClassroomDue(t *testing.T) { + tests := []struct { + name string + date *classroom.Date + time *classroom.TimeOfDay + want string + }{ + { + name: "nil date and time returns empty", + date: nil, + time: nil, + want: "", + }, + { + name: "date only", + date: &classroom.Date{Year: 2024, Month: 3, Day: 15}, + time: nil, + want: "2024-03-15", + }, + { + name: "time only", + date: nil, + time: &classroom.TimeOfDay{Hours: 14, Minutes: 30}, + want: "14:30", + }, + { + name: "date and time", + date: &classroom.Date{Year: 2024, Month: 3, Day: 15}, + time: &classroom.TimeOfDay{Hours: 14, Minutes: 30}, + want: "2024-03-15 14:30", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatClassroomDue(tt.date, tt.time) + if got != tt.want { + t.Errorf("formatClassroomDue() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestParseClassroomDate(t *testing.T) { + tests := []struct { + name string + value string + wantErr bool + want *classroom.Date + }{ + { + name: "empty value errors", + value: "", + wantErr: true, + }, + { + name: "whitespace only errors", + value: " ", + wantErr: true, + }, + { + name: "invalid format errors", + value: "2024/03/15", + wantErr: true, + }, + { + name: "valid date parses correctly", + value: "2024-03-15", + wantErr: false, + want: &classroom.Date{Year: 2024, Month: 3, Day: 15}, + }, + { + name: "whitespace trimmed", + value: " 2024-03-15 ", + wantErr: false, + want: &classroom.Date{Year: 2024, Month: 3, Day: 15}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseClassroomDate(tt.value) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.Year != tt.want.Year || got.Month != tt.want.Month || got.Day != tt.want.Day { + t.Errorf("parseClassroomDate() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestParseClassroomTime(t *testing.T) { + tests := []struct { + name string + value string + wantErr bool + want *classroom.TimeOfDay + }{ + { + name: "empty value errors", + value: "", + wantErr: true, + }, + { + name: "invalid format errors", + value: "14:30:45:00", + wantErr: true, + }, + { + name: "HH:MM format parses", + value: "14:30", + wantErr: false, + want: &classroom.TimeOfDay{Hours: 14, Minutes: 30, Seconds: 0}, + }, + { + name: "HH:MM:SS format parses", + value: "14:30:45", + wantErr: false, + want: &classroom.TimeOfDay{Hours: 14, Minutes: 30, Seconds: 45}, + }, + { + name: "whitespace trimmed", + value: " 14:30 ", + wantErr: false, + want: &classroom.TimeOfDay{Hours: 14, Minutes: 30, Seconds: 0}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseClassroomTime(tt.value) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.Hours != tt.want.Hours || got.Minutes != tt.want.Minutes || got.Seconds != tt.want.Seconds { + t.Errorf("parseClassroomTime() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestParseClassroomDue(t *testing.T) { + tests := []struct { + name string + value string + wantErr bool + wantDate *classroom.Date + wantTime *classroom.TimeOfDay + }{ + { + name: "empty value returns nil", + value: "", + wantErr: false, + wantDate: nil, + wantTime: nil, + }, + { + name: "RFC3339 format parses", + value: "2024-03-15T14:30:00Z", + wantErr: false, + wantDate: &classroom.Date{Year: 2024, Month: 3, Day: 15}, + wantTime: &classroom.TimeOfDay{Hours: 14, Minutes: 30, Seconds: 0}, + }, + { + name: "YYYY-MM-DD HH:MM format parses", + value: "2024-03-15 14:30", + wantErr: false, + wantDate: &classroom.Date{Year: 2024, Month: 3, Day: 15}, + wantTime: &classroom.TimeOfDay{Hours: 14, Minutes: 30, Seconds: 0}, + }, + { + name: "YYYY-MM-DDTHH:MM format parses", + value: "2024-03-15T14:30", + wantErr: false, + wantDate: &classroom.Date{Year: 2024, Month: 3, Day: 15}, + wantTime: &classroom.TimeOfDay{Hours: 14, Minutes: 30, Seconds: 0}, + }, + { + name: "date only parses", + value: "2024-03-15", + wantErr: false, + wantDate: &classroom.Date{Year: 2024, Month: 3, Day: 15}, + wantTime: nil, + }, + { + name: "invalid format errors", + value: "not-a-date", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotDate, gotTime, err := parseClassroomDue(tt.value) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if tt.wantDate == nil { + if gotDate != nil { + t.Errorf("expected nil date, got %+v", gotDate) + } + } else { + if gotDate == nil { + t.Fatal("expected non-nil date") + } + if gotDate.Year != tt.wantDate.Year || gotDate.Month != tt.wantDate.Month || gotDate.Day != tt.wantDate.Day { + t.Errorf("date = %+v, want %+v", gotDate, tt.wantDate) + } + } + + if tt.wantTime == nil { + if gotTime != nil { + t.Errorf("expected nil time, got %+v", gotTime) + } + } else { + if gotTime == nil { + t.Fatal("expected non-nil time") + } + if gotTime.Hours != tt.wantTime.Hours || gotTime.Minutes != tt.wantTime.Minutes { + t.Errorf("time = %+v, want %+v", gotTime, tt.wantTime) + } + } + }) + } +} + +func TestUpdateMask(t *testing.T) { + tests := []struct { + name string + fields []string + want string + }{ + { + name: "empty slice returns empty", + fields: []string{}, + want: "", + }, + { + name: "single field", + fields: []string{"name"}, + want: "name", + }, + { + name: "multiple fields joined with comma", + fields: []string{"name", "description", "state"}, + want: "name,description,state", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateMask(tt.fields) + if got != tt.want { + t.Errorf("updateMask() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestParseFloat(t *testing.T) { + tests := []struct { + name string + value string + wantErr bool + want float64 + }{ + { + name: "empty value errors", + value: "", + wantErr: true, + }, + { + name: "invalid format errors", + value: "not-a-number", + wantErr: true, + }, + { + name: "integer parses", + value: "42", + wantErr: false, + want: 42.0, + }, + { + name: "decimal parses", + value: "3.14", + wantErr: false, + want: 3.14, + }, + { + name: "whitespace trimmed", + value: " 3.14 ", + wantErr: false, + want: 3.14, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseFloat(tt.value) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("parseFloat() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestProfileName(t *testing.T) { + tests := []struct { + name string + profile *classroom.UserProfile + want string + }{ + { + name: "nil profile returns empty", + profile: nil, + want: "", + }, + { + name: "nil name returns empty", + profile: &classroom.UserProfile{Name: nil}, + want: "", + }, + { + name: "full name preferred", + profile: &classroom.UserProfile{Name: &classroom.Name{FullName: "John Doe", GivenName: "John", FamilyName: "Doe"}}, + want: "John Doe", + }, + { + name: "falls back to given + family name", + profile: &classroom.UserProfile{Name: &classroom.Name{GivenName: "John", FamilyName: "Doe"}}, + want: "John Doe", + }, + { + name: "handles missing family name", + profile: &classroom.UserProfile{Name: &classroom.Name{GivenName: "John"}}, + want: "John", + }, + { + name: "handles missing given name", + profile: &classroom.UserProfile{Name: &classroom.Name{FamilyName: "Doe"}}, + want: "Doe", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := profileName(tt.profile) + if got != tt.want { + t.Errorf("profileName() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestProfileEmail(t *testing.T) { + tests := []struct { + name string + profile *classroom.UserProfile + want string + }{ + { + name: "nil profile returns empty", + profile: nil, + want: "", + }, + { + name: "returns email address", + profile: &classroom.UserProfile{EmailAddress: "test@example.com"}, + want: "test@example.com", + }, + { + name: "empty email returns empty", + profile: &classroom.UserProfile{EmailAddress: ""}, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := profileEmail(tt.profile) + if got != tt.want { + t.Errorf("profileEmail() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestFormatFloatValue(t *testing.T) { + tests := []struct { + name string + value float64 + want string + }{ + { + name: "integer value", + value: 100.0, + want: "100", + }, + { + name: "single decimal place", + value: 85.5, + want: "85.5", + }, + { + name: "two decimal places", + value: 85.75, + want: "85.75", + }, + { + name: "trailing zeros removed", + value: 85.10, + want: "85.1", + }, + { + name: "zero", + value: 0.0, + want: "0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatFloatValue(tt.value) + if got != tt.want { + t.Errorf("formatFloatValue() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/internal/cmd/classroom_materials.go b/internal/cmd/classroom_materials.go index c2489a0..3876511 100644 --- a/internal/cmd/classroom_materials.go +++ b/internal/cmd/classroom_materials.go @@ -23,6 +23,7 @@ type ClassroomMaterialsCmd struct { type ClassroomMaterialsListCmd struct { CourseID string `arg:"" name:"courseId" help:"Course ID or alias"` States string `name:"state" help:"Material states filter (comma-separated: PUBLISHED,DRAFT,DELETED)"` + Topic string `name:"topic" help:"Filter by topic ID"` OrderBy string `name:"order-by" help:"Order by (e.g., updateTime desc)"` Max int64 `name:"max" aliases:"limit" help:"Max results" default:"100"` Page string `name:"page" help:"Page token"` @@ -61,14 +62,27 @@ func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) e return wrapClassroomError(err) } + // Client-side filter by topic (API doesn't support server-side topic filter) + topicFilter := strings.TrimSpace(c.Topic) + materials := resp.CourseWorkMaterial + if topicFilter != "" { + filtered := make([]*classroom.CourseWorkMaterial, 0, len(materials)) + for _, material := range materials { + if material != nil && material.TopicId == topicFilter { + filtered = append(filtered, material) + } + } + materials = filtered + } + if outfmt.IsJSON(ctx) { return outfmt.WriteJSON(os.Stdout, map[string]any{ - "materials": resp.CourseWorkMaterial, + "materials": materials, "nextPageToken": resp.NextPageToken, }) } - if len(resp.CourseWorkMaterial) == 0 { + if len(materials) == 0 { u.Err().Println("No materials") return nil } @@ -76,7 +90,7 @@ func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) e w, flush := tableWriter(ctx) defer flush() fmt.Fprintln(w, "ID\tTITLE\tSTATE\tUPDATED") - for _, material := range resp.CourseWorkMaterial { + for _, material := range materials { if material == nil { continue } diff --git a/internal/cmd/classroom_rosters.go b/internal/cmd/classroom_rosters.go index e4eabb8..3415995 100644 --- a/internal/cmd/classroom_rosters.go +++ b/internal/cmd/classroom_rosters.go @@ -67,7 +67,7 @@ func (c *ClassroomStudentsListCmd) Run(ctx context.Context, flags *RootFlags) er } fmt.Fprintf(w, "%s\t%s\t%s\n", sanitizeTab(student.UserId), - sanitizeTab(student.Profile.EmailAddress), + sanitizeTab(profileEmail(student.Profile)), sanitizeTab(profileName(student.Profile)), ) } @@ -110,7 +110,7 @@ func (c *ClassroomStudentsGetCmd) Run(ctx context.Context, flags *RootFlags) err } u.Out().Printf("user_id\t%s", student.UserId) - u.Out().Printf("email\t%s", student.Profile.EmailAddress) + u.Out().Printf("email\t%s", profileEmail(student.Profile)) u.Out().Printf("name\t%s", profileName(student.Profile)) if student.StudentWorkFolder != nil { u.Out().Printf("work_folder\t%s", student.StudentWorkFolder.Id) @@ -158,7 +158,7 @@ func (c *ClassroomStudentsAddCmd) Run(ctx context.Context, flags *RootFlags) err return outfmt.WriteJSON(os.Stdout, map[string]any{"student": created}) } u.Out().Printf("user_id\t%s", created.UserId) - u.Out().Printf("email\t%s", created.Profile.EmailAddress) + u.Out().Printf("email\t%s", profileEmail(created.Profile)) u.Out().Printf("name\t%s", profileName(created.Profile)) return nil } @@ -265,7 +265,7 @@ func (c *ClassroomTeachersListCmd) Run(ctx context.Context, flags *RootFlags) er } fmt.Fprintf(w, "%s\t%s\t%s\n", sanitizeTab(teacher.UserId), - sanitizeTab(teacher.Profile.EmailAddress), + sanitizeTab(profileEmail(teacher.Profile)), sanitizeTab(profileName(teacher.Profile)), ) } @@ -308,7 +308,7 @@ func (c *ClassroomTeachersGetCmd) Run(ctx context.Context, flags *RootFlags) err } u.Out().Printf("user_id\t%s", teacher.UserId) - u.Out().Printf("email\t%s", teacher.Profile.EmailAddress) + u.Out().Printf("email\t%s", profileEmail(teacher.Profile)) u.Out().Printf("name\t%s", profileName(teacher.Profile)) return nil } @@ -348,7 +348,7 @@ func (c *ClassroomTeachersAddCmd) Run(ctx context.Context, flags *RootFlags) err return outfmt.WriteJSON(os.Stdout, map[string]any{"teacher": created}) } u.Out().Printf("user_id\t%s", created.UserId) - u.Out().Printf("email\t%s", created.Profile.EmailAddress) + u.Out().Printf("email\t%s", profileEmail(created.Profile)) u.Out().Printf("name\t%s", profileName(created.Profile)) return nil } @@ -466,7 +466,7 @@ func (c *ClassroomRosterCmd) Run(ctx context.Context, flags *RootFlags) error { } fmt.Fprintf(w, "teacher\t%s\t%s\t%s\n", sanitizeTab(teacher.UserId), - sanitizeTab(teacher.Profile.EmailAddress), + sanitizeTab(profileEmail(teacher.Profile)), sanitizeTab(profileName(teacher.Profile)), ) } @@ -481,7 +481,7 @@ func (c *ClassroomRosterCmd) Run(ctx context.Context, flags *RootFlags) error { } fmt.Fprintf(w, "student\t%s\t%s\t%s\n", sanitizeTab(student.UserId), - sanitizeTab(student.Profile.EmailAddress), + sanitizeTab(profileEmail(student.Profile)), sanitizeTab(profileName(student.Profile)), ) }