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