Blog multi-tag query: whitelist and blacklist

* Can query blog posts by multiple tags now.
* e.g. /tagged/blog,updates,-photos would query all posts that have tags
  "blog" OR "updates" but NOT show any post with tag "photos"
master
Noah 2020-02-17 20:26:30 -08:00
parent bf86ceb585
commit b642562792
2 changed files with 73 additions and 8 deletions

View File

@ -141,14 +141,16 @@ func (m postMan) GetPostsByTag(tag, privacy string, page, perPage int) (PagedPos
pp.PerPage = 20 pp.PerPage = 20
} }
// Get the distinct post IDs for this tag. // Multi-tag query.
var tags []TaggedPost whitelist, blacklist, err := ParseMultitag(tag)
var postIDs []uint if err != nil {
r := DB.Where("tag = ?", tag).Find(&tags) return pp, err
for _, taggedPost := range tags {
postIDs = append(postIDs, taggedPost.PostID)
} }
// Query the whitelist of post IDs which match the whitelist tags.
postIDs := getPostIDsByTag("tag IN (?)", whitelist)
notPostIDs := getPostIDsByTag("tag IN (?)", blacklist)
postIDs = narrowWhitelistByBlacklist(postIDs, notPostIDs)
if len(postIDs) == 0 { if len(postIDs) == 0 {
return pp, errors.New("no posts found") return pp, errors.New("no posts found")
} }
@ -162,7 +164,7 @@ func (m postMan) GetPostsByTag(tag, privacy string, page, perPage int) (PagedPos
query.Model(&Post{}).Count(&pp.Total) query.Model(&Post{}).Count(&pp.Total)
// Query the paginated slice of results. // Query the paginated slice of results.
r = query. r := query.
Offset((page - 1) * perPage). Offset((page - 1) * perPage).
Limit(perPage). Limit(perPage).
Find(&pp.Posts) Find(&pp.Posts)
@ -182,6 +184,43 @@ func (m postMan) GetPostsByTag(tag, privacy string, page, perPage int) (PagedPos
return pp, r.Error return pp, r.Error
} }
// getPostIDsByTag helper function returns the post IDs that either whitelist,
// or blacklist, a set of tags.
func getPostIDsByTag(query string, value []string) []int {
var tags []TaggedPost
var result []int
if len(value) == 0 {
return result
}
DB.Where(query, value).Find(&tags)
for _, tag := range tags {
result = append(result, tag.PostID)
}
return result
}
// narrowWhitelistByBlacklist removes IDs in whitelist that appear in blacklist.
func narrowWhitelistByBlacklist(wl []int, bl []int) []int {
// Map the blacklist into a hash map.
var blacklist = map[int]interface{}{}
for _, id := range bl {
blacklist[id] = nil
}
// Limit the whitelist by the blacklist.
var result []int
for _, id := range wl {
if _, ok := blacklist[id]; !ok {
result = append(result, id)
}
}
return result
}
// GetArchive queries the archive view of the blog. // GetArchive queries the archive view of the blog.
// Set private=true to return private posts, false returns public only. // Set private=true to return private posts, false returns public only.
func (m postMan) GetArchive(private bool) ([]*PostArchive, error) { func (m postMan) GetArchive(private bool) ([]*PostArchive, error) {
@ -271,6 +310,32 @@ func (m postMan) CountComments(posts ...Post) (map[int]int, error) {
return result, nil return result, nil
} }
// ParseMultitag parses a tag string to return arrays for IN and NOT IN queries from DB.
//
// Example input: "blog,updates,-photo,-ask"
// Returns: ["blog", "updates"], ["photo", "ask"]
func ParseMultitag(tagline string) (whitelist, blacklist []string, err error) {
words := strings.Split(tagline, ",")
for _, word := range words {
word = strings.TrimSpace(word)
if len(word) == 0 {
continue
}
// Negation
if strings.HasPrefix(word, "-") {
blacklist = append(blacklist, strings.TrimPrefix(word, "-"))
} else {
whitelist = append(whitelist, word)
}
}
if len(whitelist) == 0 && len(blacklist) == 0 {
err = errors.New("parsing error")
}
return
}
// CountComments on the posts in a PagedPosts list. // CountComments on the posts in a PagedPosts list.
func (pp *PagedPosts) CountComments() error { func (pp *PagedPosts) CountComments() error {
counts, err := Posts.CountComments(pp.Posts...) counts, err := Posts.CountComments(pp.Posts...)

View File

@ -8,7 +8,7 @@ import (
type TaggedPost struct { type TaggedPost struct {
ID int `gorm:"primary_key"` ID int `gorm:"primary_key"`
Tag string Tag string
PostID uint // foreign key to Post PostID int // foreign key to Post
} }
// SummarizeTags returns the list of all tags ordered by frequency used. // SummarizeTags returns the list of all tags ordered by frequency used.