Base creation of education articles
This commit is contained in:
parent
3067b3fc3e
commit
a508c4bf6e
|
@ -187,6 +187,7 @@ func TestFishbowl(t *testing.T) {
|
|||
|
||||
func TestEducationIndex(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEducationIndex(), RegexEducationIndex, nil)
|
||||
AssertRegexNoMatch(t, BuildEducationArticle("foo"), RegexEducationIndex)
|
||||
}
|
||||
|
||||
func TestEducationGlossary(t *testing.T) {
|
||||
|
|
|
@ -463,6 +463,12 @@ func BuildEducationArticle(slug string) string {
|
|||
return Url(fmt.Sprintf("/education/%s", slug), nil)
|
||||
}
|
||||
|
||||
var RegexEducationArticleNew = regexp.MustCompile(`^/education/new$`)
|
||||
|
||||
func BuildEducationArticleNew() string {
|
||||
return Url("/education/new", nil)
|
||||
}
|
||||
|
||||
var RegexEducationArticleEdit = regexp.MustCompile(`^/education/(?P<slug>[^/]+)/edit$`)
|
||||
|
||||
func BuildEducationArticleEdit(slug string) string {
|
||||
|
|
|
@ -41,7 +41,7 @@ func (m AddEducationResources) Up(ctx context.Context, tx pgx.Tx) error {
|
|||
description TEXT NOT NULL,
|
||||
type INT NOT NULL,
|
||||
published BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
current_version INT NOT NULL REFERENCES education_article_version (id)
|
||||
current_version INT NOT NULL REFERENCES education_article_version (id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
|
||||
ALTER TABLE education_article_version
|
||||
|
@ -49,7 +49,7 @@ func (m AddEducationResources) Up(ctx context.Context, tx pgx.Tx) error {
|
|||
ADD date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
ADD content_raw TEXT NOT NULL,
|
||||
ADD content_html TEXT NOT NULL,
|
||||
ADD editor_id INT REFERENCES hmn_user (id) ON DELETE SET NULL;
|
||||
ADD editor_id INT REFERENCES hmn_user (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;
|
||||
`,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
@ -19,15 +19,15 @@
|
|||
|
||||
{{ define "content" }}
|
||||
<div class="content-block ph3 ph0-ns">
|
||||
{{ if not .CanEditTitle }}
|
||||
<h2>{{ .Title }}</h2>
|
||||
{{ if not .CanEditPostTitle }}
|
||||
<h2>{{ .PostTitle }}</h2>
|
||||
{{ end }}
|
||||
<div class="flex flex-column flex-row-ns">
|
||||
<form id="form" action="{{ .SubmitUrl }}" method="post" class="flex-fair-ns overflow-hidden">
|
||||
{{ csrftoken .Session }}
|
||||
|
||||
{{ if .CanEditTitle }}
|
||||
<input id="title" class="b w-100 mb1" name="title" type="text" placeholder="Post title..." value="{{ .Title }}" />
|
||||
{{ if .CanEditPostTitle }}
|
||||
<input id="title" class="b w-100 mb1" name="title" type="text" placeholder="Post title..." value="{{ .PostTitle }}" />
|
||||
{{ end }}
|
||||
{{/* TODO: Reintroduce the toolbar in a way that makes sense for Markdown */}}
|
||||
{{/*
|
||||
|
@ -79,6 +79,26 @@
|
|||
{{ template "forum_post_standalone.html" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if .ShowEduOptions }}
|
||||
<div class="mb2">
|
||||
<label for="slug">Slug:</label>
|
||||
<input name="slug" maxlength="255" type="text" id="slug" required />
|
||||
</div>
|
||||
<div class="mb2">
|
||||
<label for="type">Type:</label>
|
||||
<select name="type" id="type">
|
||||
<option value="article">Article</option>
|
||||
<option value="glossary">Glossary Term</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb2">
|
||||
<label for="description">Description:</label>
|
||||
<div>
|
||||
<textarea name="description" id="slug" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</form>
|
||||
<div id="preview-container" class="post post-preview mathjax flex-fair-ns overflow-auto mv3 mv0-ns ml3-ns pa3 br3 bg--dim">
|
||||
<div id="preview" class="post-content"></div>
|
||||
|
|
|
@ -2,12 +2,16 @@ package website
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/parsing"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
)
|
||||
|
||||
|
@ -115,12 +119,120 @@ func EducationAdmin(c *RequestContext) ResponseData {
|
|||
return res
|
||||
}
|
||||
|
||||
func EducationArticleNew(c *RequestContext) ResponseData {
|
||||
type adminData struct {
|
||||
editorData
|
||||
}
|
||||
|
||||
tmpl := adminData{
|
||||
editorData: getEditorDataForEduArticle(c.UrlContext, c.CurrentUser, getBaseData(c, "New Education Article", nil), nil),
|
||||
}
|
||||
tmpl.editorData.SubmitUrl = hmnurl.BuildEducationArticleNew()
|
||||
|
||||
var res ResponseData
|
||||
res.MustWriteTemplate("editor.html", tmpl, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func EducationArticleNewSubmit(c *RequestContext) ResponseData {
|
||||
form, err := c.GetFormValues()
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
title := form.Get("title")
|
||||
slug := form.Get("slug")
|
||||
description := form.Get("description")
|
||||
var eduType models.EduArticleType
|
||||
switch form.Get("type") {
|
||||
case "article":
|
||||
eduType = models.EduArticleTypeArticle
|
||||
case "glossary":
|
||||
eduType = models.EduArticleTypeGlossary
|
||||
default:
|
||||
panic(fmt.Errorf("unknown education article type: %s", form.Get("type")))
|
||||
}
|
||||
published := form.Get("published") != ""
|
||||
body := form.Get("body")
|
||||
|
||||
// TODO: Education-specific Markdown
|
||||
bodyRendered := parsing.ParseMarkdown(body, parsing.ForumRealMarkdown)
|
||||
|
||||
tx, err := c.Conn.Begin(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer tx.Rollback(c)
|
||||
{
|
||||
// articleID := db.MustQueryOneScalar[int](c, tx,
|
||||
var articleID int
|
||||
err := tx.QueryRow(c,
|
||||
`
|
||||
INSERT INTO education_article (title, slug, description, published, type, current_version)
|
||||
VALUES ($1, $2, $3, $4, $5, -1)
|
||||
RETURNING id
|
||||
`,
|
||||
title, slug, description, published, eduType,
|
||||
).Scan(&articleID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
versionID := db.MustQueryOneScalar[int](c, tx,
|
||||
`
|
||||
INSERT INTO education_article_version (article_id, date, editor_id, content_raw, content_html)
|
||||
VALUES ($1, $2, $3, $4, $5 )
|
||||
RETURNING id
|
||||
`,
|
||||
articleID, time.Now(), c.CurrentUser.ID, body, bodyRendered,
|
||||
)
|
||||
tx.Exec(c,
|
||||
`UPDATE education_article SET current_version = $1 WHERE id = $2`,
|
||||
versionID, articleID,
|
||||
)
|
||||
}
|
||||
err = tx.Commit(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return c.Redirect(hmnurl.BuildEducationArticle(slug), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func EducationArticleEdit(c *RequestContext) ResponseData {
|
||||
// TODO
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
func EducationArticleEditSubmit(c *RequestContext) ResponseData {
|
||||
// TODO
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
func EducationArticleDelete(c *RequestContext) ResponseData {
|
||||
// TODO
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
func EducationArticleDeleteSubmit(c *RequestContext) ResponseData {
|
||||
// TODO
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
func getEditorDataForEduArticle(
|
||||
urlContext *hmnurl.UrlContext,
|
||||
currentUser *models.User,
|
||||
baseData templates.BaseData,
|
||||
article *models.EduArticle,
|
||||
) editorData {
|
||||
result := editorData{
|
||||
BaseData: baseData,
|
||||
SubmitLabel: "Submit",
|
||||
|
||||
CanEditPostTitle: true,
|
||||
MaxFileSize: AssetMaxSize(currentUser),
|
||||
UploadUrl: urlContext.BuildAssetUpload(),
|
||||
ShowEduOptions: true,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -43,11 +43,12 @@ type editorData struct {
|
|||
|
||||
// The following are filled out automatically by the
|
||||
// getEditorDataFor* functions.
|
||||
Title string
|
||||
CanEditTitle bool
|
||||
PostTitle string
|
||||
CanEditPostTitle bool
|
||||
IsEditing bool
|
||||
EditInitialContents string
|
||||
PostReplyingTo *templates.Post
|
||||
ShowEduOptions bool
|
||||
|
||||
MaxFileSize int
|
||||
UploadUrl string
|
||||
|
@ -55,15 +56,15 @@ type editorData struct {
|
|||
|
||||
func getEditorDataForNew(urlContext *hmnurl.UrlContext, currentUser *models.User, baseData templates.BaseData, replyPost *templates.Post) editorData {
|
||||
result := editorData{
|
||||
BaseData: baseData,
|
||||
CanEditTitle: replyPost == nil,
|
||||
PostReplyingTo: replyPost,
|
||||
MaxFileSize: AssetMaxSize(currentUser),
|
||||
UploadUrl: urlContext.BuildAssetUpload(),
|
||||
BaseData: baseData,
|
||||
CanEditPostTitle: replyPost == nil,
|
||||
PostReplyingTo: replyPost,
|
||||
MaxFileSize: AssetMaxSize(currentUser),
|
||||
UploadUrl: urlContext.BuildAssetUpload(),
|
||||
}
|
||||
|
||||
if replyPost != nil {
|
||||
result.Title = "Replying to post"
|
||||
result.PostTitle = "Replying to post"
|
||||
}
|
||||
|
||||
return result
|
||||
|
@ -72,8 +73,8 @@ func getEditorDataForNew(urlContext *hmnurl.UrlContext, currentUser *models.User
|
|||
func getEditorDataForEdit(urlContext *hmnurl.UrlContext, currentUser *models.User, baseData templates.BaseData, p hmndata.PostAndStuff) editorData {
|
||||
return editorData{
|
||||
BaseData: baseData,
|
||||
Title: p.Thread.Title,
|
||||
CanEditTitle: p.Thread.FirstID == p.Post.ID,
|
||||
PostTitle: p.Thread.Title,
|
||||
CanEditPostTitle: p.Thread.FirstID == p.Post.ID,
|
||||
IsEditing: true,
|
||||
EditInitialContents: p.CurrentVersion.TextRaw,
|
||||
MaxFileSize: AssetMaxSize(currentUser),
|
||||
|
|
|
@ -120,9 +120,13 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
|||
hmnOnly.GET(hmnurl.RegexEducationIndex, EducationIndex)
|
||||
hmnOnly.GET(hmnurl.RegexEducationGlossary, EducationGlossary)
|
||||
hmnOnly.GET(hmnurl.RegexEducationAdmin, educationAuthorsOnly(EducationAdmin))
|
||||
hmnOnly.GET(hmnurl.RegexEducationArticleNew, educationAuthorsOnly(EducationArticleNew))
|
||||
hmnOnly.POST(hmnurl.RegexEducationArticleNew, educationAuthorsOnly(EducationArticleNewSubmit))
|
||||
hmnOnly.GET(hmnurl.RegexEducationArticle, EducationArticle) // Article stuff must be last so `/glossary` and others do not match as an article slug
|
||||
hmnOnly.GET(hmnurl.RegexEducationArticleEdit, educationAuthorsOnly(EducationArticleEdit))
|
||||
hmnOnly.POST(hmnurl.RegexEducationArticleEdit, educationAuthorsOnly(EducationArticleEditSubmit))
|
||||
hmnOnly.GET(hmnurl.RegexEducationArticleDelete, educationAuthorsOnly(EducationArticleDelete))
|
||||
hmnOnly.POST(hmnurl.RegexEducationArticleDelete, educationAuthorsOnly(EducationArticleDeleteSubmit))
|
||||
|
||||
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
|
||||
|
||||
|
|
Loading…
Reference in New Issue