parent
5b6712ea97
commit
bf86ceb585
9 changed files with 575 additions and 6 deletions
@ -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) |
||||
} |
@ -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.") |
||||
} |
||||
} |
@ -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") |
||||
} |
@ -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 }} |
@ -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> |
@ -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