Controller docs, RSVP counts, comment integration
This commit is contained in:
parent
a3ba16c9b2
commit
5c4f5612f8
15
internal/controllers/admin/admin_doc.go
Normal file
15
internal/controllers/admin/admin_doc.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
Package admin provides controllers for admin features of the app.
|
||||||
|
|
||||||
|
Routes
|
||||||
|
|
||||||
|
Admin Only
|
||||||
|
/admin/ Admin index page
|
||||||
|
/admin/settings Manage app settings
|
||||||
|
/admin/editor Web page editor
|
||||||
|
|
||||||
|
Related Models
|
||||||
|
|
||||||
|
users
|
||||||
|
*/
|
||||||
|
package admin
|
21
internal/controllers/authctl/authctl_doc.go
Normal file
21
internal/controllers/authctl/authctl_doc.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
Package authctl implements the authentication controllers.
|
||||||
|
|
||||||
|
Routes
|
||||||
|
|
||||||
|
/login Log in to a user account
|
||||||
|
/logout Log out
|
||||||
|
/account Account home page (to edit profile or reset password)
|
||||||
|
/age-verify If the blog implements age gating
|
||||||
|
|
||||||
|
Related Models
|
||||||
|
|
||||||
|
users
|
||||||
|
|
||||||
|
Age Gating
|
||||||
|
|
||||||
|
If the blog marks itself as NSFW, visitors to the blog must verify their age
|
||||||
|
to enter. The middleware that controls this is at `middleware/age-gate.go`.
|
||||||
|
The controller is here at `controllers/authctl/gate-gate.go`
|
||||||
|
*/
|
||||||
|
package authctl
|
48
internal/controllers/comments/comments_doc.go
Normal file
48
internal/controllers/comments/comments_doc.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
Package comments implements the controllers for the commenting system.
|
||||||
|
|
||||||
|
Routes
|
||||||
|
|
||||||
|
/comments Main comment handler
|
||||||
|
/comments/subscription Manage subscription to comment threads
|
||||||
|
/comments/quick-delete Quickly delete spam comments from admin email
|
||||||
|
|
||||||
|
Related Models
|
||||||
|
|
||||||
|
comments
|
||||||
|
|
||||||
|
Description
|
||||||
|
|
||||||
|
Comments are a generic comment thread system that can be placed on any page.
|
||||||
|
They are automatically attached to blog posts (unless you disable comments on
|
||||||
|
them) but they can be used anywhere. A guestbook, on the events pages, on any
|
||||||
|
custom pages, etc.
|
||||||
|
|
||||||
|
Every comment thread has a unique ID, so some automated threads have name spaces,
|
||||||
|
like "blog-$id".
|
||||||
|
|
||||||
|
Subscriptions
|
||||||
|
|
||||||
|
When users leave a comment with their e-mail address, they may opt in to getting
|
||||||
|
notified about future comments left on the same thread.
|
||||||
|
|
||||||
|
Go Template Function
|
||||||
|
|
||||||
|
You can create a comment form on a page in Go templates like this:
|
||||||
|
|
||||||
|
func RenderComments(r *http.Request, subject string, ids ...string) template.HTML
|
||||||
|
{{ RenderComments .Request "Title" "id part" "id part" "id part..." }}
|
||||||
|
|
||||||
|
The subject is used in the notification e-mail. The ID strings are joined together
|
||||||
|
by dashes and you can have as many as you need. Examples:
|
||||||
|
|
||||||
|
Blog posts in the format `blog-<postID>` like `blog-42`
|
||||||
|
{{ RenderComments .Request .Data.Title "blog" .Data.IDString }}
|
||||||
|
|
||||||
|
Events in the format `event-<eventID>` like `event-2`
|
||||||
|
{{ RenderComments .Request "My Big Party" "event" "2" }}
|
||||||
|
|
||||||
|
Custom ID for a guestbook
|
||||||
|
{{ RenderComments .Request "Guestbook" "guestbook" }}
|
||||||
|
*/
|
||||||
|
package comments
|
|
@ -49,6 +49,8 @@ func contactAuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Authenticate the contact in the session.
|
// Authenticate the contact in the session.
|
||||||
session := sessions.Get(r)
|
session := sessions.Get(r)
|
||||||
session.Values["contact-id"] = c.ID
|
session.Values["contact-id"] = c.ID
|
||||||
|
session.Values["c.name"] = c.Name() // comment form values auto-filled nicely
|
||||||
|
session.Values["c.email"] = c.Email
|
||||||
err = session.Save(r, w)
|
err = session.Save(r, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("contactAuthHandler: save session error: %s", err)
|
log.Error("contactAuthHandler: save session error: %s", err)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package events
|
package events
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
"github.com/kirsle/blog/internal/middleware/auth"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/internal/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/internal/responses"
|
||||||
|
"github.com/kirsle/blog/models/comments"
|
||||||
"github.com/kirsle/blog/models/events"
|
"github.com/kirsle/blog/models/events"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
@ -85,11 +87,47 @@ func viewHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count up the RSVP statuses, and also look for the authed contact's RSVP.
|
||||||
|
var (
|
||||||
|
countGoing int
|
||||||
|
countMaybe int
|
||||||
|
countNotGoing int
|
||||||
|
countInvited int
|
||||||
|
)
|
||||||
|
for _, rsvp := range event.RSVP {
|
||||||
|
if authedContact.ID != 0 && rsvp.ContactID == authedContact.ID {
|
||||||
|
v["authedRVSP"] = rsvp
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rsvp.Status {
|
||||||
|
case events.StatusGoing:
|
||||||
|
countGoing++
|
||||||
|
case events.StatusMaybe:
|
||||||
|
countMaybe++
|
||||||
|
case events.StatusNotGoing:
|
||||||
|
countNotGoing++
|
||||||
|
default:
|
||||||
|
countInvited++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v["countGoing"] = countGoing
|
||||||
|
v["countMaybe"] = countMaybe
|
||||||
|
v["countNotGoing"] = countNotGoing
|
||||||
|
v["countInvited"] = countInvited
|
||||||
|
|
||||||
// If we're posting, are we RSVPing?
|
// If we're posting, are we RSVPing?
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
action := r.PostFormValue("action")
|
action := r.PostFormValue("action")
|
||||||
switch action {
|
switch action {
|
||||||
case "answer-rsvp":
|
case "answer-rsvp":
|
||||||
|
// Subscribe them to the comment thread on this page if we have an email.
|
||||||
|
if authedContact.Email != "" {
|
||||||
|
thread := fmt.Sprintf("event-%d", event.ID)
|
||||||
|
log.Info("events.viewHandler: subscribe email %s to thread %s", authedContact.Email, thread)
|
||||||
|
ml := comments.LoadMailingList()
|
||||||
|
ml.Subscribe(thread, authedContact.Email)
|
||||||
|
}
|
||||||
|
|
||||||
answer := r.PostFormValue("submit")
|
answer := r.PostFormValue("submit")
|
||||||
for _, rsvp := range event.RSVP {
|
for _, rsvp := range event.RSVP {
|
||||||
if rsvp.ContactID == authedContact.ID {
|
if rsvp.ContactID == authedContact.ID {
|
||||||
|
|
73
internal/controllers/events/events_doc.go
Normal file
73
internal/controllers/events/events_doc.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
Package events provides controllers for the event system.
|
||||||
|
|
||||||
|
Routes
|
||||||
|
|
||||||
|
Admin Only
|
||||||
|
/e/admin/edit Edit an event
|
||||||
|
/e/admin/invite/<event_id> Manage invitations and contacts to an event
|
||||||
|
/e/admin/ Event admin index
|
||||||
|
|
||||||
|
Public
|
||||||
|
/e/<event_fragment> Public URL for event page
|
||||||
|
/c/logout Logout authenticated contact
|
||||||
|
/c/<user_secret> Authenticate a contact
|
||||||
|
|
||||||
|
Related Models
|
||||||
|
|
||||||
|
contacts
|
||||||
|
events
|
||||||
|
|
||||||
|
Description
|
||||||
|
|
||||||
|
Events provide basic features of event planning software, including
|
||||||
|
(for the admin user of the site):
|
||||||
|
|
||||||
|
* View, edit, delete events.
|
||||||
|
* Events have a title, datetime range, Markdown body, etc.
|
||||||
|
* You can invite users (contacts) to the event.
|
||||||
|
|
||||||
|
Contacts are their own distinct entity in the database, separate from Users
|
||||||
|
(which are the website admin user accounts, with passwords).
|
||||||
|
|
||||||
|
When you invite people to an event, you create new Contact entries for the
|
||||||
|
people who don't have them yet or invite the ones who exist. Each Contact
|
||||||
|
uniquely groups a first and last name, e-mail address and SMS number.
|
||||||
|
|
||||||
|
When you send out invite e-mail or SMS messages, each Contact is given their own
|
||||||
|
personal link to view the event details. The link goes to the URL
|
||||||
|
`/c/<user_secret>?e=<event_id>`, where "user_secret" is a secret random string
|
||||||
|
generated on their Contact object (to identify the Contact) and "event_id" is
|
||||||
|
the ID number of the event.
|
||||||
|
|
||||||
|
The Contact Authenticator endpoint at `/c/<user_secret>` "authenticates" them
|
||||||
|
in their browser session by setting the session key "contact.id" -- this is only
|
||||||
|
of any interest to the Events controller anyway.
|
||||||
|
|
||||||
|
Events, Contacts, and RSVPs
|
||||||
|
|
||||||
|
There is an Event row for every distinct event, and a single Contact row for
|
||||||
|
every distinct person.
|
||||||
|
|
||||||
|
RSVP's are how we marry Events to their invited Contacts, *and* how we track
|
||||||
|
the RSVP response of each contact.
|
||||||
|
|
||||||
|
When a user clicks the link in their invite e-mail, their browser authenticates
|
||||||
|
as their Contact (it greets them by their name and shows the buttons to respond
|
||||||
|
to the event). When they click "Going" or "Not Going", the server knows which
|
||||||
|
contact they are and can find them on the RSVP list, and mark their status
|
||||||
|
accordingly.
|
||||||
|
|
||||||
|
Comment Form
|
||||||
|
|
||||||
|
Events have comment forms using the thread format "event-<id>", like "event-1"
|
||||||
|
for the first event. When an authenticated Contact (one who clicked an email
|
||||||
|
link) interacts with the Response Form, we auto-subscribe their e-mail to the
|
||||||
|
comment form. This way anybody leaving comments on the page will naturally
|
||||||
|
notify the users who have 1) awareness of the event, 2) have given an answer
|
||||||
|
about it.
|
||||||
|
|
||||||
|
They can easily unsubscribe from the comment thread as normal for the blog's
|
||||||
|
commenting system.
|
||||||
|
*/
|
||||||
|
package events
|
45
internal/controllers/posts/posts_doc.go
Normal file
45
internal/controllers/posts/posts_doc.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
Package postctl implements all the web blog features.
|
||||||
|
|
||||||
|
Routes
|
||||||
|
|
||||||
|
Public
|
||||||
|
/blog Blog index
|
||||||
|
/blog.rss RSS feed
|
||||||
|
/blog.atom Atom feed
|
||||||
|
/archive Blog archives
|
||||||
|
/tagged Index of all blog tags
|
||||||
|
/tagged/<tag> View posts by tag
|
||||||
|
/<fragment> View blog entry by its URL fragment
|
||||||
|
|
||||||
|
Admin Only
|
||||||
|
/blog/edit Create or edit blog post
|
||||||
|
/blog/delete Confirm deletion of blog post
|
||||||
|
/blog/drafts View all draft entries
|
||||||
|
/blog/private View all private entries
|
||||||
|
|
||||||
|
Related Models
|
||||||
|
|
||||||
|
posts
|
||||||
|
|
||||||
|
Description
|
||||||
|
|
||||||
|
Each post is in its own JsonDB document at `posts/entries/<id>.json` and
|
||||||
|
contains all its data (title, body, tags, timestamps, etc.)
|
||||||
|
|
||||||
|
For faster retrieval and caching of overall post data, there is a Blog Index
|
||||||
|
that gets saved in JsonDB at `posts/index.json`. The index summarizes ALL of
|
||||||
|
the blog posts by caching their basic details (ID, URL fragment, title,
|
||||||
|
tags, created time). This document is used for getting a narrower list of posts
|
||||||
|
to work with, for index pages (with pagination), "by tagged" pages, etc.
|
||||||
|
|
||||||
|
Usually the front-end settles on 5 or 10 posts it wants to render, and it only
|
||||||
|
had to look at the index. For the archive view where it only needs the blog
|
||||||
|
titles, it already has these too. For the posts where it needs the full body,
|
||||||
|
it has the IDs and can just select each one pretty quickly.
|
||||||
|
|
||||||
|
In case anything goes wrong with the blog index, you can always delete the
|
||||||
|
`posts/index.json` and it will be re-generated from scratch in a one-time scan
|
||||||
|
of the entire posts DB (opening every document).
|
||||||
|
*/
|
||||||
|
package postctl
|
|
@ -24,6 +24,7 @@
|
||||||
<p class="small">
|
<p class="small">
|
||||||
[<a href="/c/logout?next={{ $.Request.URL.Path }}">not {{ $authedContact.Name }}?</a>]
|
[<a href="/c/logout?next={{ $.Request.URL.Path }}">not {{ $authedContact.Name }}?</a>]
|
||||||
</p>
|
</p>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -56,6 +57,15 @@
|
||||||
|
|
||||||
<h4 class="mt-4">Invited</h4>
|
<h4 class="mt-4">Invited</h4>
|
||||||
|
|
||||||
|
{{ if $.Data.countGoing }}
|
||||||
|
<p class="text-muted">
|
||||||
|
<em>{{ $.Data.countGoing }}
|
||||||
|
{{ if eq $.Data.countGoing 1 }}person is{{ else }}people are{{ end }}
|
||||||
|
going:
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<div style="max-height: 500px; overflow: auto">
|
<div style="max-height: 500px; overflow: auto">
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{{ range .RSVP }}
|
{{ range .RSVP }}
|
||||||
|
@ -105,6 +115,9 @@
|
||||||
</li>
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
<p class="text-muted mt-2">
|
||||||
|
{{ $.Data.countInvited }} invited.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user