fix(classroom): add tests, nil guards, and topic filter

- Add comprehensive unit tests for classroom helper functions
  (date/time parsing, error wrapping, profile helpers)
- Add profileEmail() helper for nil-safe email extraction
- Fix nil pointer guards for Profile access (12 locations in
  courses, rosters, and guardians)
- Add --topic filter for coursework and materials list commands
- Update docs/spec.md with new filter options

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
salmonumbrella 2026-01-13 17:01:55 -08:00 committed by Peter Steinberger
parent 9b96009d91
commit 5e55964a85
8 changed files with 667 additions and 20 deletions

View File

@ -185,13 +185,13 @@ Flag aliases:
- `gog classroom teachers add <courseId> <userId>`
- `gog classroom teachers remove <courseId> <userId>`
- `gog classroom roster <courseId> [--students] [--teachers]`
- `gog classroom coursework <courseId> [--state ...] [--max N] [--page TOKEN]`
- `gog classroom coursework <courseId> [--state ...] [--topic TOPIC_ID] [--max N] [--page TOKEN]`
- `gog classroom coursework get <courseId> <courseworkId>`
- `gog classroom coursework create <courseId> --title TITLE [--type ASSIGNMENT|...]`
- `gog classroom coursework update <courseId> <courseworkId> [--title ...]`
- `gog classroom coursework delete <courseId> <courseworkId>`
- `gog classroom coursework assignees <courseId> <courseworkId> [--mode ...] [--add-student ...]`
- `gog classroom materials <courseId> [--state ...] [--max N] [--page TOKEN]`
- `gog classroom materials <courseId> [--state ...] [--topic TOPIC_ID] [--max N] [--page TOKEN]`
- `gog classroom materials get <courseId> <materialId>`
- `gog classroom materials create <courseId> --title TITLE`
- `gog classroom materials update <courseId> <materialId> [--title ...]`

View File

@ -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:

View File

@ -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
}

View File

@ -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
}

View File

@ -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"), ".")
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)),
)
}