got Find Communities to work
This commit is contained in:
+7
-1
@@ -131,7 +131,13 @@ func CommunityProfileForm(ctxt ui.AmContext) (string, any, error) {
|
|||||||
dlg.Field("joinkey").SetVal(comm.JoinKey)
|
dlg.Field("joinkey").SetVal(comm.JoinKey)
|
||||||
}
|
}
|
||||||
dlg.Field("membersonly").SetChecked(comm.MembersOnly)
|
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("read_lvl").Value = fmt.Sprintf("%d", comm.ReadLevel)
|
||||||
dlg.Field("write_lvl").Value = fmt.Sprintf("%d", comm.WriteLevel)
|
dlg.Field("write_lvl").Value = fmt.Sprintf("%d", comm.WriteLevel)
|
||||||
dlg.Field("create_lvl").Value = fmt.Sprintf("%d", comm.CreateLevel)
|
dlg.Field("create_lvl").Value = fmt.Sprintf("%d", comm.CreateLevel)
|
||||||
|
|||||||
+93
-15
@@ -15,6 +15,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -69,6 +70,16 @@ const (
|
|||||||
CommunityFlagPicturesInPosts = uint(0)
|
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.
|
// communityCache is the cache for Community objects.
|
||||||
var communityCache *lru.TwoQueueCache = nil
|
var communityCache *lru.TwoQueueCache = nil
|
||||||
|
|
||||||
@@ -163,16 +174,6 @@ func (c *Community) LanguageTag() (*language.Tag, error) {
|
|||||||
return &t, nil
|
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.
|
/* Membership returns the details of the specified user's membership in the community.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* u - The user to check the membership of.
|
* 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 {
|
if err != nil {
|
||||||
return nil, total, err
|
return nil, total, err
|
||||||
}
|
}
|
||||||
rcCap := max
|
rc := make([]*Community, 0, min(max, 10000))
|
||||||
if rcCap > 10000 {
|
for rs.Next() {
|
||||||
rcCap = 10000
|
var commid int32
|
||||||
}
|
rs.Scan(&commid)
|
||||||
rc := make([]*Community, 0, rcCap)
|
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() {
|
for rs.Next() {
|
||||||
var commid int32
|
var commid int32
|
||||||
rs.Scan(&commid)
|
rs.Scan(&commid)
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import (
|
|||||||
|
|
||||||
// loadCategoryInformation loads the current category information to the context.
|
// loadCategoryInformation loads the current category information to the context.
|
||||||
func loadCategoryInformation(ctxt ui.AmContext, offset int) error {
|
func loadCategoryInformation(ctxt ui.AmContext, offset int) error {
|
||||||
|
if ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
u := ctxt.CurrentUser()
|
u := ctxt.CurrentUser()
|
||||||
catid := int32(-1)
|
catid := int32(-1)
|
||||||
p := ctxt.Parameter("catid")
|
p := ctxt.Parameter("catid")
|
||||||
@@ -112,6 +115,9 @@ func FindPage(ctxt ui.AmContext) (string, any, error) {
|
|||||||
ofs = v
|
ofs = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) {
|
||||||
|
ctxt.VarMap().Set("catIsPresent", true)
|
||||||
|
}
|
||||||
ctxt.SetSession("find.mode", mode)
|
ctxt.SetSession("find.mode", mode)
|
||||||
ctxt.VarMap().Set("mode", mode)
|
ctxt.VarMap().Set("mode", mode)
|
||||||
ctxt.VarMap().Set("ofs", ofs)
|
ctxt.VarMap().Set("ofs", ofs)
|
||||||
@@ -142,3 +148,97 @@ func FindPage(ctxt ui.AmContext) (string, any, error) {
|
|||||||
ctxt.SetLeftMenu("top")
|
ctxt.SetLeftMenu("top")
|
||||||
return "framed_template", "find.jet", nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func setupEcho() *echo.Echo {
|
|||||||
e.GET("/profile_photo", ui.AmWrap(ProfilePhotoForm))
|
e.GET("/profile_photo", ui.AmWrap(ProfilePhotoForm))
|
||||||
e.POST("/profile_photo", ui.AmWrap(ProfilePhoto))
|
e.POST("/profile_photo", ui.AmWrap(ProfilePhoto))
|
||||||
e.GET("/find", ui.AmWrap(FindPage))
|
e.GET("/find", ui.AmWrap(FindPage))
|
||||||
|
e.POST("/find", ui.AmWrap(Find))
|
||||||
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
|
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
|
||||||
e.POST("/quick_email", ui.AmWrap(QuickEMail))
|
e.POST("/quick_email", ui.AmWrap(QuickEMail))
|
||||||
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
|
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
|
||||||
|
|||||||
+22
-5
@@ -24,12 +24,14 @@
|
|||||||
<a href="/find?mode=USR" class="text-blue-700 hover:text-blue-900">Users</a>
|
<a href="/find?mode=USR" class="text-blue-700 hover:text-blue-900">Users</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<span class="mx-2">|</span>
|
<span class="mx-2">|</span>
|
||||||
{{ if mode == "CAT" }}
|
{{ if isset(catIsPresent) }}
|
||||||
<span class="font-bold">Categories</span>
|
{{ if mode == "CAT" }}
|
||||||
{{ else }}
|
<span class="font-bold">Categories</span>
|
||||||
<a href="/find?mode=CAT" class="text-blue-700 hover:text-blue-900">Categories</a>
|
{{ else }}
|
||||||
|
<a href="/find?mode=CAT" class="text-blue-700 hover:text-blue-900">Categories</a>
|
||||||
|
{{ end }}
|
||||||
|
<span class="mx-2">|</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<span class="mx-2">|</span>
|
|
||||||
{{ if mode == "PST" }}
|
{{ if mode == "PST" }}
|
||||||
<span class="font-bold">Posts</span>
|
<span class="font-bold">Posts</span>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
@@ -40,6 +42,20 @@
|
|||||||
<hr class="border-2 border-gray-400 w-4/5 mt-2 mb-6">
|
<hr class="border-2 border-gray-400 w-4/5 mt-2 mb-6">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ if isset(errorMessage) }}
|
||||||
|
<!-- Error Message Banner -->
|
||||||
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6" id="error-banner">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="text-red-500 text-xl">⚠️</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm font-medium" id="error-message">{{ CapitalizeString(errorMessage) }}.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<!-- Search Form -->
|
<!-- Search Form -->
|
||||||
<div class="max-w-3xl mb-8">
|
<div class="max-w-3xl mb-8">
|
||||||
<form method="POST" action="/find">
|
<form method="POST" action="/find">
|
||||||
@@ -254,6 +270,7 @@
|
|||||||
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||||
⏪ Prev
|
⏪ Prev
|
||||||
</button>
|
</button>
|
||||||
|
{{ end }}
|
||||||
{{ if isset(resultShowNext) }}
|
{{ if isset(resultShowNext) }}
|
||||||
<button type="submit" name="next"
|
<button type="submit" name="next"
|
||||||
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,6 +41,36 @@ func CapitalizeString(s string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* SqlEscape escapes a string in SQL terms.
|
||||||
|
* Parameters:
|
||||||
|
* s - The string to be escaped.
|
||||||
|
* wildcards - If true, also escape the wildcard characters % and _.
|
||||||
|
* Returns:
|
||||||
|
* The escaped string.
|
||||||
|
*/
|
||||||
|
func SqlEscape(s string, wildcards bool) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
c := s[i]
|
||||||
|
switch c {
|
||||||
|
case '\\', 0, '\n', '\r', '\'', '"':
|
||||||
|
sb.WriteByte('\\')
|
||||||
|
sb.WriteByte(c)
|
||||||
|
case '\032':
|
||||||
|
sb.WriteByte('\\')
|
||||||
|
sb.WriteByte('Z')
|
||||||
|
case '%', '_':
|
||||||
|
if wildcards {
|
||||||
|
sb.WriteByte('\\')
|
||||||
|
}
|
||||||
|
sb.WriteByte(c)
|
||||||
|
default:
|
||||||
|
sb.WriteByte(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
/* IsNumeric returns true if the string is numeric (all digits).
|
/* IsNumeric returns true if the string is numeric (all digits).
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* s - String to be tested.
|
* s - String to be tested.
|
||||||
|
|||||||
Reference in New Issue
Block a user