My User Notes page

face-detect
Noah Petherbridge 2023-10-29 12:29:11 -07:00
parent c0bff8ee18
commit f9a2d471f5
7 changed files with 337 additions and 0 deletions

View File

@ -4,8 +4,10 @@ import (
"net/http"
"net/url"
"regexp"
"strconv"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/middleware"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/session"
@ -159,3 +161,131 @@ func UserNotes() http.HandlerFunc {
}
})
}
// My user notes page (/notes/me)
func MyNotes() http.HandlerFunc {
tmpl := templates.Must("account/my_user_notes.html")
// Whitelist for ordering options.
var sortWhitelist = []string{
"updated_at desc",
"updated_at asc",
"username desc",
"username asc",
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Filter parameters.
var (
search = r.FormValue("search")
sort = r.FormValue("sort")
sortOK bool
)
// Sort options.
for _, v := range sortWhitelist {
if sort == v {
sortOK = true
break
}
}
if !sortOK {
sort = sortWhitelist[0]
}
// Get the current user.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "You must be signed in to view this page.")
templates.Redirect(w, "/login?next="+url.QueryEscape(r.URL.String()))
return
}
// Is the site under a Maintenance Mode restriction?
if middleware.MaintenanceMode(currentUser, w, r) {
return
}
// Are we deleting a note?
if r.Method == http.MethodPost {
var (
intent = r.PostFormValue("intent")
idStr = r.PostFormValue("id")
)
noteID, err := strconv.Atoi(idStr)
if err != nil {
session.FlashError(w, r, "Invalid note ID.")
templates.Redirect(w, r.URL.Path)
return
}
note, err := models.GetNote(uint64(noteID))
if err != nil {
session.FlashError(w, r, "Couldn't find that note.")
templates.Redirect(w, r.URL.Path)
return
}
// Assert it is our note to edit.
if note.UserID != currentUser.ID {
session.FlashError(w, r, "That is not your note to edit.")
templates.Redirect(w, r.URL.Path)
return
}
if intent == "delete" {
// Delete it!
if err := note.Delete(); err != nil {
session.FlashError(w, r, "Error deleting the note: %s.", err)
templates.Redirect(w, r.URL.Path)
return
}
session.Flash(w, r, "That note has been deleted!")
}
templates.Redirect(w, r.URL.Path)
return
}
var (
pager = &models.Pagination{
Page: 1,
PerPage: config.PageSizeAdminUserNotes,
Sort: sort,
}
userIDs = []uint64{}
)
pager.ParsePage(r)
notes, err := models.PaginateMyUserNotes(currentUser, search, pager)
if err != nil {
session.FlashError(w, r, "Error getting your user notes: %s", err)
templates.Redirect(w, "/")
return
}
// Map user IDs to users.
for _, note := range notes {
userIDs = append(userIDs, note.AboutUserID)
}
userMap, err := models.MapUsers(currentUser, userIDs)
if err != nil {
log.Error("MyUserNotes: couldn't MapUsers: %s", err)
}
vars := map[string]interface{}{
"Notes": notes,
"Pager": pager,
"UserMap": userMap,
// Search filters
"Search": search,
"Sort": sort,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}

View File

@ -36,6 +36,13 @@ func GetNoteBetweenUsers(currentUser *User, user *User) *UserNote {
return note
}
// GetNote finds a user note by its ID.
func GetNote(id uint64) (*UserNote, error) {
p := &UserNote{}
result := DB.First(&p, id)
return p, result.Error
}
// CountNotesAboutUser returns the number of notes (the current user) has about the other user.
//
// For regular user, will return zero or one; for admins, will return the total count of notes
@ -93,6 +100,40 @@ func PaginateUserNotes(user *User, pager *Pagination) ([]*UserNote, error) {
return notes, result.Error
}
// PaginateMyUserNotes shows all notes written by the current user about others.
func PaginateMyUserNotes(currentUser *User, search string, pager *Pagination) ([]*UserNote, error) {
var (
notes = []*UserNote{}
wheres = []string{}
placeholders = []interface{}{}
ilike = "%" + search + "%"
)
wheres = append(wheres, "user_notes.user_id = ?")
placeholders = append(placeholders, currentUser.ID)
// Searching?
if search != "" {
wheres = append(wheres, "(users.username ILIKE ? OR user_notes.message ILIKE ?)")
placeholders = append(placeholders, ilike, ilike)
}
query := DB.Joins("JOIN users ON users.id = user_notes.about_user_id").Where(
strings.Join(wheres, " AND "),
placeholders...,
).Order(
pager.Sort,
)
query.Model(&UserNote{}).Count(&pager.Total)
result := query.Offset(
pager.GetOffset(),
).Limit(pager.PerPage).Find(&notes)
return notes, result.Error
}
// Save the note.
func (p *UserNote) Save() error {
if p.ID == 0 {

View File

@ -59,6 +59,7 @@ func New() http.Handler {
mux.Handle("/photo/private", middleware.LoginRequired(photo.Private()))
mux.Handle("/photo/private/share", middleware.LoginRequired(photo.Share()))
mux.Handle("/notes/u/", middleware.LoginRequired(account.UserNotes()))
mux.Handle("/notes/me", middleware.LoginRequired(account.MyNotes()))
mux.Handle("/messages", middleware.LoginRequired(inbox.Inbox()))
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))

View File

@ -221,6 +221,12 @@
Blocked Users
</a>
</li>
<li>
<a href="/notes/me">
<span class="icon"><i class="fa fa-pen-to-square mr-1"></i></span>
My User Notes
</a>
</li>
<li>
<a href="/logout">
<span class="icon"><i class="fa fa-arrow-right-from-bracket"></i></span>

View File

@ -0,0 +1,144 @@
{{define "title"}}
My User Notes
{{end}}
{{define "content"}}
<div class="container">
<section class="hero is-info is-bold">
<div class="hero-body">
<div class="container">
<div class="level">
<div class="level-left">
<h1 class="title">
<span class="icon mr-4"><i class="fa fa-pen-square"></i></span>
<span>{{template "title" .}}</span>
</h1>
</div>
</div>
</div>
</div>
</section>
{{$Root := .}}
<div class="block p-4">
<div class="block">
You have saved <strong>{{.Pager.Total}}</strong> note{{Pluralize64 .Pager.Total}} about other members on {{PrettyTitle}} (page {{.Pager.Page}} of {{.Pager.Pages}}).
</div>
<!-- Filters -->
<div class="block">
<form action="{{.Request.URL.Path}}" method="GET">
<div class="card nonshy-collapsible-mobile">
<header class="card-header has-background-link-light">
<p class="card-header-title">
Search &amp; Sort
</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 is-multiline mb-0">
<div class="column">
<div class="field">
<label class="label" for="search">Search by username or content:</label>
<input type="text" class="input" name="search" id="search" placeholder="Search" value="{{.Search}}">
</div>
</div>
<div class="column">
<div class="field">
<label class="label" for="sort">Sort by:</label>
<div class="select is-fullwidth">
<select id="sort" name="sort">
<option value="updated_at desc"{{if eq .Sort "updated_at desc"}} selected{{end}}>Recently updated</option>
<option value="updated_at asc"{{if eq .Sort "updated_at asc"}} selected{{end}}>Oldest first</option>
<option value="username asc"{{if eq .Sort "username asc"}} selected{{end}}>Username (a-z)</option>
<option value="username desc"{{if eq .Sort "username desc"}} selected{{end}}>Username (z-a)</option>
</select>
</div>
</div>
</div>
<div class="column is-narrow has-text-centered">
<label class="label">&nbsp;</label><!-- Spacing :( -->
<a href="{{.Request.URL.Path}}" class="button">Reset</a>
<button type="submit" class="button is-success">
Apply Filters
</button>
</div>
</div>
</div>
</div>
</form>
</div>
{{SimplePager .Pager}}
{{range .Notes}}
<div class="card has-background-link-light mb-4">
{{$User := $Root.UserMap.Get .AboutUserID}}
<div class="card-content" style="position: relative">
<div class="columns">
<div class="column is-2 has-text-centered">
<div>
<a href="/u/{{$User.Username}}">
{{template "avatar-96x96" $User}}
</a>
</div>
<a href="/u/{{$User.Username}}">{{$User.Username}}</a>
{{if $User.IsAdmin}}
<div class="is-size-7 mt-1">
<span class="tag is-danger is-light">
<span class="icon"><i class="fa fa-peace"></i></span>
<span>Admin</span>
</span>
</div>
{{end}}
</div>
<div class="column">
<strong>About:</strong>
<a href="/u/{{$User.Username}}">{{$User.Username}}</a>
{{if $User.IsAdmin}}
<span class="tag ml-2 is-danger is-light">
<i class="fa fa-peace mr-1"></i> Admin
</span>
{{end}}
<div class="my-2" style="white-space: pre-wrap; word-break: break-word; overflow: auto">{{.Message}}</div>
<div class="mt-3">
<form method="POST" action="{{$Root.Request.URL.Path}}">
{{InputCSRF}}
<input type="hidden" name="intent" value="delete">
<input type="hidden" name="id" value="{{.ID}}">
<em class="has-text-grey mr-3">
Last updated <span title="{{.UpdatedAt}}">{{SincePrettyCoarse .UpdatedAt}} ago.</span>
</em>
<!-- Delete button -->
<button type="submit" class="button is-small is-outlined is-danger"
onclick="return confirm('Do you want to delete this note?')">
<i class="fa fa-trash mr-1"></i>
Delete
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{{end}}
</div>
</div>
{{end}}

View File

@ -120,6 +120,15 @@
</p>
</a>
</li>
<li>
<a href="/notes/me">
<strong><i class="fa fa-pen-to-square mr-1"></i> My User Notes</strong>
<p class="help">
Browse and search private notes you have written about others.
</p>
</a>
</li>
</ul>
</div>
</div>

View File

@ -84,6 +84,12 @@
</p>
</div>
<div class="block">
<a href="/notes/me">
<i class="fa fa-search mr-1"></i>
Browse and search <strong>all</strong> my notes</a> <span class="tag is-success ml-2">NEW!</span>
</div>
<div class="columns">
<!-- User column -->
<div class="column">