Search and filter admin feedback & reports
This commit is contained in:
parent
2c7532434a
commit
276eddfd8e
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"> </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">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user