Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

- Gmail: include `gmail.settings.sharing` scope for filter operations to avoid 403 insufficientPermissions. (#69) — thanks @ryanh-ai.
- Gmail: resync on stale history 404s and skip missing message fetches without masking non-404 failures. (#70) — thanks @antons.
- Auth: account manager upgrade respects managed services and skips Keep OAuth scopes. (#73) — thanks @salmonumbrella.
- Classroom: normalize assignee updates + fix grade update masks. (#74) — thanks @salmonumbrella.
- Classroom: scan pages when filtering coursework/materials by topic. (#73) — thanks @salmonumbrella.

### Build

Expand Down
4 changes: 2 additions & 2 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ...] [--topic TOPIC_ID] [--max N] [--page TOKEN]`
- `gog classroom coursework <courseId> [--state ...] [--topic TOPIC_ID] [--scan-pages N] [--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 ...] [--topic TOPIC_ID] [--max N] [--page TOKEN]`
- `gog classroom materials <courseId> [--state ...] [--topic TOPIC_ID] [--scan-pages N] [--max N] [--page TOKEN]`
- `gog classroom materials get <courseId> <materialId>`
- `gog classroom materials create <courseId> --title TITLE`
- `gog classroom materials update <courseId> <materialId> [--title ...]`
Expand Down
5 changes: 5 additions & 0 deletions internal/cmd/classroom_courses.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,11 @@ func (c *ClassroomCoursesLeaveCmd) Run(ctx context.Context, flags *RootFlags) er
return usage("empty user")
}

err = confirmDestructive(ctx, flags, fmt.Sprintf("remove %s %s from course %s", role, userID, courseID))
if err != nil {
return err
}

svc, err := newClassroomService(ctx, account)
if err != nil {
return wrapClassroomError(err)
Expand Down
83 changes: 55 additions & 28 deletions internal/cmd/classroom_coursework.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ 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"`
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"`
ScanPages int `name:"scan-pages" help:"Pages to scan when filtering by topic" default:"3"`
}

func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags) error {
Expand All @@ -46,45 +47,71 @@ func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags)
return wrapClassroomError(err)
}

call := svc.Courses.CourseWork.List(courseID).PageSize(c.Max).PageToken(c.Page).Context(ctx)
if states := splitCSV(c.States); len(states) > 0 {
upper := make([]string, 0, len(states))
for _, state := range states {
upper = append(upper, strings.ToUpper(state))
makeCall := func(page string) *classroom.CoursesCourseWorkListCall {
call := svc.Courses.CourseWork.List(courseID).PageSize(c.Max).PageToken(page).Context(ctx)
if states := splitCSV(c.States); len(states) > 0 {
upper := make([]string, 0, len(states))
for _, state := range states {
upper = append(upper, strings.ToUpper(state))
}
call.CourseWorkStates(upper...)
}
call.CourseWorkStates(upper...)
}
if v := strings.TrimSpace(c.OrderBy); v != "" {
call.OrderBy(v)
}

resp, err := call.Do()
if err != nil {
return wrapClassroomError(err)
if v := strings.TrimSpace(c.OrderBy); v != "" {
call.OrderBy(v)
}
return call
}

// 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 {
pageToken := c.Page
scanPages := c.ScanPages
if scanPages <= 0 {
scanPages = 1
}

var (
coursework []*classroom.CourseWork
nextPageToken string
)
for page := 0; ; page++ {
resp, err := makeCall(pageToken).Do()
if err != nil {
return wrapClassroomError(err)
}
nextPageToken = resp.NextPageToken

if topicFilter == "" {
coursework = resp.CourseWork
break
}

filtered := make([]*classroom.CourseWork, 0, len(resp.CourseWork))
for _, work := range resp.CourseWork {
if work != nil && work.TopicId == topicFilter {
filtered = append(filtered, work)
}
}
coursework = filtered
if len(filtered) > 0 {
coursework = filtered
break
}
if nextPageToken == "" || page+1 >= scanPages {
coursework = filtered
break
}
pageToken = nextPageToken
}

if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"coursework": coursework,
"nextPageToken": resp.NextPageToken,
"nextPageToken": nextPageToken,
})
}

if len(coursework) == 0 {
u.Err().Println("No coursework")
printNextPageHint(u, nextPageToken)
return nil
}

Expand All @@ -104,7 +131,7 @@ func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags)
formatFloatValue(work.MaxPoints),
)
}
printNextPageHint(u, resp.NextPageToken)
printNextPageHint(u, nextPageToken)
return nil
}

Expand Down
83 changes: 55 additions & 28 deletions internal/cmd/classroom_materials.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ 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"`
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"`
ScanPages int `name:"scan-pages" help:"Pages to scan when filtering by topic" default:"3"`
}

func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) error {
Expand All @@ -45,45 +46,71 @@ func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) e
return wrapClassroomError(err)
}

call := svc.Courses.CourseWorkMaterials.List(courseID).PageSize(c.Max).PageToken(c.Page).Context(ctx)
if states := splitCSV(c.States); len(states) > 0 {
upper := make([]string, 0, len(states))
for _, state := range states {
upper = append(upper, strings.ToUpper(state))
makeCall := func(page string) *classroom.CoursesCourseWorkMaterialsListCall {
call := svc.Courses.CourseWorkMaterials.List(courseID).PageSize(c.Max).PageToken(page).Context(ctx)
if states := splitCSV(c.States); len(states) > 0 {
upper := make([]string, 0, len(states))
for _, state := range states {
upper = append(upper, strings.ToUpper(state))
}
call.CourseWorkMaterialStates(upper...)
}
call.CourseWorkMaterialStates(upper...)
}
if v := strings.TrimSpace(c.OrderBy); v != "" {
call.OrderBy(v)
}

resp, err := call.Do()
if err != nil {
return wrapClassroomError(err)
if v := strings.TrimSpace(c.OrderBy); v != "" {
call.OrderBy(v)
}
return call
}

// 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 {
pageToken := c.Page
scanPages := c.ScanPages
if scanPages <= 0 {
scanPages = 1
}

var (
materials []*classroom.CourseWorkMaterial
nextPageToken string
)
for page := 0; ; page++ {
resp, err := makeCall(pageToken).Do()
if err != nil {
return wrapClassroomError(err)
}
nextPageToken = resp.NextPageToken

if topicFilter == "" {
materials = resp.CourseWorkMaterial
break
}

filtered := make([]*classroom.CourseWorkMaterial, 0, len(resp.CourseWorkMaterial))
for _, material := range resp.CourseWorkMaterial {
if material != nil && material.TopicId == topicFilter {
filtered = append(filtered, material)
}
}
materials = filtered
if len(filtered) > 0 {
materials = filtered
break
}
if nextPageToken == "" || page+1 >= scanPages {
materials = filtered
break
}
pageToken = nextPageToken
}

if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"materials": materials,
"nextPageToken": resp.NextPageToken,
"nextPageToken": nextPageToken,
})
}

if len(materials) == 0 {
u.Err().Println("No materials")
printNextPageHint(u, nextPageToken)
return nil
}

Expand All @@ -101,7 +128,7 @@ func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) e
sanitizeTab(material.UpdateTime),
)
}
printNextPageHint(u, resp.NextPageToken)
printNextPageHint(u, nextPageToken)
return nil
}

Expand Down
Loading