From fe6d53a965848ae6214491a4c62bb684bd80483b Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Tue, 21 Oct 2025 21:43:00 -0600 Subject: [PATCH] got Find Communities to work --- communityadmin.go | 8 +++- database/community.go | 108 ++++++++++++++++++++++++++++++++++++------ find.go | 100 ++++++++++++++++++++++++++++++++++++++ main.go | 1 + ui/views/find.jet | 27 +++++++++-- util/util.go | 31 ++++++++++++ 6 files changed, 254 insertions(+), 21 deletions(-) diff --git a/communityadmin.go b/communityadmin.go index 56ab4fb..352cb12 100644 --- a/communityadmin.go +++ b/communityadmin.go @@ -131,7 +131,13 @@ func CommunityProfileForm(ctxt ui.AmContext) (string, any, error) { dlg.Field("joinkey").SetVal(comm.JoinKey) } dlg.Field("membersonly").SetChecked(comm.MembersOnly) - dlg.Field("hidemode").Value = comm.HideMode() + if comm.HideFromSearch { + dlg.Field("hidemode").Value = "BOTH" + } else if comm.HideFromDirectory { + dlg.Field("hidemode").Value = "DIRECTORY" + } else { + dlg.Field("hidemode").Value = "NONE" + } dlg.Field("read_lvl").Value = fmt.Sprintf("%d", comm.ReadLevel) dlg.Field("write_lvl").Value = fmt.Sprintf("%d", comm.WriteLevel) dlg.Field("create_lvl").Value = fmt.Sprintf("%d", comm.CreateLevel) diff --git a/database/community.go b/database/community.go index a4c431f..b56c56c 100644 --- a/database/community.go +++ b/database/community.go @@ -15,6 +15,7 @@ import ( "fmt" "slices" "strconv" + "strings" "sync" "time" @@ -69,6 +70,16 @@ const ( CommunityFlagPicturesInPosts = uint(0) ) +// Field and operation selectors for AmSearchCommunities. +const ( + SearchCommFieldName = 0 + SearchCommFieldSynopsis = 1 + + SearchCommOperPrefix = 0 + SearchCommOperSubstring = 1 + SearchCommOperRegex = 2 +) + // communityCache is the cache for Community objects. var communityCache *lru.TwoQueueCache = nil @@ -163,16 +174,6 @@ func (c *Community) LanguageTag() (*language.Tag, error) { return &t, nil } -func (c *Community) HideMode() string { - if c.HideFromSearch { - return "BOTH" - } else if c.HideFromDirectory { - return "DIRECTORY" - } else { - return "NONE" - } -} - /* Membership returns the details of the specified user's membership in the community. * Parameters: * u - The user to check the membership of. @@ -737,11 +738,88 @@ func AmGetCommunitiesForCategory(catid int32, offset int, max int, showAll bool) if err != nil { return nil, total, err } - rcCap := max - if rcCap > 10000 { - rcCap = 10000 - } - rc := make([]*Community, 0, rcCap) + rc := make([]*Community, 0, min(max, 10000)) + for rs.Next() { + var commid int32 + rs.Scan(&commid) + c, err := AmGetCommunity(commid) + if err == nil { + rc = append(rc, c) + } + } + return rc, total, nil +} + +/* AmSearchCommunities searches for communities matching certain criteria. + * Parameters: + * field - A value indicating which field to search: + * SearchCommFieldName - The community name. + * SearchCommFieldSynopsis - The communty synopsis. + * oper - The operation to perform on the search field: + * SearchCommOperPrefix - The specified field has the string "term" as a prefix. + * SearchCommOperSubstring - The specified field contains the string "term". + * SearchCommOperRegex - 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. + * showAll - Include communities that are "hidden in search." + * Returns: + * Array of Community pointers representing the return elements. + * The total number of communities matching this query (could be greater than max) + * Standard Go error status. + */ +func AmSearchCommunities(field int, oper int, term string, offset int, max int, showAll bool) ([]*Community, int, error) { + var queryPortion strings.Builder + queryPortion.WriteString("WHERE ") + switch field { + case SearchCommFieldName: + queryPortion.WriteString("commname ") + case SearchCommFieldSynopsis: + queryPortion.WriteString("synopsis ") + default: + return nil, -1, errors.New("invalid field selector") + } + switch oper { + case SearchCommOperPrefix: + queryPortion.WriteString("LIKE '") + queryPortion.WriteString(util.SqlEscape(term, true)) + queryPortion.WriteString("%'") + case SearchCommOperSubstring: + queryPortion.WriteString("LIKE '%") + queryPortion.WriteString(util.SqlEscape(term, true)) + queryPortion.WriteString("%'") + case SearchCommOperRegex: + queryPortion.WriteString("REGEXP '") + queryPortion.WriteString(util.SqlEscape(term, false)) + queryPortion.WriteString("'") + default: + return nil, -1, errors.New("invalid operator selector") + } + if !showAll { + queryPortion.WriteString(" AND hide_search = 0") + } + q := queryPortion.String() + rs, err := amdb.Query("SELECT COUNT(*) FROM communities " + q) + if err != nil { + return nil, -1, err + } + if !rs.Next() { + return nil, -1, errors.New("internal error getting count") + } + var total int + rs.Scan(&total) + if total == 0 { + return make([]*Community, 0), 0, nil // short-circuit return + } + if offset > 0 { + rs, err = amdb.Query("SELECT commid FROM communities "+q+" ORDER BY commname LIMIT ? OFFSET ?", max, offset) + } else { + rs, err = amdb.Query("SELECT commid FROM communities "+q+" ORDER BY commname LIMIT ?", max) + } + if err != nil { + return nil, total, err + } + rc := make([]*Community, 0, min(max, 10000)) for rs.Next() { var commid int32 rs.Scan(&commid) diff --git a/find.go b/find.go index e0eb77b..0a1d86d 100644 --- a/find.go +++ b/find.go @@ -19,6 +19,9 @@ import ( // loadCategoryInformation loads the current category information to the context. func loadCategoryInformation(ctxt ui.AmContext, offset int) error { + if ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) { + return nil + } u := ctxt.CurrentUser() catid := int32(-1) p := ctxt.Parameter("catid") @@ -112,6 +115,9 @@ func FindPage(ctxt ui.AmContext) (string, any, error) { ofs = v } } + if !ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) { + ctxt.VarMap().Set("catIsPresent", true) + } ctxt.SetSession("find.mode", mode) ctxt.VarMap().Set("mode", mode) ctxt.VarMap().Set("ofs", ofs) @@ -142,3 +148,97 @@ func FindPage(ctxt ui.AmContext) (string, any, error) { ctxt.SetLeftMenu("top") return "framed_template", "find.jet", nil } + +/* Find performs the "find" operation. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ +func Find(ctxt ui.AmContext) (string, any, error) { + if !ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) { + ctxt.VarMap().Set("catIsPresent", true) + } + mode := ctxt.FormField("mode") + ctxt.VarMap().Set("mode", mode) + field := ctxt.FormField("field") + ctxt.VarMap().Set("field", field) + oper := ctxt.FormField("oper") + ctxt.VarMap().Set("oper", oper) + term := ctxt.FormField("term") + ctxt.VarMap().Set("term", term) + ctxt.VarMap().Set("amsterdam_pageTitle", "Find") + ctxt.SetLeftMenu("top") + ofs, _ := ctxt.FormFieldInt("ofs") + if ctxt.FormFieldIsSet("search") { + ofs = 0 + } else if ctxt.FormFieldIsSet("prev") { + ofs -= 1 + } else if ctxt.FormFieldIsSet("next") { + ofs += 1 + } + ctxt.VarMap().Set("ofs", ofs) + listMax := int(ctxt.Globals().MaxSearchPage) + var numResults, total int + var err error + switch mode { + case "COM": + var iField, iOper int + switch field { + case "name": + iField = database.SearchCommFieldName + case "synopsis": + iField = database.SearchCommFieldSynopsis + default: + ctxt.VarMap().Set("errorMessage", "invalid parameter to find") + return "framed_template", "find.jet", nil + } + switch oper { + case "st": + iOper = database.SearchCommOperPrefix + case "in": + iOper = database.SearchCommOperSubstring + case "re": + iOper = database.SearchCommOperRegex + default: + ctxt.VarMap().Set("errorMessage", "invalid parameter to find") + return "framed_template", "find.jet", nil + } + var clist []*database.Community + clist, total, err = database.AmSearchCommunities(iField, iOper, term, ofs*listMax, listMax, + ctxt.TestPermission("Global.SearchHiddenCommunities")) + if err == nil { + if clist == nil { + numResults = 0 + } else { + numResults = len(clist) + ctxt.VarMap().Set("resultList", clist) + } + } + case "USR": + // TODO + case "CAT": + // TODO + case "PST": + // TODO + } + if err != nil { + ctxt.VarMap().Set("errorMessage", err.Error()) + return "framed_template", "find.jet", nil + } + if numResults == 0 { + ctxt.VarMap().Set("resultHeader", "Search Results: (None)") + } else { + ctxt.VarMap().Set("resultHeader", fmt.Sprintf("Search Results: Displaying %d-%d of %d", + ofs*listMax+1, ofs*listMax+numResults, total)) + if ofs > 0 { + ctxt.VarMap().Set("resultShowPrev", true) + } + if ofs*listMax+numResults < total { + ctxt.VarMap().Set("resultShowNext", true) + } + } + return "framed_template", "find.jet", nil +} diff --git a/main.go b/main.go index c3a7c3d..8f3f609 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,7 @@ func setupEcho() *echo.Echo { e.GET("/profile_photo", ui.AmWrap(ProfilePhotoForm)) e.POST("/profile_photo", ui.AmWrap(ProfilePhoto)) e.GET("/find", ui.AmWrap(FindPage)) + e.POST("/find", ui.AmWrap(Find)) e.GET("/user/:uname", ui.AmWrap(ShowProfile)) e.POST("/quick_email", ui.AmWrap(QuickEMail)) e.GET("/sysadmin", ui.AmWrap(SysAdminMenu)) diff --git a/ui/views/find.jet b/ui/views/find.jet index b322c76..90923a1 100644 --- a/ui/views/find.jet +++ b/ui/views/find.jet @@ -24,12 +24,14 @@ Users {{ end }} | - {{ if mode == "CAT" }} - Categories - {{ else }} - Categories + {{ if isset(catIsPresent) }} + {{ if mode == "CAT" }} + Categories + {{ else }} + Categories + {{ end }} + | {{ end }} - | {{ if mode == "PST" }} Posts {{ else }} @@ -40,6 +42,20 @@
+ {{ if isset(errorMessage) }} + +
+
+
+ ⚠️ +
+
+

{{ CapitalizeString(errorMessage) }}.

+
+
+
+ {{ end }} +
@@ -254,6 +270,7 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors"> ⏪ Prev + {{ end }} {{ if isset(resultShowNext) }}