Feature: Ask Me Anything
Implements a Tumblr-style "Ask Me Anything" feature to the blog, with a bit of automation and niceness: * The route is at `/ask` * User can leave their name (Anonymous), email address (if they want to be notified when you answer) and ask their question. * The blog owner is notified about the question via email. * The owner sees recent pending questions on the `/ask` page. * Answering a question creates a blog entry and (if the asker left their email) notifies the asker to check the blog at the permalink-to-be for the new post. Along with this feature, some changes to the blog application in general: * Added support for SQL databases in addition to the JsonDB system previously in use for blogs, users, comments, etc. * Default uses a SQLite DB at $root/.private/database.sqlite * The "Ask Me Anything" feature uses SQLite models instead of JSON. * Restructure the code layout: * Rename the /internal/ package path to /src/ * Begin to consolidate models into /src/models
This commit is contained in:
parent
9b938ccff3
commit
1821ef60d4
22
blog.go
22
blog.go
|
@ -9,6 +9,14 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/kirsle/blog/jsondb"
|
||||||
|
"github.com/kirsle/blog/jsondb/caches"
|
||||||
|
"github.com/kirsle/blog/jsondb/caches/null"
|
||||||
|
"github.com/kirsle/blog/jsondb/caches/redis"
|
||||||
|
"github.com/kirsle/blog/models/comments"
|
||||||
|
"github.com/kirsle/blog/models/posts"
|
||||||
|
"github.com/kirsle/blog/models/settings"
|
||||||
|
"github.com/kirsle/blog/models/users"
|
||||||
"github.com/kirsle/blog/src/controllers/admin"
|
"github.com/kirsle/blog/src/controllers/admin"
|
||||||
"github.com/kirsle/blog/src/controllers/authctl"
|
"github.com/kirsle/blog/src/controllers/authctl"
|
||||||
commentctl "github.com/kirsle/blog/src/controllers/comments"
|
commentctl "github.com/kirsle/blog/src/controllers/comments"
|
||||||
|
@ -20,18 +28,10 @@ import (
|
||||||
"github.com/kirsle/blog/src/markdown"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
"github.com/kirsle/blog/src/middleware"
|
"github.com/kirsle/blog/src/middleware"
|
||||||
"github.com/kirsle/blog/src/middleware/auth"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
|
"github.com/kirsle/blog/src/models"
|
||||||
"github.com/kirsle/blog/src/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/src/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/src/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
"github.com/kirsle/blog/jsondb"
|
|
||||||
"github.com/kirsle/blog/jsondb/caches"
|
|
||||||
"github.com/kirsle/blog/jsondb/caches/null"
|
|
||||||
"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"
|
"github.com/shurcooL/github_flavored_markdown/gfmstyle"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
@ -106,7 +106,7 @@ func (b *Blog) Configure() {
|
||||||
posts.DB = b.jsonDB
|
posts.DB = b.jsonDB
|
||||||
users.DB = b.jsonDB
|
users.DB = b.jsonDB
|
||||||
comments.DB = b.jsonDB
|
comments.DB = b.jsonDB
|
||||||
questions.UseDB(b.db)
|
models.UseDB(b.db)
|
||||||
|
|
||||||
// Redis cache?
|
// Redis cache?
|
||||||
if config.Redis.Enabled {
|
if config.Redis.Enabled {
|
||||||
|
@ -139,7 +139,7 @@ func (b *Blog) SetupHTTP() {
|
||||||
contact.Register(r)
|
contact.Register(r)
|
||||||
postctl.Register(r, b.MustLogin)
|
postctl.Register(r, b.MustLogin)
|
||||||
commentctl.Register(r)
|
commentctl.Register(r)
|
||||||
questionsctl.Register(r)
|
questionsctl.Register(r, b.MustLogin)
|
||||||
|
|
||||||
// GitHub Flavored Markdown CSS.
|
// GitHub Flavored Markdown CSS.
|
||||||
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
||||||
|
|
|
@ -8,60 +8,100 @@
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<form name="askme" method="POST" action="/ask">
|
<div class="card">
|
||||||
<div class="card">
|
<div class="card-body">
|
||||||
<div class="card-body">
|
<form name="askme" method="POST" action="/ask">
|
||||||
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-12 col-md-2">
|
<div class="col-12 col-md-2">
|
||||||
<label class="col-form-label" for="name">Name:</label>
|
<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>
|
||||||
<div class="form-group row">
|
<div class="col-12 col-md-10">
|
||||||
<div class="col-12 col-md-2">
|
<input type="text" class="form-control" id="name" name="name" value="{{ .Data.Q.Name }}" placeholder="Anonymous">
|
||||||
<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>
|
</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>
|
||||||
|
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ if .LoggedIn }}
|
{{ if .LoggedIn }}
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Pending Questions
|
Pending Questions
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
xxx
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
<div class="card-body">
|
||||||
</form>
|
{{ if not .Data.Pending }}
|
||||||
|
<em>There are no pending questions.</em>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ range .Data.Pending }}
|
||||||
|
<p>
|
||||||
|
<strong>{{ .Name }}</strong> {{ if .Email }}(with email){{ end }} asks:<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
<em>{{ .Created.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">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ $.CSRF }}">
|
||||||
|
<input type="hidden" name="id" value="{{ .ID }}">
|
||||||
|
<textarea cols="80" rows="4"
|
||||||
|
class="form-control"
|
||||||
|
name="answer"
|
||||||
|
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 }}
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -6,51 +6,46 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/models/posts"
|
||||||
|
"github.com/kirsle/blog/models/settings"
|
||||||
"github.com/kirsle/blog/src/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/src/mail"
|
"github.com/kirsle/blog/src/mail"
|
||||||
"github.com/kirsle/blog/src/markdown"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
|
"github.com/kirsle/blog/src/models"
|
||||||
"github.com/kirsle/blog/src/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/src/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/src/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
"github.com/kirsle/blog/models/comments"
|
"github.com/urfave/negroni"
|
||||||
"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)
|
var badRequest func(http.ResponseWriter, *http.Request, string)
|
||||||
|
|
||||||
// Register the comment routes to the app.
|
// Register the comment routes to the app.
|
||||||
func Register(r *mux.Router) {
|
func Register(r *mux.Router, loginError http.HandlerFunc) {
|
||||||
badRequest = responses.BadRequest
|
badRequest = responses.BadRequest
|
||||||
|
|
||||||
r.HandleFunc("/ask", questionsHandler)
|
r.HandleFunc("/ask", questionsHandler)
|
||||||
}
|
r.Handle("/ask/answer",
|
||||||
|
negroni.New(
|
||||||
// CommentMeta is the template variables for comment threads.
|
negroni.HandlerFunc(auth.LoginRequired(loginError)),
|
||||||
type CommentMeta struct {
|
negroni.WrapFunc(answerHandler),
|
||||||
NewComment comments.Comment
|
),
|
||||||
ID string
|
).Methods(http.MethodPost)
|
||||||
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) {
|
func questionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
submit := r.FormValue("submit")
|
|
||||||
|
|
||||||
// Share their name and email with the commenting system.
|
// Share their name and email with the commenting system.
|
||||||
session := sessions.Get(r)
|
session := sessions.Get(r)
|
||||||
name, _ := session.Values["c.name"].(string)
|
name, _ := session.Values["c.name"].(string)
|
||||||
email, _ := session.Values["c.email"].(string)
|
email, _ := session.Values["c.email"].(string)
|
||||||
|
|
||||||
Q := questions.New()
|
Q := models.NewQuestion()
|
||||||
Q.Name = name
|
Q.Name = name
|
||||||
Q.Email = email
|
Q.Email = email
|
||||||
|
|
||||||
|
@ -67,71 +62,165 @@ func questionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
Q.ParseForm(r)
|
Q.ParseForm(r)
|
||||||
log.Info("Q: %+v", Q)
|
log.Info("Q: %+v", Q)
|
||||||
|
|
||||||
switch submit {
|
if err := Q.Validate(); err != nil {
|
||||||
case "ask":
|
log.Debug("Validation error on question form: %s", err.Error())
|
||||||
if err := Q.Validate(); err != nil {
|
v["Error"] = err
|
||||||
log.Debug("Validation error on question form: %s", err.Error())
|
} else {
|
||||||
v["Error"] = err
|
// Cache their name and email in their session.
|
||||||
} else {
|
session.Values["c.name"] = Q.Name
|
||||||
// Cache their name and email in their session.
|
session.Values["c.email"] = Q.Email
|
||||||
session.Values["c.name"] = Q.Name
|
session.Save(r, w)
|
||||||
session.Values["c.email"] = Q.Email
|
|
||||||
session.Save(r, w)
|
|
||||||
|
|
||||||
// Append their comment.
|
// Append their comment.
|
||||||
err := Q.Save()
|
err := Q.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error saving new question: %s", err.Error())
|
log.Error("Error saving new question: %s", err.Error())
|
||||||
responses.FlashAndRedirect(w, r, "/ask", "Error saving question: %s", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
case "answer":
|
|
||||||
case "delete":
|
// Email the site admin.
|
||||||
default:
|
subject := fmt.Sprintf("Ask Me Anything (%s) from %s", cfg.Site.Title, Q.Name)
|
||||||
responses.FlashAndRedirect(w, r, "/ask", "Unknown submit action.")
|
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\nAnswer 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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v["Q"] = Q
|
v["Q"] = Q
|
||||||
|
|
||||||
|
// Load the pending questions.
|
||||||
|
pending, err := models.PendingQuestions(0, 20)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
||||||
|
v["Pending"] = pending
|
||||||
|
|
||||||
render.Template(w, r, "questions.gohtml", v)
|
render.Template(w, r, "questions.gohtml", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func answerHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
submit := r.FormValue("submit")
|
||||||
|
|
||||||
|
cfg, err := settings.Load()
|
||||||
|
if err != nil {
|
||||||
|
responses.Error(w, r, "Error loading site configuration!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type answerForm struct {
|
||||||
|
ID int
|
||||||
|
Answer string
|
||||||
|
Submit string
|
||||||
|
}
|
||||||
|
id, _ := strconv.Atoi(r.FormValue("id"))
|
||||||
|
form := answerForm{
|
||||||
|
ID: id,
|
||||||
|
Answer: r.FormValue("answer"),
|
||||||
|
Submit: r.FormValue("submit"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the question.
|
||||||
|
Q, err := models.GetQuestion(form.ID)
|
||||||
|
if err != nil {
|
||||||
|
responses.FlashAndRedirect(w, r, "/ask",
|
||||||
|
fmt.Sprintf("Did not find question ID %d", form.ID),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch submit {
|
||||||
|
case "answer":
|
||||||
|
// Prepare a Markdown-themed blog post and go to the Preview page for it.
|
||||||
|
blog := posts.New()
|
||||||
|
blog.Title = "Ask"
|
||||||
|
blog.Tags = []string{"ask"}
|
||||||
|
blog.Fragment = fmt.Sprintf("ask-%s",
|
||||||
|
time.Now().Format("20060102150405"),
|
||||||
|
)
|
||||||
|
blog.Body = fmt.Sprintf(
|
||||||
|
"> **%s** asks:\n>\n> %s\n\n"+
|
||||||
|
"%s\n",
|
||||||
|
Q.Name,
|
||||||
|
strings.Replace(Q.Question, "\n", "> \n", 0),
|
||||||
|
form.Answer,
|
||||||
|
)
|
||||||
|
|
||||||
|
Q.Status = models.Answered
|
||||||
|
Q.Save()
|
||||||
|
|
||||||
|
// TODO: email the person who asked about the new URL.
|
||||||
|
if Q.Email != "" {
|
||||||
|
log.Info("Notifying user %s by email that the question is answered", Q.Email)
|
||||||
|
go mail.SendEmail(mail.Email{
|
||||||
|
To: Q.Email,
|
||||||
|
Subject: "Your question has been answered",
|
||||||
|
Template: ".email/generic.gohtml",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Subject": "Your question has been answered",
|
||||||
|
"Message": template.HTML(
|
||||||
|
markdown.RenderMarkdown(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Hello, %s\n\n"+
|
||||||
|
"Your recent question on %s has been answered. To "+
|
||||||
|
"view the answer, please visit the following link:\n\n"+
|
||||||
|
"%s/%s",
|
||||||
|
Q.Name,
|
||||||
|
cfg.Site.Title,
|
||||||
|
cfg.Site.URL,
|
||||||
|
blog.Fragment,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Template(w, r, "blog/edit", map[string]interface{}{
|
||||||
|
"preview": template.HTML(markdown.RenderTrustedMarkdown(blog.Body)),
|
||||||
|
"post": blog,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case "delete":
|
||||||
|
Q.Status = models.Deleted
|
||||||
|
Q.Save()
|
||||||
|
responses.FlashAndRedirect(w, r, "/ask", "Question deleted.")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
responses.FlashAndRedirect(w, r, "/ask", "Unknown submit action.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
21
src/models/models.go
Normal file
21
src/models/models.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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")
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package questions
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -6,26 +6,8 @@ import (
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"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.
|
// Question is a question asked of the blog owner.
|
||||||
type Question struct {
|
type Question struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
|
@ -37,17 +19,34 @@ type Question struct {
|
||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a blank Question with sensible defaults.
|
// NewQuestion creates a blank Question with sensible defaults.
|
||||||
func New() *Question {
|
func NewQuestion() *Question {
|
||||||
return &Question{
|
return &Question{
|
||||||
Status: Pending,
|
Status: Pending,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All returns all the Questions.
|
// GetQuestion by its ID.
|
||||||
func All() ([]*Question, error) {
|
func GetQuestion(id int) (*Question, error) {
|
||||||
|
result := &Question{}
|
||||||
|
err := DB.First(&result, id).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllQuestions returns all the Questions.
|
||||||
|
func AllQuestions() ([]*Question, error) {
|
||||||
result := []*Question{}
|
result := []*Question{}
|
||||||
err := DB.Order("start_time desc").Find(&result).Error
|
err := DB.Order("created desc").Find(&result).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PendingQuestions returns pending questions in order of recency.
|
||||||
|
func PendingQuestions(offset, limit int) ([]*Question, error) {
|
||||||
|
result := []*Question{}
|
||||||
|
err := DB.Where("status = ?", Pending).
|
||||||
|
Offset(offset).Limit(limit).
|
||||||
|
Order("created desc").
|
||||||
|
Find(&result).Error
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package questions
|
package models
|
||||||
|
|
||||||
// Status of a Question.
|
// Status of a Question.
|
||||||
type Status string
|
type Status string
|
Loading…
Reference in New Issue
Block a user