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) {
|
func TestEducationIndex(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildEducationIndex(), RegexEducationIndex, nil)
|
AssertRegexMatch(t, BuildEducationIndex(), RegexEducationIndex, nil)
|
||||||
|
AssertRegexNoMatch(t, BuildEducationArticle("foo"), RegexEducationIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEducationGlossary(t *testing.T) {
|
func TestEducationGlossary(t *testing.T) {
|
||||||
|
|
|
@ -463,6 +463,12 @@ func BuildEducationArticle(slug string) string {
|
||||||
return Url(fmt.Sprintf("/education/%s", slug), nil)
|
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$`)
|
var RegexEducationArticleEdit = regexp.MustCompile(`^/education/(?P<slug>[^/]+)/edit$`)
|
||||||
|
|
||||||
func BuildEducationArticleEdit(slug string) string {
|
func BuildEducationArticleEdit(slug string) string {
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (m AddEducationResources) Up(ctx context.Context, tx pgx.Tx) error {
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
type INT NOT NULL,
|
type INT NOT NULL,
|
||||||
published BOOLEAN NOT NULL DEFAULT FALSE,
|
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
|
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 date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
ADD content_raw TEXT NOT NULL,
|
ADD content_raw TEXT NOT NULL,
|
||||||
ADD content_html 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 {
|
if err != nil {
|
||||||
|
|
|
@ -19,15 +19,15 @@
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="content-block ph3 ph0-ns">
|
<div class="content-block ph3 ph0-ns">
|
||||||
{{ if not .CanEditTitle }}
|
{{ if not .CanEditPostTitle }}
|
||||||
<h2>{{ .Title }}</h2>
|
<h2>{{ .PostTitle }}</h2>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="flex flex-column flex-row-ns">
|
<div class="flex flex-column flex-row-ns">
|
||||||
<form id="form" action="{{ .SubmitUrl }}" method="post" class="flex-fair-ns overflow-hidden">
|
<form id="form" action="{{ .SubmitUrl }}" method="post" class="flex-fair-ns overflow-hidden">
|
||||||
{{ csrftoken .Session }}
|
{{ csrftoken .Session }}
|
||||||
|
|
||||||
{{ if .CanEditTitle }}
|
{{ if .CanEditPostTitle }}
|
||||||
<input id="title" class="b w-100 mb1" name="title" type="text" placeholder="Post title..." value="{{ .Title }}" />
|
<input id="title" class="b w-100 mb1" name="title" type="text" placeholder="Post title..." value="{{ .PostTitle }}" />
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{/* TODO: Reintroduce the toolbar in a way that makes sense for Markdown */}}
|
{{/* TODO: Reintroduce the toolbar in a way that makes sense for Markdown */}}
|
||||||
{{/*
|
{{/*
|
||||||
|
@ -79,6 +79,26 @@
|
||||||
{{ template "forum_post_standalone.html" . }}
|
{{ template "forum_post_standalone.html" . }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ 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>
|
</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-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>
|
<div id="preview" class="post-content"></div>
|
||||||
|
|
|
@ -2,12 +2,16 @@ package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
"git.handmade.network/hmn/hmn/src/hmndata"
|
"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/models"
|
||||||
|
"git.handmade.network/hmn/hmn/src/parsing"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,12 +119,120 @@ func EducationAdmin(c *RequestContext) ResponseData {
|
||||||
return res
|
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 {
|
func EducationArticleEdit(c *RequestContext) ResponseData {
|
||||||
// TODO
|
// TODO
|
||||||
panic("not implemented yet")
|
panic("not implemented yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EducationArticleEditSubmit(c *RequestContext) ResponseData {
|
||||||
|
// TODO
|
||||||
|
panic("not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
func EducationArticleDelete(c *RequestContext) ResponseData {
|
func EducationArticleDelete(c *RequestContext) ResponseData {
|
||||||
// TODO
|
// TODO
|
||||||
panic("not implemented yet")
|
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
|
// The following are filled out automatically by the
|
||||||
// getEditorDataFor* functions.
|
// getEditorDataFor* functions.
|
||||||
Title string
|
PostTitle string
|
||||||
CanEditTitle bool
|
CanEditPostTitle bool
|
||||||
IsEditing bool
|
IsEditing bool
|
||||||
EditInitialContents string
|
EditInitialContents string
|
||||||
PostReplyingTo *templates.Post
|
PostReplyingTo *templates.Post
|
||||||
|
ShowEduOptions bool
|
||||||
|
|
||||||
MaxFileSize int
|
MaxFileSize int
|
||||||
UploadUrl string
|
UploadUrl string
|
||||||
|
@ -55,15 +56,15 @@ type editorData struct {
|
||||||
|
|
||||||
func getEditorDataForNew(urlContext *hmnurl.UrlContext, currentUser *models.User, baseData templates.BaseData, replyPost *templates.Post) editorData {
|
func getEditorDataForNew(urlContext *hmnurl.UrlContext, currentUser *models.User, baseData templates.BaseData, replyPost *templates.Post) editorData {
|
||||||
result := editorData{
|
result := editorData{
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
CanEditTitle: replyPost == nil,
|
CanEditPostTitle: replyPost == nil,
|
||||||
PostReplyingTo: replyPost,
|
PostReplyingTo: replyPost,
|
||||||
MaxFileSize: AssetMaxSize(currentUser),
|
MaxFileSize: AssetMaxSize(currentUser),
|
||||||
UploadUrl: urlContext.BuildAssetUpload(),
|
UploadUrl: urlContext.BuildAssetUpload(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if replyPost != nil {
|
if replyPost != nil {
|
||||||
result.Title = "Replying to post"
|
result.PostTitle = "Replying to post"
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
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 {
|
func getEditorDataForEdit(urlContext *hmnurl.UrlContext, currentUser *models.User, baseData templates.BaseData, p hmndata.PostAndStuff) editorData {
|
||||||
return editorData{
|
return editorData{
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
Title: p.Thread.Title,
|
PostTitle: p.Thread.Title,
|
||||||
CanEditTitle: p.Thread.FirstID == p.Post.ID,
|
CanEditPostTitle: p.Thread.FirstID == p.Post.ID,
|
||||||
IsEditing: true,
|
IsEditing: true,
|
||||||
EditInitialContents: p.CurrentVersion.TextRaw,
|
EditInitialContents: p.CurrentVersion.TextRaw,
|
||||||
MaxFileSize: AssetMaxSize(currentUser),
|
MaxFileSize: AssetMaxSize(currentUser),
|
||||||
|
|
|
@ -120,9 +120,13 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
||||||
hmnOnly.GET(hmnurl.RegexEducationIndex, EducationIndex)
|
hmnOnly.GET(hmnurl.RegexEducationIndex, EducationIndex)
|
||||||
hmnOnly.GET(hmnurl.RegexEducationGlossary, EducationGlossary)
|
hmnOnly.GET(hmnurl.RegexEducationGlossary, EducationGlossary)
|
||||||
hmnOnly.GET(hmnurl.RegexEducationAdmin, educationAuthorsOnly(EducationAdmin))
|
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.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.GET(hmnurl.RegexEducationArticleEdit, educationAuthorsOnly(EducationArticleEdit))
|
||||||
|
hmnOnly.POST(hmnurl.RegexEducationArticleEdit, educationAuthorsOnly(EducationArticleEditSubmit))
|
||||||
hmnOnly.GET(hmnurl.RegexEducationArticleDelete, educationAuthorsOnly(EducationArticleDelete))
|
hmnOnly.GET(hmnurl.RegexEducationArticleDelete, educationAuthorsOnly(EducationArticleDelete))
|
||||||
|
hmnOnly.POST(hmnurl.RegexEducationArticleDelete, educationAuthorsOnly(EducationArticleDeleteSubmit))
|
||||||
|
|
||||||
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
|
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue