From 69e7a2a39a849181cfba258013fa908e3f055e2d Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Tue, 21 Oct 2025 23:11:47 -0600 Subject: [PATCH] got Find Categories to work --- database/category.go | 128 ++++++++++++++++++++++++++++++++++++++++++- database/user.go | 4 +- find.go | 25 ++++++++- ui/templates.go | 14 +++++ ui/views/find.jet | 9 ++- 5 files changed, 173 insertions(+), 7 deletions(-) diff --git a/database/category.go b/database/category.go index b43977b..1d60c13 100644 --- a/database/category.go +++ b/database/category.go @@ -14,6 +14,8 @@ import ( "slices" "strings" "sync" + + "git.erbosoft.com/amy/amsterdam/util" ) // Category is the structure defining a category. @@ -26,6 +28,13 @@ type Category struct { Name string `db:"name"` } +// Selectors for operator in category search. +const ( + SearchCatOperPrefix = 0 + SearchCatOperSubstring = 1 + SearchCatOperRegex = 2 +) + // allCategories is the list of all categories loaded from the database. var allCategories []Category @@ -35,6 +44,19 @@ var categoryIdMap map[int32]*Category = make(map[int32]*Category) // categoryMutex syncs the loading of the categories. var categoryMutex sync.Mutex +// isCatEnabled determines if category features are enabled. +func isCatEnabled() (bool, error) { + g, err := AmGlobals() + if err != nil { + return false, err + } + set, err := g.Flags() + if err != nil { + return false, err + } + return !set.Get(GlobalFlagNoCategories), nil +} + // loadCategories loads the categories list from the database. func loadCategories() error { categoryMutex.Lock() @@ -69,7 +91,14 @@ func loadCategories() error { * Standard Go error status. */ func AmGetCategory(catid int32) (*Category, error) { - err := loadCategories() + ok, err := isCatEnabled() + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("category feature not supported") + } + err = loadCategories() if err != nil { return nil, err } @@ -93,7 +122,14 @@ func AmGetCategory(catid int32) (*Category, error) { * Standard Go error status. */ func AmGetCategoryHierarchy(catid int32) ([]*Category, error) { - err := loadCategories() + ok, err := isCatEnabled() + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("category feature not supported") + } + err = loadCategories() if err != nil { return nil, err } @@ -124,7 +160,14 @@ func AmGetCategoryHierarchy(catid int32) ([]*Category, error) { * Standard Go error status. */ func AmGetSubCategories(catid int32) ([]*Category, error) { - err := loadCategories() + ok, err := isCatEnabled() + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("category feature not supported") + } + err = loadCategories() if err != nil { return nil, err } @@ -139,3 +182,82 @@ func AmGetSubCategories(catid int32) ([]*Category, error) { }) return rc, nil } + +/* AmSearchCategories searches for categories matching certain criteria. + * Parameters: + * oper - The operation to perform on the category name: + * SearchCatOperPrefix - The category name has the string "term" as a prefix. + * SearchCatOperSubstring - The category name contains the string "term". + * SearchCatOperRegex - The category name matches the regular expression in "term". + * term - The search term, as specified above. + * offset - Number of categories to skip at beginning of list. + * max - Maximum number of categories to return. + * Returns: + * Array of Category pointers representing the return elements. + * The total number of categories matching this query (could be greater than max) + * Standard Go error status. + */ +func AmSearchCategories(oper int, term string, offset int, max int, showAll bool, searchAll bool) ([]*Category, int, error) { + ok, err := isCatEnabled() + if err != nil { + return nil, -1, err + } + if !ok { + return nil, -1, errors.New("category feature not supported") + } + var queryString strings.Builder + queryString.WriteString("name ") + switch oper { + case SearchCatOperPrefix: + queryString.WriteString("LIKE '") + queryString.WriteString(util.SqlEscape(term, true)) + queryString.WriteString("%'") + case SearchCatOperSubstring: + queryString.WriteString("LIKE '%") + queryString.WriteString(util.SqlEscape(term, true)) + queryString.WriteString("%'") + case SearchCatOperRegex: + queryString.WriteString("REGEXP '") + queryString.WriteString(util.SqlEscape(term, false)) + queryString.WriteString("'") + default: + return nil, -1, errors.New("invalid operator to search function") + } + if !showAll { + queryString.WriteString(" AND hide_dir = 0") + } + if !searchAll { + queryString.WriteString(" AND hide_search = 0") + } + q := queryString.String() + rs, err := amdb.Query("SELECT COUNT(*) FROM refcategory WHERE " + q) + if err != nil { + return nil, -1, err + } + if !rs.Next() { + return nil, -1, errors.New("internal error getting category total") + } + var total int + rs.Scan(&total) + if total == 0 { + return make([]*Category, 0), 0, nil + } + if offset > 0 { + rs, err = amdb.Query("SELECT catid FROM refcategory WHERE "+q+" ORDER BY parent, name LIMIT ? OFFSET ?", max, offset) + } else { + rs, err = amdb.Query("SELECT catid FROM refcategory WHERE "+q+" ORDER BY parent, name LIMIT ?", max) + } + if err != nil { + return nil, total, err + } + rc := make([]*Category, 0, min(max, 1000)) + for rs.Next() { + var catid int32 + rs.Scan(&catid) + c, err := AmGetCategory(catid) + if err == nil { + rc = append(rc, c) + } + } + return rc, total, nil +} diff --git a/database/user.go b/database/user.go index 7b0dd95..ad489ac 100644 --- a/database/user.go +++ b/database/user.go @@ -782,8 +782,8 @@ func AmSetUserProperty(uid int32, ndx int32, val *string) error { * SearchUserOperSubstring - The specified field contains the string "term". * SearchUserOperRegex - The specified field matches the regular expression in "term". * term - The search term, as specified above. - * offset - Number of communities to skip at beginning of list. - * max - Maximum number of communities to return. + * offset - Number of users to skip at beginning of list. + * max - Maximum number of users to return. * Returns: * Array of User pointers representing the return elements. * The total number of users matching this query (could be greater than max) diff --git a/find.go b/find.go index 192265a..d90ca0a 100644 --- a/find.go +++ b/find.go @@ -254,7 +254,30 @@ func Find(ctxt ui.AmContext) (string, any, error) { } } case "CAT": - // TODO + listMax = 20 + var iOper int + switch oper { + case "st": + iOper = database.SearchCatOperPrefix + case "in": + iOper = database.SearchCatOperSubstring + case "re": + iOper = database.SearchCatOperRegex + default: + ctxt.VarMap().Set("errorMessage", "invalid parameter to find") + return "framed_template", "find.jet", nil + } + var catlist []*database.Category + catlist, total, err = database.AmSearchCategories(iOper, term, ofs*listMax, listMax, + ctxt.TestPermission("Global.ShowHiddenCategories"), ctxt.TestPermission("Global.SearchHiddenCategories")) + if err == nil { + if catlist == nil { + numResults = 0 + } else { + numResults = len(catlist) + ctxt.VarMap().Set("resultList", catlist) + } + } case "PST": // TODO } diff --git a/ui/templates.go b/ui/templates.go index c0f95d6..0dc471d 100644 --- a/ui/templates.go +++ b/ui/templates.go @@ -164,6 +164,19 @@ func displayFullName(a jet.Arguments) reflect.Value { return reflect.ValueOf(rc.String()) } +func displayExpandCat(a jet.Arguments) reflect.Value { + cat := a.Get(0).Convert(reflect.TypeFor[*database.Category]()).Interface().(*database.Category) + hier, _ := database.AmGetCategoryHierarchy(cat.CatId) + var rc strings.Builder + for i, c := range hier { + if i > 0 { + rc.WriteString(": ") + } + rc.WriteString(c.Name) + } + return reflect.ValueOf(rc.String()) +} + // SetupTemplates is called to set up the template renderer after the configuration is loaded. func SetupTemplates() { views = jet.NewSet( @@ -182,6 +195,7 @@ func SetupTemplates() { views.AddGlobalFunc("DisplayActivity", displayActivity) views.AddGlobalFunc("DisplayMemberCount", displayMemberCount) views.AddGlobalFunc("DisplayFullName", displayFullName) + views.AddGlobalFunc("DisplayExpandCat", displayExpandCat) views.AddGlobalFunc("GetCountryList", func(jet.Arguments) reflect.Value { return reflect.ValueOf(util.AmCountryList()) diff --git a/ui/views/find.jet b/ui/views/find.jet index 1fb5ba3..106f54b 100644 --- a/ui/views/find.jet +++ b/ui/views/find.jet @@ -252,7 +252,14 @@ {{ else if mode == "CAT" }} - TODO: I don't know CAT yet + +
+ 🟣 + +
{{ else if mode == "PST" }} TODO: I don't know PST yet {{ end }}