diff --git a/pkg/controllers/contact.go b/pkg/controllers/contact.go new file mode 100644 index 0000000..f3e9400 --- /dev/null +++ b/pkg/controllers/contact.go @@ -0,0 +1,90 @@ +package controllers + +import ( + "fmt" + "html/template" + "net/http" + + "git.kirsle.net/apps/gophertype/pkg/glue" + "git.kirsle.net/apps/gophertype/pkg/mail" + "git.kirsle.net/apps/gophertype/pkg/markdown" + "git.kirsle.net/apps/gophertype/pkg/responses" + "git.kirsle.net/apps/gophertype/pkg/session" + "git.kirsle.net/apps/gophertype/pkg/settings" + "github.com/albrow/forms" +) + +func init() { + glue.Register(glue.Endpoint{ + Path: "/contact", + Methods: []string{"GET", "POST"}, + Handler: ContactHandler, + }) +} + +// ContactHandler receives admin emails from users. +func ContactHandler(w http.ResponseWriter, r *http.Request) { + var ( + v = responses.NewTemplateVars(w, r) + ses = session.Get(r) + ) + + // Load their cached name from any previous comments they may have posted. + name, _ := ses.Values["c.name"].(string) + email, _ := ses.Values["c.email"].(string) + + for r.Method == http.MethodPost { + // Validate form parameters. + form, _ := forms.Parse(r) + val := form.Validator() + val.Require("message") + if val.HasErrors() { + v.ValidationError = val.ErrorMap() + v.V["Error"] = "Missing required form fields." + break + } + + var ( + name = form.Get("name") + email = form.Get("email") + subject = form.Get("subject") + message = form.Get("message") + ) + + // Cache their name in their session for future comments/asks. + ses.Values["c.name"] = name + ses.Values["c.email"] = email + ses.Save(r, w) + + // Email the site admin. + if name == "" { + name = "Anonymous" + } + if subject == "" { + subject = "No Subject" + } + + go mail.EmailAdmins(mail.Email{ + Subject: fmt.Sprintf("Contact Me (%s) from %s: %s", settings.Current.Title, name, subject), + Template: "_builtin/email/generic.gohtml", + Data: map[string]interface{}{ + "Subject": subject, + "ReplyTo": email, + "Message": template.HTML( + markdown.RenderMarkdown(fmt.Sprintf( + "Subject: %s\nSender name: %s\nSender email: %s\n\n%s\n", + subject, name, email, message, + )), + ), + }, + }) + + session.Flash(w, r, "Thank you! Your message has been e-mailed to the site owner.") + responses.Redirect(w, r, "/contact") + return + } + + v.V["name"] = name + v.V["email"] = email + responses.RenderTemplate(w, r, "_builtin/contact.gohtml", v) +} diff --git a/pkg/controllers/questions.go b/pkg/controllers/questions.go new file mode 100644 index 0000000..42dd580 --- /dev/null +++ b/pkg/controllers/questions.go @@ -0,0 +1,193 @@ +package controllers + +import ( + "fmt" + "html/template" + "net/http" + "strings" + "time" + + "git.kirsle.net/apps/gophertype/pkg/authentication" + "git.kirsle.net/apps/gophertype/pkg/console" + "git.kirsle.net/apps/gophertype/pkg/glue" + "git.kirsle.net/apps/gophertype/pkg/mail" + "git.kirsle.net/apps/gophertype/pkg/markdown" + "git.kirsle.net/apps/gophertype/pkg/models" + "git.kirsle.net/apps/gophertype/pkg/responses" + "git.kirsle.net/apps/gophertype/pkg/session" + "git.kirsle.net/apps/gophertype/pkg/settings" + "github.com/albrow/forms" + "github.com/gorilla/mux" + "github.com/kirsle/blog/src/log" +) + +func init() { + glue.Register(glue.Endpoint{ + Path: "/ask", + Methods: []string{"GET", "POST"}, + Handler: QuestionHandler, + }) + glue.Register(glue.Endpoint{ + Path: "/ask/answer", + Methods: []string{"POST"}, + Middleware: []mux.MiddlewareFunc{ + authentication.LoginRequired, + }, + Handler: AnswerHandler, + }) +} + +// QuestionHandler implements the "Ask Me Anything" at the URL "/ask" +func QuestionHandler(w http.ResponseWriter, r *http.Request) { + var ( + v = responses.NewTemplateVars(w, r) + ses = session.Get(r) + ) + + // Load their cached name from any previous comments they may have posted. + name, _ := ses.Values["c.name"].(string) + + q := models.Questions.New() + q.Name = name + + for r.Method == http.MethodPost { + form, _ := forms.Parse(r) + q.ParseForm(r) + + // Validate form parameters. + val := form.Validator() + val.Require("question") + if val.HasErrors() { + v.ValidationError = val.ErrorMap() + v.V["Error"] = "Missing required form fields." + break + } + + // Cache their name in their session for future comments/asks. + ses.Values["c.name"] = q.Name + ses.Save(r, w) + + // Save the question. + err := q.Save() + if err != nil { + log.Error("Error saving neq eustion: %s", err) + responses.Error(w, r, http.StatusInternalServerError, "Error saving question: "+err.Error()) + return + } + + // Email the site admin. + if name == "" { + name = "Anonymous" + } + subject := fmt.Sprintf("Ask Me Anything (%s) from %s", settings.Current.Title, name) + go mail.EmailAdmins(mail.Email{ + Subject: subject, + Template: "_builtin/email/generic.gohtml", + Data: map[string]interface{}{ + "Subject": subject, + "Message": template.HTML( + markdown.RenderMarkdown(fmt.Sprintf( + "%s\n\nAnswer this at %s", + q.Question, + strings.TrimSuffix(settings.Current.BaseURL, "/")+"/ask", + )), + ), + }, + }) + + session.Flash(w, r, "Your question has been recorded!") + responses.Redirect(w, r, "/ask") + return + } + + // If logged in, load the pending questions. + if authentication.LoggedIn(r) { + pending, err := models.Questions.Pending() + if err != nil { + console.Error("Error loading pending questions: %s", err) + } + v.V["Pending"] = pending + } + + // Load the recently answered questions for public users. + recent, err := models.Questions.RecentlyAnswered(10) + if err != nil { + console.Error("Error loading recently answered questions: %s", err) + } + + v.V["Q"] = q + v.V["Recent"] = recent + responses.RenderTemplate(w, r, "_builtin/questions.gohtml", v) +} + +// AnswerHandler handles answering (and deleting) questions. +func AnswerHandler(w http.ResponseWriter, r *http.Request) { + v := responses.NewTemplateVars(w, r) + + CurrentUser, _ := authentication.CurrentUser(r) + + // Validate form parameters. + form, _ := forms.Parse(r) + val := form.Validator() + val.Require("id") + val.Require("answer") + val.Require("submit") + if val.HasErrors() { + v.ValidationError = val.ErrorMap() + v.V["Error"] = "Missing required form fields." + responses.RenderTemplate(w, r, "_builtin/questions.gohtml", v) + return + } + + // Look up the question. + q, err := models.Questions.Load(form.GetInt("id")) + if err != nil { + responses.Error(w, r, http.StatusInternalServerError, err.Error()) + return + } + + // Handle submit actions. + switch form.Get("submit") { + case "answer": + // Prepare a Markdown themed blog post for this answer. + name := q.Name + if name == "" { + name = "Anonymous" + } + + post := models.Post{ + Title: "Ask", + ContentType: "markdown", + Privacy: models.Public, + EnableComments: true, + AuthorID: CurrentUser.ID, + Tags: []models.TaggedPost{ + models.TaggedPost{Tag: "ask"}, + }, + Fragment: fmt.Sprintf("ask-%s", + time.Now().Format("20060102150405"), + ), + Body: fmt.Sprintf( + "> **%s** asks:\n\n> %s\n\n%s\n", + name, + strings.Replace(q.Question, "\n", "\n> ", 0), + form.Get("answer"), + ), + } + post.Save() + + // Associate the question to this post ID. + q.PostID = post.ID + q.Answered = true + q.Save() + + // Send the admin to the post edit page. + responses.Redirect(w, r, "/"+post.Fragment) + case "delete": + q.Delete() + session.Flash(w, r, "Question deleted!") + responses.Redirect(w, r, "/ask") + default: + responses.BadRequest(w, r, "Invalid submit method.") + } +} diff --git a/pkg/mail/mail.go b/pkg/mail/mail.go index 5d304d5..592be6f 100644 --- a/pkg/mail/mail.go +++ b/pkg/mail/mail.go @@ -103,6 +103,16 @@ func SendEmail(email Email) { } } +// EmailAdmins sends an e-mail to all admin user email addresses. +func EmailAdmins(email Email) { + if adminEmails, err := models.Users.ListAdminEmails(); err == nil { + email.To = strings.Join(adminEmails, ", ") + email.Admin = true + console.Info("Mail site admin '%s' about email '%s'", email.To, email.Subject) + SendEmail(email) + } +} + // NotifyComment sends notification emails about comments. func NotifyComment(subject string, originURL string, c models.Comment) { s := settings.Current diff --git a/pkg/models/models.go b/pkg/models/models.go index 719e025..05c7357 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -19,8 +19,5 @@ type BaseModel struct { // UseDB registers a database driver. func UseDB(db *gorm.DB) { DB = db - DB.AutoMigrate(&User{}) - DB.Debug().AutoMigrate(&Post{}) - DB.AutoMigrate(&TaggedPost{}) - DB.AutoMigrate(&Comment{}) + DB.AutoMigrate(&User{}, &Post{}, &TaggedPost{}, &Comment{}, &Question{}) } diff --git a/pkg/models/questions.go b/pkg/models/questions.go new file mode 100644 index 0000000..ee05857 --- /dev/null +++ b/pkg/models/questions.go @@ -0,0 +1,70 @@ +package models + +import "net/http" + +type askMan struct{} + +// Questions is a singleton manager class for Question model access. +var Questions = askMan{} + +// Question model. +type Question struct { + BaseModel + + Name string + Question string + Answered bool + PostID int // FKey Post.id + + // Relationships. + Post Post +} + +// New creates a new Question model. +func (m askMan) New() Question { + return Question{} +} + +// Load a comment by ID. +func (m askMan) Load(id int) (Question, error) { + var q Question + r := DB.Preload("Post").First(&q, id) + return q, r.Error +} + +// Pending returns questions in need of answering. +func (m askMan) Pending() ([]Question, error) { + var q []Question + r := DB.Preload("Post").Where("answered=false").Find(&q) + return q, r.Error +} + +// RecentlyAnswered returns questions that have blog posts attached. +func (m askMan) RecentlyAnswered(depth int) ([]Question, error) { + var qq []Question + r := DB.Preload("Post"). + Where("answered=true AND post_id IS NOT NULL"). + Order("created_at desc"). + Limit(depth). + Find(&qq) + return qq, r.Error +} + +// Save the question. +func (q Question) Save() error { + if DB.NewRecord(q) { + return DB.Create(&q).Error + } + return DB.Save(&q).Error +} + +// Delete the question. +func (q Question) Delete() error { + return DB.Delete(&q).Error +} + +// ParseForm sets the question's attributes from HTTP form. +func (q *Question) ParseForm(r *http.Request) { + q.Name = r.FormValue("name") + q.Question = r.FormValue("question") +} diff --git a/pvt-www/_builtin/blog/view-post.gohtml b/pvt-www/_builtin/blog/view-post.gohtml index 227e29c..2477fc3 100644 --- a/pvt-www/_builtin/blog/view-post.gohtml +++ b/pvt-www/_builtin/blog/view-post.gohtml @@ -43,8 +43,6 @@ -{{ .CurrentUser }} - {{ if .CurrentUser.IsAdmin }}
Admin: diff --git a/pvt-www/_builtin/contact.gohtml b/pvt-www/_builtin/contact.gohtml new file mode 100644 index 0000000..9c42310 --- /dev/null +++ b/pvt-www/_builtin/contact.gohtml @@ -0,0 +1,53 @@ +{{ define "title" }}Contact Me{{ end }} +{{ define "content" }} + +

