diff --git a/go.mod b/go.mod index 07a38cf..f6672c0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Life-USTC/CLI go 1.26.0 require ( - github.com/Life-USTC/client-core v0.1.0 + github.com/Life-USTC/client-core v0.3.0 github.com/PuerkitoBio/goquery v1.13.0 github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 diff --git a/go.sum b/go.sum index a44c64b..bec693d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Life-USTC/client-core v0.1.0 h1:kAtPq/m/W7SEMiR9KG/m2F0ZRyBXnzkNbfoKa4mMDmc= -github.com/Life-USTC/client-core v0.1.0/go.mod h1:2SgYKZ3hbEFEigNGPdbfFS2m149evVw2t/mEjA/S/LM= +github.com/Life-USTC/client-core v0.3.0 h1:hpTnY1x6CIVQDIH0PoZdg7/9K+BEpaSss3BlaVdCHWs= +github.com/Life-USTC/client-core v0.3.0/go.mod h1:2SgYKZ3hbEFEigNGPdbfFS2m149evVw2t/mEjA/S/LM= github.com/PuerkitoBio/goquery v1.13.0 h1:mqHbjD7Jmnul4DTR24LKTjo1uUmHUh072kteGV+xpFM= github.com/PuerkitoBio/goquery v1.13.0/go.mod h1:Hip5mdBL8K2wEGKJdr27sRaNwIdDajmCwB/ExUPwW+g= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= diff --git a/internal/cmd/young_event/young_event.go b/internal/cmd/young_event/young_event.go index 5fda25c..6375e7b 100644 --- a/internal/cmd/young_event/young_event.go +++ b/internal/cmd/young_event/young_event.go @@ -15,6 +15,7 @@ import ( "github.com/Life-USTC/CLI/internal/cmd/cmdutil" "github.com/Life-USTC/CLI/internal/cmd/youngutil" "github.com/Life-USTC/CLI/internal/output" + "github.com/Life-USTC/client-core/young" ) type listOpts struct { @@ -166,47 +167,53 @@ func buildListParams(opts listOpts) (url.Values, error) { if err != nil { return nil, err } - active, err := normalizeActive(opts.active) + query, err := buildEventQuery(opts) if err != nil { return nil, err } + for key, values := range query.Parameters(nil, nil) { + params[key] = values + } + return params, nil +} + +func buildEventQuery(opts listOpts) (young.EventQuery, error) { + query := young.EventQuery{ + Category: opts.category, Search: opts.search, OrganizerID: opts.organizerID, + } + active, err := normalizeActive(opts.active) + if err != nil { + return query, err + } if active != "" { - params.Set("active", active) + value := active == "true" + query.Active = &value } dateUnknown, err := normalizeBooleanFilter("--date-unknown", opts.dateUnknown) if err != nil { - return nil, err + return query, err } from := strings.TrimSpace(opts.dateFrom) to := strings.TrimSpace(opts.dateTo) if (from == "") != (to == "") { - return nil, fmt.Errorf("--date-from and --date-to must be provided together") + return query, fmt.Errorf("--date-from and --date-to must be provided together") } if dateUnknown != "" && (from != "" || to != "") { - return nil, fmt.Errorf("--date-unknown cannot be combined with --date-from or --date-to") + return query, fmt.Errorf("--date-unknown cannot be combined with --date-from or --date-to") } if dateUnknown != "" { - params.Set("dateUnknown", dateUnknown) - } - for key, value := range map[string]string{ - "category": opts.category, - "search": opts.search, - "organizerId": opts.organizerID, - "dateFrom": from, - "dateTo": to, - } { - if value = strings.TrimSpace(value); value != "" { - params.Set(key, value) - } + value := dateUnknown == "true" + query.DateUnknown = &value } + query.DateFrom, query.DateTo = from, to timeBasis := strings.TrimSpace(opts.timeBasis) if timeBasis != "" { if timeBasis != "activity" && timeBasis != "registration" { - return nil, fmt.Errorf("--time-basis must be activity or registration") + return query, fmt.Errorf("--time-basis must be activity or registration") } - params.Set("timeBasis", timeBasis) + query.TimeBasis = young.TimeBasis(timeBasis) } - return params, nil + return query, nil } func normalizeActive(value string) (string, error) { @@ -360,7 +367,7 @@ func runDateView(cmd *cobra.Command, view youngutil.DateView, anchor string, opt dateTo: dateTo, timeBasis: opts.timeBasis, } - query, err := buildListParams(params) + query, err := buildEventQuery(params) if err != nil { return err } @@ -368,7 +375,7 @@ func runDateView(cmd *cobra.Command, view youngutil.DateView, anchor string, opt if err != nil { return err } - data, err := fetchDateEvents(cmd.Context(), client, query) + data, err := fetchDateEvents(cmd.Context(), client, query.Parameters(nil, nil)) if err != nil { return err } @@ -379,7 +386,7 @@ func runDateView(cmd *cobra.Command, view youngutil.DateView, anchor string, opt {Key: "Last synced", Value: output.Resolve(meta, "source.lastSyncedAt")}, {Key: "Unknown dates", Value: output.Resolve(meta, "unknownDateCount")}, }, "Calendar source") - fmt.Println("Unknown-date activities: life-ustc catalog young-event list --date-unknown true --time-basis " + opts.timeBasis) + fmt.Println("Unknown-date activities: " + unknownDateCommand(query)) } list := cmdutil.NewListResult(data, "data") return output.OutputList(list.Raw, list.Rows, []output.Column{ @@ -392,6 +399,20 @@ func runDateView(cmd *cobra.Command, view youngutil.DateView, anchor string, opt }, list.Total, list.Page) } +func unknownDateCommand(query young.EventQuery) string { + params := query.ForUnknownDates().Parameters(nil, nil) + command := "life-ustc catalog young-event list --date-unknown " + params.Get("dateUnknown") + for _, field := range [][2]string{ + {"active", "active"}, {"category", "category"}, {"search", "search"}, + {"organizer-id", "organizerId"}, {"time-basis", "timeBasis"}, + } { + if value := params.Get(field[1]); value != "" { + command += " --" + field[0] + " '" + strings.ReplaceAll(value, "'", "'\\''") + "'" + } + } + return command +} + func dateStartKey(timeBasis string) string { if strings.TrimSpace(timeBasis) == "registration" { return "applyStartAt" diff --git a/internal/cmd/young_event/young_event_http_test.go b/internal/cmd/young_event/young_event_http_test.go index 092dbb8..dffe441 100644 --- a/internal/cmd/young_event/young_event_http_test.go +++ b/internal/cmd/young_event/young_event_http_test.go @@ -5,9 +5,16 @@ import ( "io" "net/http" "net/http/httptest" + "os" + "path/filepath" + "reflect" + "strings" "testing" "github.com/Life-USTC/CLI/internal/api" + "github.com/Life-USTC/CLI/internal/cmd/youngutil" + "github.com/Life-USTC/CLI/internal/output" + "github.com/spf13/cobra" ) func TestFetchListMapsAllServerFilters(t *testing.T) { @@ -46,3 +53,44 @@ func TestFetchListMapsAllServerFilters(t *testing.T) { t.Fatal(err) } } + +func TestDateViewUnknownDateHintPreservesFilters(t *testing.T) { + t.Setenv("LIFE_USTC_CONFIG_DIR", t.TempDir()) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + want := map[string][]string{ + "active": {"false"}, "category": {"Science & Tech"}, "search": {"freshmen's fair"}, + "organizerId": {"org/科大+1"}, "timeBasis": {"registration"}, + "dateFrom": {"2026-09-15"}, "dateTo": {"2026-09-15"}, "page": {"1"}, "pageSize": {"100"}, + } + if r.URL.Path != "/api/catalog/young-events" || !reflect.DeepEqual(map[string][]string(r.URL.Query()), want) { + t.Errorf("request = %s, want filters %#v", r.URL, want) + } + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"data":[],"pagination":{"page":1,"pageSize":100,"total":0,"totalPages":1},"meta":{"unknownDateCount":2,"source":{"status":"unknown","lastSyncedAt":null}}}`) + })) + defer server.Close() + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + cmd.PersistentFlags().String("server", server.URL, "") + file, err := os.Create(filepath.Join(t.TempDir(), "output.txt")) + if err != nil { + t.Fatal(err) + } + oldStdout, oldOutput := os.Stdout, output.Current + os.Stdout, output.Current = file, &output.Opts{Format: "table", NoColor: true} + t.Cleanup(func() { os.Stdout, output.Current = oldStdout, oldOutput; _ = file.Close() }) + if err := runDateView(cmd, youngutil.DateDay, "2026-09-15", dateOpts{ + active: "false", category: "Science & Tech", search: "freshmen's fair", + organizerID: "org/科大+1", timeBasis: "registration", + }); err != nil { + t.Fatal(err) + } + printed, err := os.ReadFile(file.Name()) + if err != nil { + t.Fatal(err) + } + want := "Unknown-date activities: life-ustc catalog young-event list --date-unknown true --active 'false' --category 'Science & Tech' --search 'freshmen'\\''s fair' --organizer-id 'org/科大+1' --time-basis 'registration'\n" + if !strings.Contains(string(printed), want) { + t.Fatalf("output = %s\nwant hint = %s", printed, want) + } +} diff --git a/internal/cmd/young_event/young_event_test.go b/internal/cmd/young_event/young_event_test.go index e6454ba..7336487 100644 --- a/internal/cmd/young_event/young_event_test.go +++ b/internal/cmd/young_event/young_event_test.go @@ -82,6 +82,23 @@ func TestBuildListParamsSupportsUnknownDateFilter(t *testing.T) { } } +func TestBuildListParamsKeepsFalseDistinctFromAbsent(t *testing.T) { + params, err := buildListParams(listOpts{active: " false ", dateUnknown: "false", search: " 科大 & club "}) + if err != nil { + t.Fatal(err) + } + if len(params) != 3 || params.Get("active") != "false" || params.Get("dateUnknown") != "false" || params.Get("search") != "科大 & club" { + t.Fatalf("params = %#v", params) + } + absent, err := buildListParams(listOpts{}) + if err != nil || len(absent) != 0 { + t.Fatalf("absent filters = %#v, %v", absent, err) + } + if _, err := buildListParams(listOpts{dateUnknown: "false", dateFrom: "2026-09-01", dateTo: "2026-09-30"}); err == nil { + t.Fatal("CLI accepted explicit dateUnknown=false with date bounds") + } +} + func TestDateRegistrationUsesRegistrationColumns(t *testing.T) { if got := dateStartKey("registration"); got != "applyStartAt" { t.Fatalf("registration start key = %q", got)