My User Notes page
This commit is contained in:
parent
c0bff8ee18
commit
f9a2d471f5
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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(¬es)
|
||||
|
||||
return notes, result.Error
|
||||
}
|
||||
|
||||
// Save the note.
|
||||
func (p *UserNote) Save() error {
|
||||
if p.ID == 0 {
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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>
|
||||
|
|
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>
|
||||
</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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue
Block a user