Search and filter admin feedback & reports

This commit is contained in:
Noah Petherbridge 2024-09-07 14:50:11 -07:00
parent 2c7532434a
commit 276eddfd8e
3 changed files with 153 additions and 10 deletions

View File

@ -14,6 +14,13 @@ import (
// Feedback controller (/admin/feedback) // Feedback controller (/admin/feedback)
func Feedback() http.HandlerFunc { func Feedback() http.HandlerFunc {
tmpl := templates.Must("admin/feedback.html") tmpl := templates.Must("admin/feedback.html")
// Whitelist for ordering options.
var sortWhitelist = []string{
"created_at desc",
"created_at asc",
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Query params. // Query params.
var ( var (
@ -23,8 +30,26 @@ func Feedback() http.HandlerFunc {
profile = r.FormValue("profile") == "true" // visit associated user profile profile = r.FormValue("profile") == "true" // visit associated user profile
verdict = r.FormValue("verdict") verdict = r.FormValue("verdict")
fb *models.Feedback fb *models.Feedback
// Search filters.
searchQuery = r.FormValue("q")
search = models.ParseSearchString(searchQuery)
subject = r.FormValue("subject")
sort = r.FormValue("sort")
sortOK bool
) )
// Sort options.
for _, v := range sortWhitelist {
if sort == v {
sortOK = true
break
}
}
if !sortOK {
sort = sortWhitelist[0]
}
currentUser, err := session.CurrentUser(r) currentUser, err := session.CurrentUser(r)
if err != nil { if err != nil {
session.FlashError(w, r, "Couldn't get your current user: %s", err) session.FlashError(w, r, "Couldn't get your current user: %s", err)
@ -149,10 +174,10 @@ func Feedback() http.HandlerFunc {
pager := &models.Pagination{ pager := &models.Pagination{
Page: 1, Page: 1,
PerPage: config.PageSizeAdminFeedback, PerPage: config.PageSizeAdminFeedback,
Sort: "updated_at desc", Sort: sort,
} }
pager.ParsePage(r) pager.ParsePage(r)
page, err := models.PaginateFeedback(acknowledged, intent, pager) page, err := models.PaginateFeedback(acknowledged, intent, subject, search, pager)
if err != nil { if err != nil {
session.FlashError(w, r, "Couldn't load feedback from DB: %s", err) session.FlashError(w, r, "Couldn't load feedback from DB: %s", err)
} }
@ -170,6 +195,12 @@ func Feedback() http.HandlerFunc {
} }
var vars = map[string]interface{}{ var vars = map[string]interface{}{
// Filter settings.
"DistinctSubjects": models.DistinctFeedbackSubjects(),
"SearchTerm": searchQuery,
"Subject": subject,
"Sort": sort,
"Intent": intent, "Intent": intent,
"Acknowledged": acknowledged, "Acknowledged": acknowledged,
"Feedback": page, "Feedback": page,

View File

@ -1,6 +1,7 @@
package models package models
import ( import (
"sort"
"strings" "strings"
"time" "time"
@ -45,7 +46,7 @@ func CountUnreadFeedback() int64 {
} }
// PaginateFeedback // PaginateFeedback
func PaginateFeedback(acknowledged bool, intent string, pager *Pagination) ([]*Feedback, error) { func PaginateFeedback(acknowledged bool, intent, subject string, search *Search, pager *Pagination) ([]*Feedback, error) {
var ( var (
fb = []*Feedback{} fb = []*Feedback{}
wheres = []string{} wheres = []string{}
@ -60,6 +61,23 @@ func PaginateFeedback(acknowledged bool, intent string, pager *Pagination) ([]*F
placeholders = append(placeholders, intent) placeholders = append(placeholders, intent)
} }
if subject != "" {
wheres = append(wheres, "subject = ?")
placeholders = append(placeholders, subject)
}
// Search terms.
for _, term := range search.Includes {
var ilike = "%" + strings.ToLower(term) + "%"
wheres = append(wheres, "message ILIKE ?")
placeholders = append(placeholders, ilike)
}
for _, term := range search.Excludes {
var ilike = "%" + strings.ToLower(term) + "%"
wheres = append(wheres, "message NOT ILIKE ?")
placeholders = append(placeholders, ilike)
}
query := DB.Where( query := DB.Where(
strings.Join(wheres, " AND "), strings.Join(wheres, " AND "),
placeholders..., placeholders...,
@ -91,9 +109,10 @@ func PaginateFeedbackAboutUser(user *User, pager *Pagination) ([]*Feedback, erro
wheres = append(wheres, ` wheres = append(wheres, `
(table_name = 'users' AND table_id = ?) OR (table_name = 'users' AND table_id = ?) OR
(table_name = 'photos' AND table_id IN ?) (table_name = 'photos' AND table_id IN ?) OR
message LIKE ?
`) `)
placeholders = append(placeholders, user.ID, photoIDs) placeholders = append(placeholders, user.ID, photoIDs, user.Username)
query := DB.Where( query := DB.Where(
strings.Join(wheres, " AND "), strings.Join(wheres, " AND "),
@ -111,6 +130,22 @@ func PaginateFeedbackAboutUser(user *User, pager *Pagination) ([]*Feedback, erro
return fb, result.Error return fb, result.Error
} }
// DistinctFeedbackSubjects returns the distinct subjects on feedback & reports.
func DistinctFeedbackSubjects() []string {
var results = []string{}
query := DB.Model(&Feedback{}).
Select("DISTINCT feedbacks.subject").
Group("feedbacks.subject").
Find(&results)
if query.Error != nil {
log.Error("DistinctFeedbackSubjects: %s", query.Error)
return nil
}
sort.Strings(results)
return results
}
// CreateFeedback saves a new Feedback row to the DB. // CreateFeedback saves a new Feedback row to the DB.
func CreateFeedback(fb *Feedback) error { func CreateFeedback(fb *Feedback) error {
result := DB.Create(fb) result := DB.Create(fb)

View File

@ -23,13 +23,13 @@
<div class="tabs is-toggle"> <div class="tabs is-toggle">
<ul> <ul>
<li{{if eq .Intent ""}} class="is-active"{{end}}> <li{{if eq .Intent ""}} class="is-active"{{end}}>
<a href="{{.Request.URL.Path}}?acknowledged={{.Acknowledged}}">All</a> <a href="{{.Request.URL.Path}}?{{QueryPlus "intent" ""}}">All</a>
</li> </li>
<li{{if eq .Intent "contact"}} class="is-active"{{end}}> <li{{if eq .Intent "contact"}} class="is-active"{{end}}>
<a href="{{.Request.URL.Path}}?acknowledged={{.Acknowledged}}&intent=contact">Contact</a> <a href="{{.Request.URL.Path}}?{{QueryPlus "intent" "contact"}}">Contact</a>
</li> </li>
<li{{if eq .Intent "report"}} class="is-active"{{end}}> <li{{if eq .Intent "report"}} class="is-active"{{end}}>
<a href="{{.Request.URL.Path}}?acknowledged={{.Acknowledged}}&intent=report">Reports</a> <a href="{{.Request.URL.Path}}?{{QueryPlus "intent" "report"}}">Reports</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -38,16 +38,93 @@
<div class="tabs is-toggle"> <div class="tabs is-toggle">
<ul> <ul>
<li{{if not .Acknowledged}} class="is-active"{{end}}> <li{{if not .Acknowledged}} class="is-active"{{end}}>
<a href="{{.Request.URL.Path}}?intent={{.Intent}}">Unread</a> <a href="{{.Request.URL.Path}}?{{QueryPlus "acknowledged" "false"}}">Unread</a>
</li> </li>
<li{{if .Acknowledged}} class="is-active"{{end}}> <li{{if .Acknowledged}} class="is-active"{{end}}>
<a href="{{.Request.URL.Path}}?acknowledged=true&intent={{.Intent}}">Acknowledged</a> <a href="{{.Request.URL.Path}}?{{QueryPlus "acknowledged" "true"}}">Acknowledged</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<!-- Search fields -->
<div class="mb-4">
<form action="{{.Request.URL.Path}}" method="GET">
<input type="hidden" name="intent" value="{{.Intent}}">
<input type="hidden" name="acknowledged" value="{{.Acknowledged}}">
<div class="card nonshy-collapsible-mobile">
<header class="card-header has-background-link-light">
<p class="card-header-title has-text-dark">
Search Filters
</p>
<button class="card-header-icon" type="button">
<span class="icon">
<i class="fa fa-angle-up"></i>
</span>
</button>
</header>
<div class="card-content">
<div class="columns">
<div class="column pr-1">
<div class="field">
<label class="label" for="q">Search terms:</label>
<input type="text" class="input"
name="q" id="q"
autocomplete="off"
value="{{.SearchTerm}}">
<p class="help">
Tip: you can <span class="has-text-success">"quote exact phrases"</span> and
<span class="has-text-success">-exclude</span> words (or
<span class="has-text-success">-"exclude phrases"</span>) from your search.
</p>
</div>
</div>
<div class="column px-1">
<div class="field">
<label class="label" for="subject">Subject:</label>
<div class="select is-fullwidth">
<select id="subject" name="subject">
<option value=""></option>
{{range .DistinctSubjects}}
<option value="{{.}}" {{if eq $Root.Subject .}}selected{{end}}>{{.}}</option>
{{end}}
</select>
</div>
</div>
</div>
<div class="column px-1">
<div class="field">
<label class="label" for="sort">Sort by:</label>
<div class="select is-fullwidth">
<select id="sort" name="sort">
<option value="created_at desc"{{if eq .Sort "created_at desc"}} selected{{end}}>Newest</option>
<option value="created_at asc"{{if eq .Sort "created_at asc"}} selected{{end}}>Oldest</option>
</select>
</div>
</div>
</div>
<div class="column is-narrow pl-1 has-text-right">
<label class="label">&nbsp;</label>
<a href="{{.Request.URL.Path}}" class="button">Reset</a>
<button type="submit" class="button is-success">
<span>Search</span>
<span class="icon"><i class="fa fa-search"></i></span>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
{{SimplePager .Pager}} {{SimplePager .Pager}}
<div class="columns is-multiline"> <div class="columns is-multiline">