Contact Me

+ +
+
+

+ Fill out this form to send an e-mail to the site owner. +

+ + +
+ {{ CSRF }} + +
+
+ + +
+
+ + + + If you want a response; optional. + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+ +{{ end }} diff --git a/pvt-www/_builtin/email/generic.gohtml b/pvt-www/_builtin/email/generic.gohtml new file mode 100644 index 0000000..2d706db --- /dev/null +++ b/pvt-www/_builtin/email/generic.gohtml @@ -0,0 +1,39 @@ + + + + + + + + {{ .Subject }} + + + +
+ + + + + + + + + + +
+ + {{ .Subject }} + +
+ + {{ .Data.Message }} + +
+ + This e-mail was automatically generated; do not reply to it. + +
+
+ + + diff --git a/pvt-www/_builtin/questions.gohtml b/pvt-www/_builtin/questions.gohtml new file mode 100644 index 0000000..c100b7d --- /dev/null +++ b/pvt-www/_builtin/questions.gohtml @@ -0,0 +1,119 @@ +{{ define "title" }}Ask Me Anything{{ end }} +{{ define "content" }} + +{{ $Q := .V.Q }} +
+
+
+ {{ CSRF }} + +
+
+ +
+
+ +
+
+
+ + +
+ +
+
+ +
+
+ +
+
+
+ +{{ if gt (len .V.Recent) 0 }} +
+
+ Recently Answered +
+
+ {{ range .V.Recent }} + {{ or .Name "Anonymous" }} asks: +

{{ .Question }}

+

+ Read answer> +

+

+ + {{ .CreatedAt.Format "January 2, 2006" }} + +

+ +
+ {{ end }} + +

+ → More questions & answers +

+
+
+{{ end }} + +{{ if .LoggedIn }} +
+
+ Pending Questions +
+
+ {{ if not .V.Pending }} + There are no pending questions. + {{ end }} + + {{ range .V.Pending }} +

+ {{ or .Name "Anonymous" }} asks:
+ + {{ .CreatedAt.Format "January 2, 2006 @ 15:04 MST" }} by + +

+

+ {{ .Question }} +

+ +
+
+ {{ CSRF }} + + + +
+ + +
+
+
+ + +
+ {{ end }} +
+
+{{ end }}{# if .LoggedIn #} + +{{ end }}