Controller docs, RSVP counts, comment integration

This commit is contained in:
Noah 2018-05-12 11:48:18 -07:00
parent a3ba16c9b2
commit 5c4f5612f8
8 changed files with 255 additions and 0 deletions

View 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

View 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

View 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

View File

@ -49,6 +49,8 @@ func contactAuthHandler(w http.ResponseWriter, r *http.Request) {
// Authenticate the contact in the session.
session := sessions.Get(r)
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)
if err != nil {
log.Error("contactAuthHandler: save session error: %s", err)

View File

@ -1,6 +1,7 @@
package events
import (
"fmt"
"net/http"
"sort"
@ -9,6 +10,7 @@ import (
"github.com/kirsle/blog/internal/middleware/auth"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/models/comments"
"github.com/kirsle/blog/models/events"
"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 r.Method == http.MethodPost {
action := r.PostFormValue("action")
switch action {
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")
for _, rsvp := range event.RSVP {
if rsvp.ContactID == authedContact.ID {

View 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

View 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

View File

@ -24,6 +24,7 @@
<p class="small">
[<a href="/c/logout?next={{ $.Request.URL.Path }}">not {{ $authedContact.Name }}?</a>]
</p>
</form>
</div>
</div>
{{ end }}
@ -56,6 +57,15 @@
<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">
<ul class="list-group">
{{ range .RSVP }}
@ -105,6 +115,9 @@
</li>
{{ end }}
</ul>
<p class="text-muted mt-2">
{{ $.Data.countInvited }} invited.
</p>
</div>
</div>
</div>