Base creation of education articles

This commit is contained in:
Ben Visness 2022-07-16 13:24:34 -05:00
parent 3067b3fc3e
commit a508c4bf6e
7 changed files with 160 additions and 16 deletions

View File

@ -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) {

View File

@ -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 {

View File

@ -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 {

View File

@ -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>

View File

@ -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
}

View File

@ -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),

View File

@ -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))