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")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 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.
|
||||
user, err := models.FindUser(username)
|
||||
|
@ -108,7 +111,7 @@ func UserNotes() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
feedback = fb
|
||||
|
@ -141,6 +144,7 @@ func UserNotes() http.HandlerFunc {
|
|||
"MyNote": myNote,
|
||||
|
||||
// Admin concerns.
|
||||
"Show": show,
|
||||
"Feedback": feedback,
|
||||
"FeedbackPager": fbPager,
|
||||
"OtherNotes": otherNotes,
|
||||
|
|
|
@ -69,6 +69,15 @@ func Feedback() http.HandlerFunc {
|
|||
|
||||
// Are we visiting a linked resource (via TableID)?
|
||||
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 {
|
||||
case "users":
|
||||
user, err := models.GetUser(fb.TableID)
|
||||
|
@ -81,15 +90,29 @@ func Feedback() http.HandlerFunc {
|
|||
case "photos":
|
||||
pic, err := models.GetPhoto(fb.TableID)
|
||||
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)
|
||||
} else {
|
||||
// Going to the user's profile page?
|
||||
if profile {
|
||||
user, err := models.GetUser(pic.UserID)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't visit user %d: %s", fb.TableID, err)
|
||||
|
||||
// Going forward: the aboutUser will be populated, this is for legacy reports.
|
||||
if aboutUser == nil {
|
||||
if user, err := models.GetUser(pic.UserID); err == nil {
|
||||
aboutUser = user
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,9 @@ func Report() http.HandlerFunc {
|
|||
|
||||
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.
|
||||
fb := &models.Feedback{
|
||||
Intent: "report",
|
||||
|
@ -87,7 +90,7 @@ func Report() http.HandlerFunc {
|
|||
"A message was reported on the chat room!\n\n"+
|
||||
"* From username: [%s](/u/%s)\n"+
|
||||
"* About username: [%s](/u/%s)\n"+
|
||||
"* Channel: **%s**\n"+
|
||||
"* Channel: [**%s**](/u/%s)\n"+
|
||||
"* Timestamp: %s\n"+
|
||||
"* Classification: %s\n"+
|
||||
"* User comment: %s\n\n"+
|
||||
|
@ -95,7 +98,7 @@ func Report() http.HandlerFunc {
|
|||
"The reported message on chat was:\n\n%s",
|
||||
report.FromUsername, report.FromUsername,
|
||||
report.AboutUsername, report.AboutUsername,
|
||||
report.Channel,
|
||||
report.Channel, otherUsername,
|
||||
report.Timestamp,
|
||||
report.Reason,
|
||||
report.Comment,
|
||||
|
@ -116,6 +119,7 @@ func Report() http.HandlerFunc {
|
|||
if err == nil {
|
||||
fb.TableName = "users"
|
||||
fb.TableID = targetUser.ID
|
||||
fb.AboutUserID = targetUser.ID
|
||||
} else {
|
||||
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
|
||||
tableName string
|
||||
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
|
||||
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"
|
||||
if user, err := models.GetUser(uint64(tableID)); err == nil {
|
||||
tableLabel = fmt.Sprintf(`User account "%s"`, user.Username)
|
||||
aboutUser = user
|
||||
} else {
|
||||
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 user, err := models.GetUser(pic.UserID); err == nil {
|
||||
tableLabel = fmt.Sprintf(`A profile photo of user account "%s"`, user.Username)
|
||||
aboutUser = user
|
||||
} else {
|
||||
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]"
|
||||
if sender, err := models.GetUser(msg.SourceUserID); err == nil {
|
||||
username = sender.Username
|
||||
aboutUser = sender
|
||||
}
|
||||
|
||||
footer = fmt.Sprintf(`
|
||||
|
@ -100,6 +104,7 @@ From: <a href="/u/%s">@%s</a>
|
|||
// Find this comment.
|
||||
if comment, err := models.GetComment(uint64(tableID)); err == nil {
|
||||
tableLabel = fmt.Sprintf(`A comment written by "%s"`, comment.User.Username)
|
||||
aboutUser = &comment.User
|
||||
} else {
|
||||
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),
|
||||
}
|
||||
|
||||
if aboutUser != nil {
|
||||
fb.AboutUserID = aboutUser.ID
|
||||
}
|
||||
|
||||
if currentUser != nil && currentUser.ID > 0 {
|
||||
fb.UserID = currentUser.ID
|
||||
} else if replyTo != "" {
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
type Feedback struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
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
|
||||
Intent 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
|
||||
// of their current photo IDs. Additionally, it will look for chat room reports which were about their
|
||||
// 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 (
|
||||
fb = []*Feedback{}
|
||||
photoIDs, _ = user.AllPhotoIDs()
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
like = "%" + user.Username + "%"
|
||||
)
|
||||
|
||||
// How to apply the search filters?
|
||||
switch show {
|
||||
case "about":
|
||||
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 = 'photos' AND table_id IN ?) OR
|
||||
message LIKE ?
|
||||
`)
|
||||
placeholders = append(placeholders, user.ID, photoIDs, user.Username)
|
||||
placeholders = append(placeholders, user.ID, user.ID, user.ID, photoIDs, like)
|
||||
}
|
||||
|
||||
query := DB.Where(
|
||||
strings.Join(wheres, " AND "),
|
||||
|
|
|
@ -187,11 +187,38 @@
|
|||
|
||||
<div class="card-content">
|
||||
{{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}}).
|
||||
</span>
|
||||
</div>
|
||||
{{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">
|
||||
{{SimplePager .FeedbackPager}}
|
||||
</div>
|
||||
|
@ -224,29 +251,29 @@
|
|||
{{if ne .TableID 0}} - {{.TableID}}{{end}}
|
||||
{{else if eq .TableName "users"}}
|
||||
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"
|
||||
target="_blank"
|
||||
title="Visit the reported user's profile"></a>
|
||||
{{else if eq .TableName "photos"}}
|
||||
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"
|
||||
target="_blank"
|
||||
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"
|
||||
target="_blank"
|
||||
title="Visit the user profile who owns the reported photo"></a>
|
||||
{{else if eq .TableName "messages"}}
|
||||
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"
|
||||
target="_blank"
|
||||
title="Impersonate the reporter and view this message thread"></a>
|
||||
{{else}}
|
||||
{{.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}}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
Loading…
Reference in New Issue
Block a user