Improvements to Feedback & Reports
* Add an AboutUserID field to feedbacks, so when the report is about a picture that is later deleted, the feedback can still link to the original owner's account instead of showing an error. * Add filters to the User Notes page so the admin can see: * All feedback From or About the user or their content (default) * Feedback created by the user * Feedback about the user or their content * Fuzzy search for any feedback containing the user's name. * On chat room reports: make the @channel ID a clickable user profile link for convenience.
This commit is contained in:
parent
704124157d
commit
e146c09850
|
@ -18,7 +18,10 @@ func UserNotes() http.HandlerFunc {
|
||||||
tmpl := templates.Must("account/user_notes.html")
|
tmpl := templates.Must("account/user_notes.html")
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Parse the username out of the URL parameters.
|
// Parse the username out of the URL parameters.
|
||||||
var username = r.PathValue("username")
|
var (
|
||||||
|
username = r.PathValue("username")
|
||||||
|
show = r.FormValue("show") // admin feedback filter
|
||||||
|
)
|
||||||
|
|
||||||
// Find this user.
|
// Find this user.
|
||||||
user, err := models.FindUser(username)
|
user, err := models.FindUser(username)
|
||||||
|
@ -108,7 +111,7 @@ func UserNotes() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paginate feedback & reports.
|
// Paginate feedback & reports.
|
||||||
if fb, err := models.PaginateFeedbackAboutUser(user, fbPager); err != nil {
|
if fb, err := models.PaginateFeedbackAboutUser(user, show, fbPager); err != nil {
|
||||||
session.FlashError(w, r, "Paginating feedback on this user: %s", err)
|
session.FlashError(w, r, "Paginating feedback on this user: %s", err)
|
||||||
} else {
|
} else {
|
||||||
feedback = fb
|
feedback = fb
|
||||||
|
@ -141,6 +144,7 @@ func UserNotes() http.HandlerFunc {
|
||||||
"MyNote": myNote,
|
"MyNote": myNote,
|
||||||
|
|
||||||
// Admin concerns.
|
// Admin concerns.
|
||||||
|
"Show": show,
|
||||||
"Feedback": feedback,
|
"Feedback": feedback,
|
||||||
"FeedbackPager": fbPager,
|
"FeedbackPager": fbPager,
|
||||||
"OtherNotes": otherNotes,
|
"OtherNotes": otherNotes,
|
||||||
|
|
|
@ -69,6 +69,15 @@ func Feedback() http.HandlerFunc {
|
||||||
|
|
||||||
// Are we visiting a linked resource (via TableID)?
|
// Are we visiting a linked resource (via TableID)?
|
||||||
if fb != nil && fb.TableID > 0 && visit {
|
if fb != nil && fb.TableID > 0 && visit {
|
||||||
|
// New (Oct 17 '24): feedbacks may carry an AboutUserID, e.g. for photos in case the reported
|
||||||
|
// photo is removed then the associated owner of the photo is still carried in the report.
|
||||||
|
var aboutUser *models.User
|
||||||
|
if fb.AboutUserID > 0 {
|
||||||
|
if user, err := models.GetUser(fb.AboutUserID); err == nil {
|
||||||
|
aboutUser = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch fb.TableName {
|
switch fb.TableName {
|
||||||
case "users":
|
case "users":
|
||||||
user, err := models.GetUser(fb.TableID)
|
user, err := models.GetUser(fb.TableID)
|
||||||
|
@ -81,15 +90,29 @@ func Feedback() http.HandlerFunc {
|
||||||
case "photos":
|
case "photos":
|
||||||
pic, err := models.GetPhoto(fb.TableID)
|
pic, err := models.GetPhoto(fb.TableID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// If there was an About User, visit their profile page instead.
|
||||||
|
if aboutUser != nil {
|
||||||
|
session.FlashError(w, r, "The photo #%d was deleted, visiting the owner's profile page instead.", fb.TableID)
|
||||||
|
templates.Redirect(w, "/u/"+aboutUser.Username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
session.FlashError(w, r, "Couldn't get photo %d: %s", fb.TableID, err)
|
session.FlashError(w, r, "Couldn't get photo %d: %s", fb.TableID, err)
|
||||||
} else {
|
} else {
|
||||||
// Going to the user's profile page?
|
// Going to the user's profile page?
|
||||||
if profile {
|
if profile {
|
||||||
user, err := models.GetUser(pic.UserID)
|
|
||||||
if err != nil {
|
// Going forward: the aboutUser will be populated, this is for legacy reports.
|
||||||
session.FlashError(w, r, "Couldn't visit user %d: %s", fb.TableID, err)
|
if aboutUser == nil {
|
||||||
|
if user, err := models.GetUser(pic.UserID); err == nil {
|
||||||
|
aboutUser = user
|
||||||
} else {
|
} else {
|
||||||
templates.Redirect(w, "/u/"+user.Username)
|
session.FlashError(w, r, "Couldn't visit user %d: %s", fb.TableID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if aboutUser != nil {
|
||||||
|
templates.Redirect(w, "/u/"+aboutUser.Username)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,9 @@ func Report() http.HandlerFunc {
|
||||||
|
|
||||||
log.Debug("Got chat report: %+v", report)
|
log.Debug("Got chat report: %+v", report)
|
||||||
|
|
||||||
|
// Make a clickable profile link for the channel ID (other user).
|
||||||
|
otherUsername := strings.TrimPrefix(report.Channel, "@")
|
||||||
|
|
||||||
// Create an admin Feedback model.
|
// Create an admin Feedback model.
|
||||||
fb := &models.Feedback{
|
fb := &models.Feedback{
|
||||||
Intent: "report",
|
Intent: "report",
|
||||||
|
@ -87,7 +90,7 @@ func Report() http.HandlerFunc {
|
||||||
"A message was reported on the chat room!\n\n"+
|
"A message was reported on the chat room!\n\n"+
|
||||||
"* From username: [%s](/u/%s)\n"+
|
"* From username: [%s](/u/%s)\n"+
|
||||||
"* About username: [%s](/u/%s)\n"+
|
"* About username: [%s](/u/%s)\n"+
|
||||||
"* Channel: **%s**\n"+
|
"* Channel: [**%s**](/u/%s)\n"+
|
||||||
"* Timestamp: %s\n"+
|
"* Timestamp: %s\n"+
|
||||||
"* Classification: %s\n"+
|
"* Classification: %s\n"+
|
||||||
"* User comment: %s\n\n"+
|
"* User comment: %s\n\n"+
|
||||||
|
@ -95,7 +98,7 @@ func Report() http.HandlerFunc {
|
||||||
"The reported message on chat was:\n\n%s",
|
"The reported message on chat was:\n\n%s",
|
||||||
report.FromUsername, report.FromUsername,
|
report.FromUsername, report.FromUsername,
|
||||||
report.AboutUsername, report.AboutUsername,
|
report.AboutUsername, report.AboutUsername,
|
||||||
report.Channel,
|
report.Channel, otherUsername,
|
||||||
report.Timestamp,
|
report.Timestamp,
|
||||||
report.Reason,
|
report.Reason,
|
||||||
report.Comment,
|
report.Comment,
|
||||||
|
@ -116,6 +119,7 @@ func Report() http.HandlerFunc {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fb.TableName = "users"
|
fb.TableName = "users"
|
||||||
fb.TableID = targetUser.ID
|
fb.TableID = targetUser.ID
|
||||||
|
fb.AboutUserID = targetUser.ID
|
||||||
} else {
|
} else {
|
||||||
log.Error("BareRTC Chat Feedback: couldn't find user ID for AboutUsername=%s: %s", report.AboutUsername, err)
|
log.Error("BareRTC Chat Feedback: couldn't find user ID for AboutUsername=%s: %s", report.AboutUsername, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ func Contact() http.HandlerFunc {
|
||||||
tableID int
|
tableID int
|
||||||
tableName string
|
tableName string
|
||||||
tableLabel string // front-end user feedback about selected report item
|
tableLabel string // front-end user feedback about selected report item
|
||||||
|
aboutUser *models.User // associated user (e.g. owner of reported photo)
|
||||||
messageRequired = true // unless we have a table ID to work with
|
messageRequired = true // unless we have a table ID to work with
|
||||||
success = "Thank you for your feedback! Your message has been delivered to the website administrators."
|
success = "Thank you for your feedback! Your message has been delivered to the website administrators."
|
||||||
)
|
)
|
||||||
|
@ -56,6 +57,7 @@ func Contact() http.HandlerFunc {
|
||||||
tableName = "users"
|
tableName = "users"
|
||||||
if user, err := models.GetUser(uint64(tableID)); err == nil {
|
if user, err := models.GetUser(uint64(tableID)); err == nil {
|
||||||
tableLabel = fmt.Sprintf(`User account "%s"`, user.Username)
|
tableLabel = fmt.Sprintf(`User account "%s"`, user.Username)
|
||||||
|
aboutUser = user
|
||||||
} else {
|
} else {
|
||||||
log.Error("/contact: couldn't produce table label for user %d: %s", tableID, err)
|
log.Error("/contact: couldn't produce table label for user %d: %s", tableID, err)
|
||||||
}
|
}
|
||||||
|
@ -66,6 +68,7 @@ func Contact() http.HandlerFunc {
|
||||||
if pic, err := models.GetPhoto(uint64(tableID)); err == nil {
|
if pic, err := models.GetPhoto(uint64(tableID)); err == nil {
|
||||||
if user, err := models.GetUser(pic.UserID); err == nil {
|
if user, err := models.GetUser(pic.UserID); err == nil {
|
||||||
tableLabel = fmt.Sprintf(`A profile photo of user account "%s"`, user.Username)
|
tableLabel = fmt.Sprintf(`A profile photo of user account "%s"`, user.Username)
|
||||||
|
aboutUser = user
|
||||||
} else {
|
} else {
|
||||||
log.Error("/contact: couldn't produce table label for user %d: %s", tableID, err)
|
log.Error("/contact: couldn't produce table label for user %d: %s", tableID, err)
|
||||||
}
|
}
|
||||||
|
@ -81,6 +84,7 @@ func Contact() http.HandlerFunc {
|
||||||
var username = "[unavailable]"
|
var username = "[unavailable]"
|
||||||
if sender, err := models.GetUser(msg.SourceUserID); err == nil {
|
if sender, err := models.GetUser(msg.SourceUserID); err == nil {
|
||||||
username = sender.Username
|
username = sender.Username
|
||||||
|
aboutUser = sender
|
||||||
}
|
}
|
||||||
|
|
||||||
footer = fmt.Sprintf(`
|
footer = fmt.Sprintf(`
|
||||||
|
@ -100,6 +104,7 @@ From: <a href="/u/%s">@%s</a>
|
||||||
// Find this comment.
|
// Find this comment.
|
||||||
if comment, err := models.GetComment(uint64(tableID)); err == nil {
|
if comment, err := models.GetComment(uint64(tableID)); err == nil {
|
||||||
tableLabel = fmt.Sprintf(`A comment written by "%s"`, comment.User.Username)
|
tableLabel = fmt.Sprintf(`A comment written by "%s"`, comment.User.Username)
|
||||||
|
aboutUser = &comment.User
|
||||||
} else {
|
} else {
|
||||||
log.Error("/contact: couldn't produce table label for comment %d: %s", tableID, err)
|
log.Error("/contact: couldn't produce table label for comment %d: %s", tableID, err)
|
||||||
}
|
}
|
||||||
|
@ -166,6 +171,10 @@ From: <a href="/u/%s">@%s</a>
|
||||||
TableID: uint64(tableID),
|
TableID: uint64(tableID),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if aboutUser != nil {
|
||||||
|
fb.AboutUserID = aboutUser.ID
|
||||||
|
}
|
||||||
|
|
||||||
if currentUser != nil && currentUser.ID > 0 {
|
if currentUser != nil && currentUser.ID > 0 {
|
||||||
fb.UserID = currentUser.ID
|
fb.UserID = currentUser.ID
|
||||||
} else if replyTo != "" {
|
} else if replyTo != "" {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
type Feedback struct {
|
type Feedback struct {
|
||||||
ID uint64 `gorm:"primaryKey"`
|
ID uint64 `gorm:"primaryKey"`
|
||||||
UserID uint64 `gorm:"index"` // if logged-in user posted this
|
UserID uint64 `gorm:"index"` // if logged-in user posted this
|
||||||
|
AboutUserID uint64 // associated 'about' user (e.g., owner of a reported photo)
|
||||||
Acknowledged bool `gorm:"index"` // admin dashboard "read" status
|
Acknowledged bool `gorm:"index"` // admin dashboard "read" status
|
||||||
Intent string
|
Intent string
|
||||||
Subject string
|
Subject string
|
||||||
|
@ -99,20 +100,49 @@ func PaginateFeedback(acknowledged bool, intent, subject string, search *Search,
|
||||||
// It returns reports where table_name=users and their user ID, or where table_name=photos and about any
|
// It returns reports where table_name=users and their user ID, or where table_name=photos and about any
|
||||||
// of their current photo IDs. Additionally, it will look for chat room reports which were about their
|
// of their current photo IDs. Additionally, it will look for chat room reports which were about their
|
||||||
// username.
|
// username.
|
||||||
func PaginateFeedbackAboutUser(user *User, pager *Pagination) ([]*Feedback, error) {
|
//
|
||||||
|
// The 'show' parameter applies some basic filter choices:
|
||||||
|
//
|
||||||
|
// - Blank string (default) = all reports From or About this user
|
||||||
|
// - "about" = all reports About this user (by table_name=users table_id=userID, or table_name=photos
|
||||||
|
// for any of their existing photo IDs)
|
||||||
|
// - "from" = all reports From this user (where reporting user_id is the user's ID)
|
||||||
|
// - "fuzzy" = fuzzy full text search on all reports that contain the user's username.
|
||||||
|
func PaginateFeedbackAboutUser(user *User, show string, pager *Pagination) ([]*Feedback, error) {
|
||||||
var (
|
var (
|
||||||
fb = []*Feedback{}
|
fb = []*Feedback{}
|
||||||
photoIDs, _ = user.AllPhotoIDs()
|
photoIDs, _ = user.AllPhotoIDs()
|
||||||
wheres = []string{}
|
wheres = []string{}
|
||||||
placeholders = []interface{}{}
|
placeholders = []interface{}{}
|
||||||
|
like = "%" + user.Username + "%"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// How to apply the search filters?
|
||||||
|
switch show {
|
||||||
|
case "about":
|
||||||
wheres = append(wheres, `
|
wheres = append(wheres, `
|
||||||
|
about_user_id = ? OR
|
||||||
|
(table_name = 'users' AND table_id = ?) OR
|
||||||
|
(table_name = 'photos' AND table_id IN ?)
|
||||||
|
`)
|
||||||
|
placeholders = append(placeholders, user.ID, user.ID, photoIDs)
|
||||||
|
case "from":
|
||||||
|
wheres = append(wheres, "user_id = ?")
|
||||||
|
placeholders = append(placeholders, user.ID)
|
||||||
|
case "fuzzy":
|
||||||
|
wheres = append(wheres, "message LIKE ?")
|
||||||
|
placeholders = append(placeholders, like)
|
||||||
|
default:
|
||||||
|
// Default=everything.
|
||||||
|
wheres = append(wheres, `
|
||||||
|
user_id = ? OR
|
||||||
|
about_user_id = ? OR
|
||||||
(table_name = 'users' AND table_id = ?) OR
|
(table_name = 'users' AND table_id = ?) OR
|
||||||
(table_name = 'photos' AND table_id IN ?) OR
|
(table_name = 'photos' AND table_id IN ?) OR
|
||||||
message LIKE ?
|
message LIKE ?
|
||||||
`)
|
`)
|
||||||
placeholders = append(placeholders, user.ID, photoIDs, user.Username)
|
placeholders = append(placeholders, user.ID, user.ID, user.ID, photoIDs, like)
|
||||||
|
}
|
||||||
|
|
||||||
query := DB.Where(
|
query := DB.Where(
|
||||||
strings.Join(wheres, " AND "),
|
strings.Join(wheres, " AND "),
|
||||||
|
|
|
@ -187,11 +187,38 @@
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{if .FeedbackPager.Total}}
|
{{if .FeedbackPager.Total}}
|
||||||
<span>
|
<div class="block">
|
||||||
Found <strong>{{.FeedbackPager.Total}}</strong> report{{Pluralize64 .FeedbackPager.Total}} about this user (page {{.FeedbackPager.Page}} of {{.FeedbackPager.Pages}}).
|
Found <strong>{{.FeedbackPager.Total}}</strong> report{{Pluralize64 .FeedbackPager.Total}} about this user (page {{.FeedbackPager.Page}} of {{.FeedbackPager.Pages}}).
|
||||||
</span>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Simple filters -->
|
||||||
|
<form action="{{.Request.URL.Path}}" method="GET">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<label class="label">Show:</label>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select name="show">
|
||||||
|
<optgroup label="By user account">
|
||||||
|
<option value="">All reports from or about this user</option>
|
||||||
|
<option value="about"{{if eq .Show "about"}} selected{{end}}>Reports about this user or their photos</option>
|
||||||
|
<option value="from"{{if eq .Show "from"}} selected{{end}}>Reports from this user about others</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Fuzzy search">
|
||||||
|
<option value="fuzzy"{{if eq .Show "fuzzy"}} selected{{end}}>All reports that contain this user's name (@{{.User.Username}})</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<a href="{{.Request.URL.Path}}" class="button">Reset</a>
|
||||||
|
<button type="submit" class="button is-primary">Apply</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
{{SimplePager .FeedbackPager}}
|
{{SimplePager .FeedbackPager}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -224,29 +251,29 @@
|
||||||
{{if ne .TableID 0}} - {{.TableID}}{{end}}
|
{{if ne .TableID 0}} - {{.TableID}}{{end}}
|
||||||
{{else if eq .TableName "users"}}
|
{{else if eq .TableName "users"}}
|
||||||
Users: {{.TableID}}
|
Users: {{.TableID}}
|
||||||
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true"
|
<a href="/admin/feedback?id={{.ID}}&visit=true"
|
||||||
class="fa fa-external-link ml-2"
|
class="fa fa-external-link ml-2"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Visit the reported user's profile"></a>
|
title="Visit the reported user's profile"></a>
|
||||||
{{else if eq .TableName "photos"}}
|
{{else if eq .TableName "photos"}}
|
||||||
Photos: {{.TableID}}
|
Photos: {{.TableID}}
|
||||||
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true"
|
<a href="/admin/feedback?id={{.ID}}&visit=true"
|
||||||
class="fa fa-external-link mx-2"
|
class="fa fa-external-link mx-2"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Visit the reported photo"></a>
|
title="Visit the reported photo"></a>
|
||||||
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true&profile=true"
|
<a href="/admin/feedback?id={{.ID}}&visit=true&profile=true"
|
||||||
class="fa fa-user"
|
class="fa fa-user"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Visit the user profile who owns the reported photo"></a>
|
title="Visit the user profile who owns the reported photo"></a>
|
||||||
{{else if eq .TableName "messages"}}
|
{{else if eq .TableName "messages"}}
|
||||||
Messages: {{.TableID}}
|
Messages: {{.TableID}}
|
||||||
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true"
|
<a href="/admin/feedback?id={{.ID}}&visit=true"
|
||||||
class="fa fa-ghost ml-2"
|
class="fa fa-ghost ml-2"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Impersonate the reporter and view this message thread"></a>
|
title="Impersonate the reporter and view this message thread"></a>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{.TableName}}: {{.TableID}}
|
{{.TableName}}: {{.TableID}}
|
||||||
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true" class="fa fa-external-link ml-2" target="_blank"></a>
|
<a href="/admin/feedback?id={{.ID}}&visit=true" class="fa fa-external-link ml-2" target="_blank"></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user