Ask Me Anything and Contact Me pages
This commit is contained in:
parent
5b6712ea97
commit
bf86ceb585
90
pkg/controllers/contact.go
Normal file
90
pkg/controllers/contact.go
Normal file
|
@ -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)
|
||||
}
|
193
pkg/controllers/questions.go
Normal file
193
pkg/controllers/questions.go
Normal file
|
@ -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.")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
|
|
70
pkg/models/questions.go
Normal file
70
pkg/models/questions.go
Normal file
|
@ -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")
|
||||
}
|
|
@ -43,8 +43,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{ .CurrentUser }}
|
||||
|
||||
{{ if .CurrentUser.IsAdmin }}
|
||||
<div class="alert alert-secondary">
|
||||
<strong>Admin:</strong>
|
||||
|
|
53
pvt-www/_builtin/contact.gohtml
Normal file
53
pvt-www/_builtin/contact.gohtml
Normal file
|
@ -0,0 +1,53 @@
|
|||
{{ define "title" }}Contact Me{{ end }}
|
||||
{{ define "content" }}
|
||||
|
||||
<h1>Contact Me</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
Fill out this form to send an e-mail to the site owner.
|
||||
</p>
|
||||
|
||||
|
||||
<form action="/contact" method="POST">
|
||||
{{ CSRF }}
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="name">Your name:</label>
|
||||
<input type="text" class="form-control" id="name" placeholder="Anonymous" value="{{ .V.name }}">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="name">Your email:</label>
|
||||
<input type="email" class="form-control" id="email" placeholder="name@example.com" value="{{ .V.email }}">
|
||||
<small id="email-help" class="form-text text-muted">
|
||||
If you want a response; optional.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-12">
|
||||
<label for="subject">Subject</label>
|
||||
<input type="text" class="form-control" name="subject" placeholder="No Subject">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-12">
|
||||
<label for="message" title="Required">Message<span class="text-danger">*</span></label>
|
||||
<textarea cols="80" rows="10" class="form-control" name="message" id="message" required="required"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-12">
|
||||
<button type="submit" class="btn btn-primary">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ end }}
|
39
pvt-www/_builtin/email/generic.gohtml
Normal file
39
pvt-www/_builtin/email/generic.gohtml
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="x-apple-disable-message-reformatting"><!-- Disable auto-scale in iOS 10 Mail -->
|
||||
<title>{{ .Subject }}</title>
|
||||
</head>
|
||||
<body width="100%" bgcolor="#FFFFFF" color="#000000" style="margin: 0; mso-line-height-rule: exactly;">
|
||||
|
||||
<center>
|
||||
<table width="90%" cellspacing="0" cellpadding="8" style="border: 1px solid #000000">
|
||||
<tr>
|
||||
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||
<font face="Helvetica,Arial,Verdana-sans-serif" size="6" color="#000000">
|
||||
<b>{{ .Subject }}</b>
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="top" bgcolor="#FEFEFE">
|
||||
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||
{{ .Data.Message }}
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||
This e-mail was automatically generated; do not reply to it.
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
|
||||
</body>
|
||||
</html>
|
119
pvt-www/_builtin/questions.gohtml
Normal file
119
pvt-www/_builtin/questions.gohtml
Normal file
|
@ -0,0 +1,119 @@
|
|||
{{ define "title" }}Ask Me Anything{{ end }}
|
||||
{{ define "content" }}
|
||||
|
||||
{{ $Q := .V.Q }}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form action="/ask" method="POST">
|
||||
{{ CSRF }}
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-12 col-md-2">
|
||||
<label class="col-form-label" for="name">Name:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-10">
|
||||
<input type="text" class="form-control" id="name" name="name" value="{{ $Q.Name }}" placeholder="Anonymous">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-12" for="question">Question: <small>(required)</small></label>
|
||||
<textarea cols="80" rows="6"
|
||||
class="col-12 form-control"
|
||||
name="question"
|
||||
id="question"
|
||||
placeholder="Ask me anything">{{ $Q.Question }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col">
|
||||
<button type="submit"
|
||||
name="submit"
|
||||
value="ask"
|
||||
class="btn btn-primary">Ask away!</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ if gt (len .V.Recent) 0 }}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
Recently Answered
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ range .V.Recent }}
|
||||
<strong>{{ or .Name "Anonymous" }}</strong> asks:
|
||||
<p>{{ .Question }}</p>
|
||||
<p>
|
||||
<a href="/{{ .Post.Fragment }}">Read answer></a>
|
||||
</p>
|
||||
<p class="blog-meta">
|
||||
<em title="{{ .CreatedAt.Format "Jan 2 2006 15:04:05 MST" }}">
|
||||
{{ .CreatedAt.Format "January 2, 2006" }}
|
||||
</em>
|
||||
</p>
|
||||
|
||||
<hr class="my-4">
|
||||
{{ end }}
|
||||
|
||||
<p>
|
||||
<a href="/tagged/ask">→ More questions & answers</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if .LoggedIn }}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header bg-secondary text-light">
|
||||
Pending Questions
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ if not .V.Pending }}
|
||||
<em>There are no pending questions.</em>
|
||||
{{ end }}
|
||||
|
||||
{{ range .V.Pending }}
|
||||
<p>
|
||||
<strong>{{ or .Name "Anonymous" }}</strong> asks:<br>
|
||||
<small class="text-muted">
|
||||
<em>{{ .CreatedAt.Format "January 2, 2006 @ 15:04 MST" }}</em> by
|
||||
</small>
|
||||
</p>
|
||||
<p>
|
||||
{{ .Question }}
|
||||
</p>
|
||||
|
||||
<div id="form-{{ .ID }}" class="dhtml-forms">
|
||||
<form method="POST" action="/ask/answer">
|
||||
{{ CSRF }}
|
||||
<input type="hidden" name="id" value="{{ .ID }}">
|
||||
<textarea cols="80" rows="4"
|
||||
class="form-control"
|
||||
name="answer"
|
||||
required="required"
|
||||
placeholder="Answer (Markdown formatting allowed)"></textarea>
|
||||
|
||||
<div class="btn-group mt-3">
|
||||
<button type="submit" name="submit" value="answer" class="btn btn-primary">
|
||||
Answer
|
||||
</button>
|
||||
<button type="submit" name="submit" value="delete" class="btn btn-danger">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="button-{{ .ID }}" class="dhtml-buttons" style="display: none">
|
||||
<button type="button" class="btn" id="show-{{ .ID }}" class="dhtml-show-button">Answer or delete</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}{# if .LoggedIn #}
|
||||
|
||||
{{ end }}
|
Loading…
Reference in New Issue
Block a user