My User Notes page
This commit is contained in:
parent
c0bff8ee18
commit
f9a2d471f5
|
@ -4,8 +4,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"code.nonshy.com/nonshy/website/pkg/config"
|
"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/middleware"
|
||||||
"code.nonshy.com/nonshy/website/pkg/models"
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
"code.nonshy.com/nonshy/website/pkg/session"
|
"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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,13 @@ func GetNoteBetweenUsers(currentUser *User, user *User) *UserNote {
|
||||||
return note
|
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.
|
// 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
|
// 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
|
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(¬es)
|
||||||
|
|
||||||
|
return notes, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
// Save the note.
|
// Save the note.
|
||||||
func (p *UserNote) Save() error {
|
func (p *UserNote) Save() error {
|
||||||
if p.ID == 0 {
|
if p.ID == 0 {
|
||||||
|
|
|
@ -59,6 +59,7 @@ func New() http.Handler {
|
||||||
mux.Handle("/photo/private", middleware.LoginRequired(photo.Private()))
|
mux.Handle("/photo/private", middleware.LoginRequired(photo.Private()))
|
||||||
mux.Handle("/photo/private/share", middleware.LoginRequired(photo.Share()))
|
mux.Handle("/photo/private/share", middleware.LoginRequired(photo.Share()))
|
||||||
mux.Handle("/notes/u/", middleware.LoginRequired(account.UserNotes()))
|
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", middleware.LoginRequired(inbox.Inbox()))
|
||||||
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
|
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
|
||||||
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
||||||
|
|
|
@ -221,6 +221,12 @@
|
||||||
Blocked Users
|
Blocked Users
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<a href="/logout">
|
<a href="/logout">
|
||||||
<span class="icon"><i class="fa fa-arrow-right-from-bracket"></i></span>
|
<span class="icon"><i class="fa fa-arrow-right-from-bracket"></i></span>
|
||||||
|
|
144
web/templates/account/my_user_notes.html
Normal file
144
web/templates/account/my_user_notes.html
Normal 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 & 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"> </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}}
|
|
@ -120,6 +120,15 @@
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -84,6 +84,12 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div class="columns">
|
||||||
<!-- User column -->
|
<!-- User column -->
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user