diff --git a/internal/controllers/admin/admin_doc.go b/internal/controllers/admin/admin_doc.go new file mode 100644 index 0000000..0bdaef9 --- /dev/null +++ b/internal/controllers/admin/admin_doc.go @@ -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 diff --git a/internal/controllers/authctl/authctl_doc.go b/internal/controllers/authctl/authctl_doc.go new file mode 100644 index 0000000..b7edce1 --- /dev/null +++ b/internal/controllers/authctl/authctl_doc.go @@ -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 diff --git a/internal/controllers/comments/comments_doc.go b/internal/controllers/comments/comments_doc.go new file mode 100644 index 0000000..832aa0f --- /dev/null +++ b/internal/controllers/comments/comments_doc.go @@ -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-` like `blog-42` + {{ RenderComments .Request .Data.Title "blog" .Data.IDString }} + + Events in the format `event-` like `event-2` + {{ RenderComments .Request "My Big Party" "event" "2" }} + + Custom ID for a guestbook + {{ RenderComments .Request "Guestbook" "guestbook" }} +*/ +package comments diff --git a/internal/controllers/events/contact-auth.go b/internal/controllers/events/contact-auth.go index d453c73..4bc0038 100644 --- a/internal/controllers/events/contact-auth.go +++ b/internal/controllers/events/contact-auth.go @@ -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) diff --git a/internal/controllers/events/events.go b/internal/controllers/events/events.go index 7407e02..f0987a0 100644 --- a/internal/controllers/events/events.go +++ b/internal/controllers/events/events.go @@ -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 { diff --git a/internal/controllers/events/events_doc.go b/internal/controllers/events/events_doc.go new file mode 100644 index 0000000..3d1e57a --- /dev/null +++ b/internal/controllers/events/events_doc.go @@ -0,0 +1,73 @@ +/* +Package events provides controllers for the event system. + +Routes + + Admin Only + /e/admin/edit Edit an event + /e/admin/invite/ Manage invitations and contacts to an event + /e/admin/ Event admin index + + Public + /e/ Public URL for event page + /c/logout Logout authenticated contact + /c/ 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/?e=`, 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/` "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-", 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 diff --git a/internal/controllers/posts/posts_doc.go b/internal/controllers/posts/posts_doc.go new file mode 100644 index 0000000..528d3d4 --- /dev/null +++ b/internal/controllers/posts/posts_doc.go @@ -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/ View posts by tag + / 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/.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 diff --git a/root/events/view.gohtml b/root/events/view.gohtml index 189253f..94a51e1 100644 --- a/root/events/view.gohtml +++ b/root/events/view.gohtml @@ -24,6 +24,7 @@

[not {{ $authedContact.Name }}?]

+ {{ end }} @@ -56,6 +57,15 @@

Invited

+ {{ if $.Data.countGoing }} +

+ {{ $.Data.countGoing }} + {{ if eq $.Data.countGoing 1 }}person is{{ else }}people are{{ end }} + going: + +

+ {{ end }} +
    {{ range .RSVP }} @@ -105,6 +115,9 @@ {{ end }}
+

+ {{ $.Data.countInvited }} invited. +