Add editing of education resources
This commit is contained in:
parent
a17ac21f49
commit
ccd63e7a2e
|
@ -29,7 +29,7 @@ func StartServer(ctx context.Context) jobs.Job {
|
||||||
return jobs.Noop()
|
return jobs.Noop()
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.Must0(os.MkdirAll(dir, fs.ModePerm))
|
utils.Must(os.MkdirAll(dir, fs.ModePerm))
|
||||||
|
|
||||||
s := server{
|
s := server{
|
||||||
log: logging.ExtractLogger(ctx).With().
|
log: logging.ExtractLogger(ctx).With().
|
||||||
|
@ -84,7 +84,7 @@ func (s *server) putObject(w http.ResponseWriter, r *http.Request) {
|
||||||
bucket, key := bucketKey(r)
|
bucket, key := bucketKey(r)
|
||||||
|
|
||||||
w.Header().Set("Location", fmt.Sprintf("/%s", bucket))
|
w.Header().Set("Location", fmt.Sprintf("/%s", bucket))
|
||||||
utils.Must0(os.MkdirAll(filepath.Join(dir, bucket), fs.ModePerm))
|
utils.Must(os.MkdirAll(filepath.Join(dir, bucket), fs.ModePerm))
|
||||||
if key != "" {
|
if key != "" {
|
||||||
file := utils.Must1(os.Create(filepath.Join(dir, bucket, key)))
|
file := utils.Must1(os.Create(filepath.Join(dir, bucket, key)))
|
||||||
io.Copy(file, r.Body)
|
io.Copy(file, r.Body)
|
||||||
|
|
|
@ -37,7 +37,7 @@ func (m AddEducationResources) Up(ctx context.Context, tx pgx.Tx) error {
|
||||||
CREATE TABLE education_article (
|
CREATE TABLE education_article (
|
||||||
id SERIAL NOT NULL PRIMARY KEY,
|
id SERIAL NOT NULL PRIMARY KEY,
|
||||||
title VARCHAR(255) NOT NULL,
|
title VARCHAR(255) NOT NULL,
|
||||||
slug VARCHAR(255) NOT NULL,
|
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||||
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,
|
||||||
|
|
|
@ -64,7 +64,7 @@ func BareMinimumSeed() *models.Project {
|
||||||
fmt.Println("Creating HMN project...")
|
fmt.Println("Creating HMN project...")
|
||||||
hmn := seedProject(ctx, tx, seedHMN, nil)
|
hmn := seedProject(ctx, tx, seedHMN, nil)
|
||||||
|
|
||||||
utils.Must0(tx.Commit(ctx))
|
utils.Must(tx.Commit(ctx))
|
||||||
|
|
||||||
return hmn
|
return hmn
|
||||||
}
|
}
|
||||||
|
@ -166,7 +166,7 @@ func SampleSeed() {
|
||||||
// Finally, set sequence numbers to things that won't conflict
|
// Finally, set sequence numbers to things that won't conflict
|
||||||
utils.Must1(tx.Exec(ctx, "SELECT setval('project_id_seq', 100, true);"))
|
utils.Must1(tx.Exec(ctx, "SELECT setval('project_id_seq', 100, true);"))
|
||||||
|
|
||||||
utils.Must0(tx.Commit(ctx))
|
utils.Must(tx.Commit(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedUser(ctx context.Context, conn db.ConnOrTx, input models.User) *models.User {
|
func seedUser(ctx context.Context, conn db.ConnOrTx, input models.User) *models.User {
|
||||||
|
@ -198,7 +198,7 @@ func seedUser(ctx context.Context, conn db.ConnOrTx, input models.User) *models.
|
||||||
utils.OrDefault(input.Name, randomName()), utils.OrDefault(input.Bio, lorem.Paragraph(0, 2)), utils.OrDefault(input.Blurb, lorem.Sentence(0, 14)), utils.OrDefault(input.Signature, lorem.Sentence(0, 16)),
|
utils.OrDefault(input.Name, randomName()), utils.OrDefault(input.Bio, lorem.Paragraph(0, 2)), utils.OrDefault(input.Blurb, lorem.Sentence(0, 14)), utils.OrDefault(input.Signature, lorem.Sentence(0, 16)),
|
||||||
input.ShowEmail,
|
input.ShowEmail,
|
||||||
)
|
)
|
||||||
utils.Must0(auth.SetPassword(ctx, conn, input.Username, "password"))
|
utils.Must(auth.SetPassword(ctx, conn, input.Username, "password"))
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type EduArticle struct {
|
type EduArticle struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
|
|
|
@ -492,7 +492,7 @@ func TagToTemplate(t *models.Tag) Tag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EducationArticleToTemplate(a *models.EduArticle, currentVersion *models.EduArticleVersion) EduArticle {
|
func EducationArticleToTemplate(a *models.EduArticle) EduArticle {
|
||||||
res := EduArticle{
|
res := EduArticle{
|
||||||
Title: a.Title,
|
Title: a.Title,
|
||||||
Slug: a.Slug,
|
Slug: a.Slug,
|
||||||
|
@ -505,9 +505,15 @@ func EducationArticleToTemplate(a *models.EduArticle, currentVersion *models.Edu
|
||||||
|
|
||||||
Content: "NO CONTENT HERE FOLKS YOU DID A BUG",
|
Content: "NO CONTENT HERE FOLKS YOU DID A BUG",
|
||||||
}
|
}
|
||||||
|
switch a.Type {
|
||||||
|
case models.EduArticleTypeArticle:
|
||||||
|
res.Type = "article"
|
||||||
|
case models.EduArticleTypeGlossary:
|
||||||
|
res.Type = "glossary"
|
||||||
|
}
|
||||||
|
|
||||||
if currentVersion != nil {
|
if a.CurrentVersion != nil {
|
||||||
res.Content = template.HTML(currentVersion.ContentHTML)
|
res.Content = template.HTML(a.CurrentVersion.ContentHTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
|
@ -81,21 +81,22 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .ShowEduOptions }}
|
{{ if .ShowEduOptions }}
|
||||||
|
{{/* Hope you have a .Article field! */}}
|
||||||
<div class="mb2">
|
<div class="mb2">
|
||||||
<label for="slug">Slug:</label>
|
<label for="slug">Slug:</label>
|
||||||
<input name="slug" maxlength="255" type="text" id="slug" required />
|
<input name="slug" maxlength="255" type="text" id="slug" required value="{{ .Article.Slug }}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb2">
|
<div class="mb2">
|
||||||
<label for="type">Type:</label>
|
<label for="type">Type:</label>
|
||||||
<select name="type" id="type">
|
<select name="type" id="type">
|
||||||
<option value="article">Article</option>
|
<option value="article" {{ if eq .Article.Type "article" }}selected{{ end }}>Article</option>
|
||||||
<option value="glossary">Glossary Term</option>
|
<option value="glossary" {{ if eq .Article.Type "glossary" }}selected{{ end }}>Glossary Term</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb2">
|
<div class="mb2">
|
||||||
<label for="description">Description:</label>
|
<label for="description">Description:</label>
|
||||||
<div>
|
<div>
|
||||||
<textarea name="description" id="slug" required></textarea>
|
<textarea name="description" id="slug" required>{{ .Article.Description }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{ template "base.html" . }}
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
{{ .Content }}
|
{{ .Article.Content }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -390,6 +390,7 @@ type EduArticle struct {
|
||||||
Slug string
|
Slug string
|
||||||
Description string
|
Description string
|
||||||
Published bool
|
Published bool
|
||||||
|
Type string
|
||||||
|
|
||||||
Url string
|
Url string
|
||||||
EditUrl string
|
EditUrl string
|
||||||
|
|
|
@ -22,7 +22,7 @@ func OrDefault[T comparable](v T, def T) T {
|
||||||
|
|
||||||
// Takes an (error) return and panics if there is an error.
|
// Takes an (error) return and panics if there is an error.
|
||||||
// Helps avoid `if err != nil` in scripts. Use sparingly in real code.
|
// Helps avoid `if err != nil` in scripts. Use sparingly in real code.
|
||||||
func Must0(err error) {
|
func Must(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
package website
|
package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
"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/parsing"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
|
"git.handmade.network/hmn/hmn/src/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func EducationIndex(c *RequestContext) ResponseData {
|
func EducationIndex(c *RequestContext) ResponseData {
|
||||||
|
@ -46,28 +48,10 @@ func EducationGlossary(c *RequestContext) ResponseData {
|
||||||
func EducationArticle(c *RequestContext) ResponseData {
|
func EducationArticle(c *RequestContext) ResponseData {
|
||||||
type articleData struct {
|
type articleData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
|
Article templates.EduArticle
|
||||||
Content template.HTML
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type articleResult struct {
|
article, err := fetchEduArticle(c, c.Conn, c.PathParams["slug"], models.EduArticleTypeArticle)
|
||||||
Article models.EduArticle `db:"a"`
|
|
||||||
CurrentVersion models.EduArticleVersion `db:"v"`
|
|
||||||
}
|
|
||||||
|
|
||||||
article, err := db.QueryOne[articleResult](c, c.Conn,
|
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
education_article AS a
|
|
||||||
JOIN education_article_version AS v ON a.current_version = v.id
|
|
||||||
WHERE
|
|
||||||
slug = $1
|
|
||||||
AND type = $2
|
|
||||||
`,
|
|
||||||
c.PathParams["slug"],
|
|
||||||
models.EduArticleTypeArticle,
|
|
||||||
)
|
|
||||||
if errors.Is(err, db.NotFound) {
|
if errors.Is(err, db.NotFound) {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -76,7 +60,7 @@ func EducationArticle(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
tmpl := articleData{
|
tmpl := articleData{
|
||||||
BaseData: getBaseData(c, "Handmade Education", nil),
|
BaseData: getBaseData(c, "Handmade Education", nil),
|
||||||
Content: template.HTML(article.CurrentVersion.ContentHTML),
|
Article: templates.EducationArticleToTemplate(article),
|
||||||
}
|
}
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
|
@ -93,7 +77,7 @@ func EducationAdmin(c *RequestContext) ResponseData {
|
||||||
var tmplArticles []templates.EduArticle
|
var tmplArticles []templates.EduArticle
|
||||||
var tmplGlossaryTerms []templates.EduArticle
|
var tmplGlossaryTerms []templates.EduArticle
|
||||||
for _, a := range articles {
|
for _, a := range articles {
|
||||||
tmpl := templates.EducationArticleToTemplate(&a.Article, &a.CurrentVersion)
|
tmpl := templates.EducationArticleToTemplate(&a.Article)
|
||||||
switch a.Article.Type {
|
switch a.Article.Type {
|
||||||
case models.EduArticleTypeArticle:
|
case models.EduArticleTypeArticle:
|
||||||
tmplArticles = append(tmplArticles, tmpl)
|
tmplArticles = append(tmplArticles, tmpl)
|
||||||
|
@ -122,6 +106,7 @@ func EducationAdmin(c *RequestContext) ResponseData {
|
||||||
func EducationArticleNew(c *RequestContext) ResponseData {
|
func EducationArticleNew(c *RequestContext) ResponseData {
|
||||||
type adminData struct {
|
type adminData struct {
|
||||||
editorData
|
editorData
|
||||||
|
Article map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := adminData{
|
tmpl := adminData{
|
||||||
|
@ -140,82 +125,69 @@ func EducationArticleNewSubmit(c *RequestContext) ResponseData {
|
||||||
return c.ErrorResponse(http.StatusBadRequest, err)
|
return c.ErrorResponse(http.StatusBadRequest, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var redirectUrl string
|
art, ver := getEduArticleFromForm(form)
|
||||||
|
|
||||||
title := form.Get("title")
|
dupe := 0 < db.MustQueryOneScalar[int](c, c.Conn,
|
||||||
slug := form.Get("slug")
|
`
|
||||||
description := form.Get("description")
|
SELECT COUNT(*) FROM education_article
|
||||||
var eduType models.EduArticleType
|
WHERE slug = $1
|
||||||
switch form.Get("type") {
|
`,
|
||||||
case "article":
|
art.Slug,
|
||||||
eduType = models.EduArticleTypeArticle
|
)
|
||||||
redirectUrl = hmnurl.BuildEducationArticle(slug)
|
if dupe {
|
||||||
case "glossary":
|
return c.RejectRequest("A resource already exists with that slug.")
|
||||||
eduType = models.EduArticleTypeGlossary
|
|
||||||
redirectUrl = hmnurl.BuildEducationGlossary(slug)
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
dupe := 0 < db.MustQueryOneScalar[int](c, tx,
|
|
||||||
`
|
|
||||||
SELECT COUNT(*) FROM education_article
|
|
||||||
WHERE slug = $1 AND type = $2
|
|
||||||
`,
|
|
||||||
slug, eduType,
|
|
||||||
)
|
|
||||||
if dupe {
|
|
||||||
return c.RejectRequest("A resource already exists with that slug and type.")
|
|
||||||
}
|
|
||||||
|
|
||||||
articleID := db.MustQueryOneScalar[int](c, tx,
|
|
||||||
`
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
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(redirectUrl, http.StatusSeeOther)
|
createEduArticle(c, art, ver)
|
||||||
|
|
||||||
|
res := c.Redirect(eduArticleURL(&art), http.StatusSeeOther)
|
||||||
|
res.AddFutureNotice("success", "Created new education article.")
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func EducationArticleEdit(c *RequestContext) ResponseData {
|
func EducationArticleEdit(c *RequestContext) ResponseData {
|
||||||
// TODO
|
type adminData struct {
|
||||||
panic("not implemented yet")
|
editorData
|
||||||
|
Article templates.EduArticle
|
||||||
|
}
|
||||||
|
|
||||||
|
article, err := fetchEduArticle(c, c.Conn, c.PathParams["slug"], 0)
|
||||||
|
if errors.Is(err, db.NotFound) {
|
||||||
|
return FourOhFour(c)
|
||||||
|
} else if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := adminData{
|
||||||
|
editorData: getEditorDataForEduArticle(c.UrlContext, c.CurrentUser, getBaseData(c, "Edit Education Article", nil), article),
|
||||||
|
Article: templates.EducationArticleToTemplate(article),
|
||||||
|
}
|
||||||
|
tmpl.editorData.SubmitUrl = hmnurl.BuildEducationArticleEdit(c.PathParams["slug"])
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
res.MustWriteTemplate("editor.html", tmpl, c.Perf)
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func EducationArticleEditSubmit(c *RequestContext) ResponseData {
|
func EducationArticleEditSubmit(c *RequestContext) ResponseData {
|
||||||
// TODO
|
form, err := c.GetFormValues()
|
||||||
panic("not implemented yet")
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fetchEduArticle(c, c.Conn, c.PathParams["slug"], 0)
|
||||||
|
if errors.Is(err, db.NotFound) {
|
||||||
|
return FourOhFour(c)
|
||||||
|
} else if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
art, ver := getEduArticleFromForm(form)
|
||||||
|
updateEduArticle(c, c.PathParams["slug"], art, ver)
|
||||||
|
|
||||||
|
res := c.Redirect(eduArticleURL(&art), http.StatusSeeOther)
|
||||||
|
res.AddFutureNotice("success", "Edited education article.")
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func EducationArticleDelete(c *RequestContext) ResponseData {
|
func EducationArticleDelete(c *RequestContext) ResponseData {
|
||||||
|
@ -228,6 +200,37 @@ func EducationArticleDeleteSubmit(c *RequestContext) ResponseData {
|
||||||
panic("not implemented yet")
|
panic("not implemented yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchEduArticle(ctx context.Context, dbConn db.ConnOrTx, slug string, t models.EduArticleType) (*models.EduArticle, error) {
|
||||||
|
type eduArticleResult struct {
|
||||||
|
Article models.EduArticle `db:"a"`
|
||||||
|
CurrentVersion models.EduArticleVersion `db:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var qb db.QueryBuilder
|
||||||
|
qb.Add(
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
education_article AS a
|
||||||
|
JOIN education_article_version AS v ON a.current_version = v.id
|
||||||
|
WHERE
|
||||||
|
slug = $?
|
||||||
|
`,
|
||||||
|
slug,
|
||||||
|
)
|
||||||
|
if t != 0 {
|
||||||
|
qb.Add(`AND type = $?`, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := db.QueryOne[eduArticleResult](ctx, dbConn, qb.String(), qb.Args()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Article.CurrentVersion = &res.CurrentVersion
|
||||||
|
return &res.Article, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getEditorDataForEduArticle(
|
func getEditorDataForEduArticle(
|
||||||
urlContext *hmnurl.UrlContext,
|
urlContext *hmnurl.UrlContext,
|
||||||
currentUser *models.User,
|
currentUser *models.User,
|
||||||
|
@ -244,5 +247,103 @@ func getEditorDataForEduArticle(
|
||||||
ShowEduOptions: true,
|
ShowEduOptions: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if article != nil {
|
||||||
|
result.PostTitle = article.Title
|
||||||
|
result.EditInitialContents = article.CurrentVersion.ContentRaw
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getEduArticleFromForm(form url.Values) (art models.EduArticle, ver models.EduArticleVersion) {
|
||||||
|
art.Title = form.Get("title")
|
||||||
|
art.Slug = form.Get("slug")
|
||||||
|
art.Description = form.Get("description")
|
||||||
|
switch form.Get("type") {
|
||||||
|
case "article":
|
||||||
|
art.Type = models.EduArticleTypeArticle
|
||||||
|
case "glossary":
|
||||||
|
art.Type = models.EduArticleTypeGlossary
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown education article type: %s", form.Get("type")))
|
||||||
|
}
|
||||||
|
art.Published = form.Get("published") != ""
|
||||||
|
|
||||||
|
// TODO: Education-specific Markdown
|
||||||
|
ver.ContentRaw = form.Get("body")
|
||||||
|
ver.ContentHTML = parsing.ParseMarkdown(ver.ContentRaw, parsing.ForumRealMarkdown)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEduArticle(c *RequestContext, art models.EduArticle, ver models.EduArticleVersion) {
|
||||||
|
tx := utils.Must1(c.Conn.Begin(c))
|
||||||
|
defer tx.Rollback(c)
|
||||||
|
{
|
||||||
|
articleID := db.MustQueryOneScalar[int](c, tx,
|
||||||
|
`
|
||||||
|
INSERT INTO education_article (title, slug, description, published, type, current_version)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, -1)
|
||||||
|
RETURNING id
|
||||||
|
`,
|
||||||
|
art.Title, art.Slug, art.Description, art.Published, art.Type,
|
||||||
|
)
|
||||||
|
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, ver.ContentRaw, ver.ContentHTML,
|
||||||
|
)
|
||||||
|
tx.Exec(c,
|
||||||
|
`UPDATE education_article SET current_version = $1 WHERE id = $2`,
|
||||||
|
versionID, articleID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
utils.Must(tx.Commit(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateEduArticle(c *RequestContext, slug string, art models.EduArticle, ver models.EduArticleVersion) {
|
||||||
|
tx := utils.Must1(c.Conn.Begin(c))
|
||||||
|
defer tx.Rollback(c)
|
||||||
|
{
|
||||||
|
articleID := db.MustQueryOneScalar[int](c, tx,
|
||||||
|
`SELECT id FROM education_article WHERE slug = $1`,
|
||||||
|
slug,
|
||||||
|
)
|
||||||
|
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, ver.ContentRaw, ver.ContentHTML,
|
||||||
|
)
|
||||||
|
tx.Exec(c,
|
||||||
|
`
|
||||||
|
UPDATE education_article
|
||||||
|
SET
|
||||||
|
title = $1, slug = $2, description = $3, published = $4, type = $5,
|
||||||
|
current_version = $6
|
||||||
|
WHERE
|
||||||
|
id = $7
|
||||||
|
`,
|
||||||
|
art.Title, art.Slug, art.Description, art.Published, art.Type,
|
||||||
|
versionID,
|
||||||
|
articleID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
utils.Must(tx.Commit(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func eduArticleURL(a *models.EduArticle) string {
|
||||||
|
switch a.Type {
|
||||||
|
case models.EduArticleTypeArticle:
|
||||||
|
return hmnurl.BuildEducationArticle(a.Slug)
|
||||||
|
case models.EduArticleTypeGlossary:
|
||||||
|
return hmnurl.BuildEducationGlossary(a.Slug)
|
||||||
|
default:
|
||||||
|
panic("unknown education article type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue