Questions system WIP
This commit is contained in:
parent
eae499c640
commit
517a2ee86b
4
blog.go
4
blog.go
|
@ -14,6 +14,7 @@ import (
|
|||
commentctl "github.com/kirsle/blog/internal/controllers/comments"
|
||||
"github.com/kirsle/blog/internal/controllers/contact"
|
||||
postctl "github.com/kirsle/blog/internal/controllers/posts"
|
||||
questionsctl "github.com/kirsle/blog/internal/controllers/questions"
|
||||
"github.com/kirsle/blog/internal/controllers/setup"
|
||||
"github.com/kirsle/blog/internal/log"
|
||||
"github.com/kirsle/blog/internal/markdown"
|
||||
|
@ -28,6 +29,7 @@ import (
|
|||
"github.com/kirsle/blog/jsondb/caches/redis"
|
||||
"github.com/kirsle/blog/models/comments"
|
||||
"github.com/kirsle/blog/models/posts"
|
||||
"github.com/kirsle/blog/models/questions"
|
||||
"github.com/kirsle/blog/models/settings"
|
||||
"github.com/kirsle/blog/models/users"
|
||||
"github.com/shurcooL/github_flavored_markdown/gfmstyle"
|
||||
|
@ -104,6 +106,7 @@ func (b *Blog) Configure() {
|
|||
posts.DB = b.jsonDB
|
||||
users.DB = b.jsonDB
|
||||
comments.DB = b.jsonDB
|
||||
questions.UseDB(b.db)
|
||||
|
||||
// Redis cache?
|
||||
if config.Redis.Enabled {
|
||||
|
@ -136,6 +139,7 @@ func (b *Blog) SetupHTTP() {
|
|||
contact.Register(r)
|
||||
postctl.Register(r, b.MustLogin)
|
||||
commentctl.Register(r)
|
||||
questionsctl.Register(r)
|
||||
|
||||
// GitHub Flavored Markdown CSS.
|
||||
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
||||
|
|
137
internal/controllers/questions/questions.go
Normal file
137
internal/controllers/questions/questions.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package questions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/kirsle/blog/internal/log"
|
||||
"github.com/kirsle/blog/internal/mail"
|
||||
"github.com/kirsle/blog/internal/markdown"
|
||||
"github.com/kirsle/blog/internal/render"
|
||||
"github.com/kirsle/blog/internal/responses"
|
||||
"github.com/kirsle/blog/internal/sessions"
|
||||
"github.com/kirsle/blog/models/comments"
|
||||
"github.com/kirsle/blog/models/questions"
|
||||
"github.com/kirsle/blog/models/settings"
|
||||
"github.com/kirsle/blog/models/users"
|
||||
)
|
||||
|
||||
var badRequest func(http.ResponseWriter, *http.Request, string)
|
||||
|
||||
// Register the comment routes to the app.
|
||||
func Register(r *mux.Router) {
|
||||
badRequest = responses.BadRequest
|
||||
|
||||
r.HandleFunc("/ask", questionsHandler)
|
||||
}
|
||||
|
||||
// CommentMeta is the template variables for comment threads.
|
||||
type CommentMeta struct {
|
||||
NewComment comments.Comment
|
||||
ID string
|
||||
OriginURL string // URL where original comment thread appeared
|
||||
Subject string // email subject
|
||||
Thread *comments.Thread
|
||||
Authors map[int]*users.User
|
||||
CSRF string
|
||||
}
|
||||
|
||||
func questionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
submit := r.FormValue("submit")
|
||||
|
||||
// Share their name and email with the commenting system.
|
||||
session := sessions.Get(r)
|
||||
name, _ := session.Values["c.name"].(string)
|
||||
email, _ := session.Values["c.email"].(string)
|
||||
|
||||
Q := questions.New()
|
||||
Q.Name = name
|
||||
Q.Email = email
|
||||
|
||||
cfg, err := settings.Load()
|
||||
if err != nil {
|
||||
responses.Error(w, r, "Error loading site configuration!")
|
||||
return
|
||||
}
|
||||
|
||||
v := map[string]interface{}{}
|
||||
|
||||
// Previewing, deleting, or posting?
|
||||
if r.Method == http.MethodPost {
|
||||
Q.ParseForm(r)
|
||||
log.Info("Q: %+v", Q)
|
||||
|
||||
switch submit {
|
||||
case "ask":
|
||||
if err := Q.Validate(); err != nil {
|
||||
log.Debug("Validation error on question form: %s", err.Error())
|
||||
v["Error"] = err
|
||||
} else {
|
||||
// Cache their name and email in their session.
|
||||
session.Values["c.name"] = Q.Name
|
||||
session.Values["c.email"] = Q.Email
|
||||
session.Save(r, w)
|
||||
|
||||
// Append their comment.
|
||||
err := Q.Save()
|
||||
if err != nil {
|
||||
log.Error("Error saving new question: %s", err.Error())
|
||||
responses.FlashAndRedirect(w, r, "/ask", "Error saving question: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Email the site admin.
|
||||
subject := fmt.Sprintf("Ask Me Anything (%s) from %s", cfg.Site.Title, Q.Name)
|
||||
log.Info("Emailing site admin about this question")
|
||||
go mail.SendEmail(mail.Email{
|
||||
To: cfg.Site.AdminEmail,
|
||||
Admin: true,
|
||||
ReplyTo: Q.Email,
|
||||
Subject: subject,
|
||||
Template: ".email/generic.gohtml",
|
||||
Data: map[string]interface{}{
|
||||
"Subject": subject,
|
||||
"Message": template.HTML(
|
||||
markdown.RenderMarkdown(Q.Question) + "\n\n" +
|
||||
"Answer this at " + strings.Trim(cfg.Site.URL, "/") + "/ask",
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
// Log it to disk, too.
|
||||
fh, err := os.OpenFile(filepath.Join(*render.UserRoot, ".questions.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
responses.Flash(w, r, "Error logging the message to disk: %s", err)
|
||||
} else {
|
||||
fh.WriteString(fmt.Sprintf(
|
||||
"Date: %s\nName: %s\nEmail: %s\n\n%s\n\n--------------------\n\n",
|
||||
time.Now().Format(time.UnixDate),
|
||||
Q.Name,
|
||||
Q.Email,
|
||||
Q.Question,
|
||||
))
|
||||
fh.Close()
|
||||
}
|
||||
|
||||
log.Info("Recorded question from %s: %s", Q.Name, Q.Question)
|
||||
responses.FlashAndRedirect(w, r, "/ask", "Your question has been recorded!")
|
||||
return
|
||||
}
|
||||
case "answer":
|
||||
case "delete":
|
||||
default:
|
||||
responses.FlashAndRedirect(w, r, "/ask", "Unknown submit action.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
v["Q"] = Q
|
||||
|
||||
render.Template(w, r, "questions.gohtml", v)
|
||||
}
|
16
internal/controllers/questions/questions_doc.go
Normal file
16
internal/controllers/questions/questions_doc.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
Package questions implements the "Ask Me Anything" feature.
|
||||
|
||||
Routes
|
||||
|
||||
/ask Ask Me Anything
|
||||
|
||||
Related Models
|
||||
|
||||
questions
|
||||
|
||||
Description
|
||||
|
||||
Pending description.
|
||||
*/
|
||||
package questions
|
11
models/questions/enums.go
Normal file
11
models/questions/enums.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package questions
|
||||
|
||||
// Status of a Question.
|
||||
type Status string
|
||||
|
||||
// Status options.
|
||||
const (
|
||||
Pending = "pending"
|
||||
Answered = "answered"
|
||||
Deleted = "deleted"
|
||||
)
|
126
models/questions/questions.go
Normal file
126
models/questions/questions.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package questions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/kirsle/golog"
|
||||
)
|
||||
|
||||
// DB is a reference to the parent app's gorm DB.
|
||||
var DB *gorm.DB
|
||||
|
||||
// UseDB registers the DB from the root app.
|
||||
func UseDB(db *gorm.DB) {
|
||||
DB = db
|
||||
DB.AutoMigrate(&Question{})
|
||||
}
|
||||
|
||||
var log *golog.Logger
|
||||
|
||||
func init() {
|
||||
log = golog.GetLogger("blog")
|
||||
}
|
||||
|
||||
// Question is a question asked of the blog owner.
|
||||
type Question struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Question string `json:"question"`
|
||||
Status Status `json:"status"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
// New creates a blank Question with sensible defaults.
|
||||
func New() *Question {
|
||||
return &Question{
|
||||
Status: Pending,
|
||||
}
|
||||
}
|
||||
|
||||
// All returns all the Questions.
|
||||
func All() ([]*Question, error) {
|
||||
result := []*Question{}
|
||||
err := DB.Order("start_time desc").Find(&result).Error
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ParseForm populates the Question from form values.
|
||||
func (ev *Question) ParseForm(r *http.Request) {
|
||||
id, _ := strconv.Atoi(r.FormValue("id"))
|
||||
|
||||
ev.ID = id
|
||||
ev.Name = r.FormValue("name")
|
||||
ev.Email = r.FormValue("email")
|
||||
ev.Question = r.FormValue("question")
|
||||
}
|
||||
|
||||
// parseDateTime parses separate date + time fields into a single time.Time.
|
||||
func parseDateTime(r *http.Request, dateField, timeField string) (time.Time, error) {
|
||||
dateValue := r.FormValue(dateField)
|
||||
timeValue := r.FormValue(timeField)
|
||||
|
||||
if dateValue != "" && timeValue != "" {
|
||||
datetime, err := time.Parse("2006-01-02 15:04", dateValue+" "+timeValue)
|
||||
return datetime, err
|
||||
} else if dateValue != "" {
|
||||
datetime, err := time.Parse("2006-01-02", dateValue)
|
||||
return datetime, err
|
||||
} else {
|
||||
return time.Time{}, errors.New("no date/times given")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate makes sure the required fields are all present.
|
||||
func (ev *Question) Validate() error {
|
||||
if ev.Question == "" {
|
||||
return errors.New("question is required")
|
||||
}
|
||||
if ev.Email != "" {
|
||||
if _, err := mail.ParseAddress(ev.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load an Question by its ID.
|
||||
func Load(id int) (*Question, error) {
|
||||
ev := &Question{}
|
||||
err := DB.First(ev, id).Error
|
||||
return ev, err
|
||||
}
|
||||
|
||||
// Save the Question.
|
||||
func (ev *Question) Save() error {
|
||||
if ev.Name == "" {
|
||||
ev.Name = "Anonymous"
|
||||
}
|
||||
|
||||
// Dates & times.
|
||||
if ev.Created.IsZero() {
|
||||
ev.Created = time.Now().UTC()
|
||||
}
|
||||
if ev.Updated.IsZero() {
|
||||
ev.Updated = ev.Created
|
||||
}
|
||||
|
||||
// Write the Question.
|
||||
return DB.Save(&ev).Error
|
||||
}
|
||||
|
||||
// Delete an Question.
|
||||
func (ev *Question) Delete() error {
|
||||
if ev.ID == 0 {
|
||||
return errors.New("Question has no ID")
|
||||
}
|
||||
|
||||
// Delete the DB files.
|
||||
return DB.Delete(ev).Error
|
||||
}
|
39
root/.email/generic.gohtml
Normal file
39
root/.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>
|
67
root/questions.gohtml
Normal file
67
root/questions.gohtml
Normal file
|
@ -0,0 +1,67 @@
|
|||
{{ define "title" }}Ask Me Anything{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>Ask Me Anything</h1>
|
||||
|
||||
{{ if .Data.Error }}
|
||||
<div class="alert alert-danger">
|
||||
Error: {{ .Data.Error }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<form name="askme" method="POST" action="/ask">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<input type="hidden" name="_csrf" value="{{ .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="{{ .Data.Q.Name }}" placeholder="Anonymous">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-12 col-md-2">
|
||||
<label class="col-form-label" for="email">Email:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-10">
|
||||
<div><input type="email" class="form-control" id="email" name="email" value="{{ .Data.Q.Email }}" placeholder="name@example.com"></div>
|
||||
<small>
|
||||
Optional. You will receive a one-time e-mail when I answer your question and no spam.
|
||||
</small>
|
||||
</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">{{ .Data.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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ if .LoggedIn }}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
Pending Questions
|
||||
</div>
|
||||
<div class="card-body">
|
||||
xxx
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</form>
|
||||
|
||||
{{ end }}
|
Loading…
Reference in New Issue
Block a user