From d1850300e551775180ae1f41d83f8279d5341620 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Fri, 15 Jan 2021 21:56:46 +0100 Subject: [PATCH] Drafts --- api.go | 9 +-- blogstats.go | 17 ++++-- databaseMigrations.go | 15 +++++ editor.go | 14 ++++- go.mod | 11 ++-- go.sum | 22 +++---- hooks.go | 2 + http.go | 19 ++++-- micropub.go | 27 +++++++-- nodeinfo.go | 4 +- posts.go | 10 ++++ postsDb.go | 100 ++++++++++++++++++++----------- postsFuncs.go | 2 +- sitemap.go | 4 +- templates/assets/css/styles.css | 6 +- templates/assets/css/styles.scss | 4 +- templates/editor.gohtml | 10 ++++ templates/strings/default.yaml | 3 +- webmention.go | 4 +- 19 files changed, 201 insertions(+), 82 deletions(-) diff --git a/api.go b/api.go index f20ab16..16a3361 100644 --- a/api.go +++ b/api.go @@ -6,6 +6,8 @@ import ( "strings" ) +// Not tested anymore + func apiPostCreateHugo(w http.ResponseWriter, r *http.Request) { blog := r.URL.Query().Get("blog") path := r.URL.Query().Get("path") @@ -29,7 +31,7 @@ func apiPostCreateHugo(w http.ResponseWriter, r *http.Request) { p.Path = path p.Section = section p.Slug = slug - err = p.replace() + err = p.create() if err != nil { serveError(w, r, err.Error(), http.StatusBadRequest) return @@ -49,12 +51,11 @@ func apiPostCreateHugo(w http.ResponseWriter, r *http.Request) { } if len(aliases) > 0 { p.Parameters["aliases"] = aliases - err = p.replace() + err = p.replace(p.Path, p.Status) if err != nil { serveError(w, r, err.Error(), http.StatusBadRequest) return } } - w.Header().Set("Location", p.fullURL()) - w.WriteHeader(http.StatusCreated) + http.Redirect(w, r, p.fullURL(), http.StatusCreated) } diff --git a/blogstats.go b/blogstats.go index c7ef28b..01ae7fe 100644 --- a/blogstats.go +++ b/blogstats.go @@ -1,29 +1,36 @@ package main import ( - "database/sql" "net/http" ) func serveBlogStats(blog, statsPath string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - var totalCount int - row, err := appDbQueryRow("select count(path) as count from posts where blog = @blog", sql.Named("blog", blog)) + // Build query + query, params := buildQuery(&postsRequestConfig{ + blog: blog, + status: statusPublished, + }) + // Count total posts + row, err := appDbQueryRow("select count(distinct path) from ("+query+")", params...) if err != nil { serveError(w, r, err.Error(), http.StatusInternalServerError) return } + var totalCount int err = row.Scan(&totalCount) if err != nil { serveError(w, r, err.Error(), http.StatusInternalServerError) return } - var years, counts []int - rows, err := appDbQuery("select substr(published, 1, 4) as year, count(path) as count from posts where blog = @blog and coalesce(published, '') != '' group by year order by year desc", sql.Named("blog", blog)) + // Count posts per year + rows, err := appDbQuery(`select substr(published, 1, 4) as year, count(distinct path) as count from (`+query+`) + where published != '' group by year order by year desc`, params...) if err != nil { serveError(w, r, err.Error(), http.StatusInternalServerError) return } + var years, counts []int for rows.Next() { var year, count int rows.Scan(&year, &count) diff --git a/databaseMigrations.go b/databaseMigrations.go index f238f66..3969e6e 100644 --- a/databaseMigrations.go +++ b/databaseMigrations.go @@ -106,6 +106,21 @@ func migrateDb() error { return err }, }, + &migrator.Migration{ + Name: "00009", + Func: func(tx *sql.Tx) error { + _, err := tx.Exec(` + alter table posts add status text; + update posts set status = 'published'; + drop view view_posts_with_title; + drop table posts_fts; + create view view_posts_with_title as select id, path, title, content, published, updated, blog, section, status from (select p.rowid as id, p.path as path, pp.value as title, content, published, updated, blog, section, status from posts p left outer join post_parameters pp on p.path = pp.path where pp.parameter = 'title'); + create virtual table posts_fts using fts5(path unindexed, title, content, published unindexed, updated unindexed, blog unindexed, section unindexed, status unindexed, content=view_posts_with_title, content_rowid=id); + insert into posts_fts(posts_fts) values ('rebuild'); + `) + return err + }, + }, ), ) if err != nil { diff --git a/editor.go b/editor.go index b43c814..872e748 100644 --- a/editor.go +++ b/editor.go @@ -11,7 +11,11 @@ import ( const editorPath = "/editor" func serveEditor(w http.ResponseWriter, _ *http.Request) { - render(w, templateEditor, &renderData{}) + render(w, templateEditor, &renderData{ + Data: map[string]interface{}{ + "Drafts": loadDrafts(), + }, + }) } func serveEditorPost(w http.ResponseWriter, r *http.Request) { @@ -33,6 +37,7 @@ func serveEditorPost(w http.ResponseWriter, r *http.Request) { Data: map[string]interface{}{ "UpdatePostURL": parsedURL.String(), "UpdatePostContent": mf.Properties.Content[0], + "Drafts": loadDrafts(), }, }) case "updatepost": @@ -69,6 +74,11 @@ func serveEditorPost(w http.ResponseWriter, r *http.Request) { editorMicropubPost(w, r, false) } +func loadDrafts() []*post { + ps, _ := getPosts(&postsRequestConfig{status: statusDraft}) + return ps +} + func editorMicropubPost(w http.ResponseWriter, r *http.Request, media bool) { recorder := httptest.NewRecorder() if media { @@ -78,7 +88,7 @@ func editorMicropubPost(w http.ResponseWriter, r *http.Request, media bool) { } result := recorder.Result() if location := result.Header.Get("Location"); location != "" { - http.Redirect(w, r, result.Header.Get("Location"), http.StatusFound) + http.Redirect(w, r, location, http.StatusFound) return } if result.StatusCode >= 200 && result.StatusCode <= 400 { diff --git a/go.mod b/go.mod index 19a6159..e7676e1 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.15 require ( codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0 - github.com/PuerkitoBio/goquery v1.6.0 + github.com/PuerkitoBio/goquery v1.6.1 github.com/andybalholm/cascadia v1.2.0 // indirect github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4 github.com/caddyserver/certmagic v0.12.0 @@ -35,7 +35,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.6 github.com/mholt/acmez v0.1.2 // indirect github.com/miekg/dns v1.1.35 // indirect - github.com/mitchellh/mapstructure v1.4.0 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/pelletier/go-toml v1.8.1 // indirect github.com/smartystreets/assertions v1.2.0 // indirect github.com/snabb/sitemap v1.0.0 @@ -43,6 +43,7 @@ require ( github.com/spf13/cast v1.3.1 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.7.0 // indirect github.com/tdewolff/minify/v2 v2.9.10 github.com/tdewolff/parse/v2 v2.5.7 // indirect github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 @@ -55,13 +56,13 @@ require ( go.uber.org/zap v1.16.0 // indirect golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect - golang.org/x/mod v0.4.0 // indirect + golang.org/x/mod v0.4.1 // indirect golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect golang.org/x/sync v0.0.0-20201207232520-09787c993a3a - golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 // indirect + golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect golang.org/x/text v0.3.5 // indirect - golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e // indirect + golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 4443e3f..9f9d765 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= -github.com/PuerkitoBio/goquery v1.6.0 h1:j7taAbelrdcsOlGeMenZxc2AWXD5fieT1/znArdnx94= -github.com/PuerkitoBio/goquery v1.6.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/goquery v1.6.1 h1:FgjbQZKl5HTmcn4sKBgvx8vv63nhyhIpv7lJpFGCWpk= +github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= @@ -232,8 +232,8 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks= -github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -309,6 +309,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tdewolff/minify/v2 v2.9.10 h1:p+ifTTl+JMFFLDYNAm7nxQ9XuCG10HTW00wlPAZ7aoE= @@ -393,8 +395,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -454,8 +456,8 @@ golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+ golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 h1:DQmQoKxQWtyybCtX/3dIuDBcAhFszqq8YiNeS6sNu1c= -golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY= +golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -495,8 +497,8 @@ golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnf golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e h1:Z2uDrs8MyXUWJbwGc4V+nGjV4Ygo+oubBbWSVQw21/I= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963 h1:K+NlvTLy0oONtRtkl1jRD9xIhnItbG2PiE7YOdjPb+k= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/hooks.go b/hooks.go index a2f50e8..c2ef6f2 100644 --- a/hooks.go +++ b/hooks.go @@ -28,6 +28,7 @@ const ( var postHooks = map[postHookType][]func(*post){} func (p *post) postPostHooks() { + // Hooks after post published for _, cmdTmplString := range appConfig.Hooks.PostPost { go func(p *post, cmdTmplString string) { executeTemplateCommand("post-post", cmdTmplString, map[string]interface{}{ @@ -42,6 +43,7 @@ func (p *post) postPostHooks() { } func (p *post) postUpdateHooks() { + // Hooks after post updated for _, cmdTmplString := range appConfig.Hooks.PostUpdate { go func(p *post, cmdTmplString string) { executeTemplateCommand("post-update", cmdTmplString, map[string]interface{}{ diff --git a/http.go b/http.go index 2a05997..63d3fba 100644 --- a/http.go +++ b/http.go @@ -108,7 +108,7 @@ func buildHandler() (http.Handler, error) { // Editor r.Route("/editor", func(mpRouter chi.Router) { - mpRouter.Use(authMiddleware, middleware.NoCache, minifier.Middleware) + mpRouter.Use(middleware.NoCache, minifier.Middleware, authMiddleware) mpRouter.Get("/", serveEditor) mpRouter.Post("/", serveEditorPost) }) @@ -137,13 +137,13 @@ func buildHandler() (http.Handler, error) { r.Route("/webmention", func(webmentionRouter chi.Router) { webmentionRouter.Use(middleware.NoCache) webmentionRouter.Post("/", handleWebmention) - webmentionRouter.With(authMiddleware, minifier.Middleware).Get("/", webmentionAdmin) + webmentionRouter.With(minifier.Middleware, authMiddleware).Get("/", webmentionAdmin) webmentionRouter.With(authMiddleware).Post("/delete", webmentionAdminDelete) webmentionRouter.With(authMiddleware).Post("/approve", webmentionAdminApprove) }) // Posts - allPostPaths, err := allPostPaths() + pp, err := allPostPaths(statusPublished) if err != nil { return nil, err } @@ -153,12 +153,23 @@ func buildHandler() (http.Handler, error) { } else { postMW = []func(http.Handler) http.Handler{cacheMiddleware, minifier.Middleware} } - for _, path := range allPostPaths { + for _, path := range pp { if path != "" { r.With(postMW...).Get(path, servePost) } } + // Drafts + dp, err := allPostPaths(statusDraft) + if err != nil { + return nil, err + } + for _, path := range dp { + if path != "" { + r.With(cacheMiddleware, minifier.Middleware, authMiddleware).Get(path, servePost) + } + } + // Post aliases allPostAliases, err := allPostAliases() if err != nil { diff --git a/micropub.go b/micropub.go index 943bbbb..04dec20 100644 --- a/micropub.go +++ b/micropub.go @@ -90,6 +90,7 @@ func (p *post) toMfItem() *microformatItem { params["blog"] = []string{p.Blog} params["published"] = []string{p.Published} params["updated"] = []string{p.Updated} + params["status"] = []string{string(p.Status)} pb, _ := yaml.Marshal(p.Parameters) content := fmt.Sprintf("---\n%s---\n%s", string(pb), p.Content) return µformatItem{ @@ -98,6 +99,7 @@ func (p *post) toMfItem() *microformatItem { Name: p.Parameters["title"], Published: []string{p.Published}, Updated: []string{p.Updated}, + PostStatus: []string{string(p.Status)}, Category: p.Parameters[appConfig.Micropub.CategoryParam], Content: []string{content}, URL: []string{p.fullURL()}, @@ -205,6 +207,9 @@ func convertMPValueMapToPost(values map[string][]string) (*post, error) { if updated, ok := values["updated"]; ok { entry.Updated = updated[0] } + if status, ok := values["post-status"]; ok { + entry.Status = postStatus(status[0]) + } // Parameter if name, ok := values["name"]; ok { entry.Parameters["title"] = name @@ -269,6 +274,7 @@ type microformatProperties struct { Name []string `json:"name,omitempty"` Published []string `json:"published,omitempty"` Updated []string `json:"updated,omitempty"` + PostStatus []string `json:"post-status,omitempty"` Category []string `json:"category,omitempty"` Content []string `json:"content,omitempty"` URL []string `json:"url,omitempty"` @@ -297,6 +303,9 @@ func convertMPMfToPost(mf *microformatItem) (*post, error) { if len(mf.Properties.Updated) == 1 { entry.Updated = mf.Properties.Updated[0] } + if len(mf.Properties.PostStatus) == 1 { + entry.Status = postStatus(mf.Properties.PostStatus[0]) + } // Parameter if len(mf.Properties.Name) == 1 { entry.Parameters["title"] = mf.Properties.Name @@ -370,26 +379,30 @@ func (p *post) computeExtraPostParameters() error { } else { p.Blog = appConfig.DefaultBlog } - if path := p.Parameters["path"]; len(path) == 1 && path[0] != "" { + if path := p.Parameters["path"]; len(path) == 1 { p.Path = path[0] delete(p.Parameters, "path") } - if section := p.Parameters["section"]; len(section) == 1 && section[0] != "" { + if section := p.Parameters["section"]; len(section) == 1 { p.Section = section[0] delete(p.Parameters, "section") } - if slug := p.Parameters["slug"]; len(slug) == 1 && slug[0] != "" { + if slug := p.Parameters["slug"]; len(slug) == 1 { p.Slug = slug[0] delete(p.Parameters, "slug") } - if published := p.Parameters["published"]; len(published) == 1 && published[0] != "" { + if published := p.Parameters["published"]; len(published) == 1 { p.Published = published[0] delete(p.Parameters, "published") } - if updated := p.Parameters["updated"]; len(updated) == 1 && updated[0] != "" { + if updated := p.Parameters["updated"]; len(updated) == 1 { p.Updated = updated[0] delete(p.Parameters, "updated") } + if status := p.Parameters["status"]; len(status) == 1 { + p.Status = postStatus(status[0]) + delete(p.Parameters, "status") + } if p.Path == "" && p.Section == "" { // Has no path or section -> default section p.Section = appConfig.Blogs[p.Blog].DefaultSection @@ -437,6 +450,8 @@ func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *micr serveError(w, r, err.Error(), http.StatusBadRequest) return } + oldPath := p.Path + oldStatus := p.Status if mf.Replace != nil { for key, value := range mf.Replace { switch key { @@ -551,7 +566,7 @@ func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *micr serveError(w, r, err.Error(), http.StatusInternalServerError) return } - err = p.replace() + err = p.replace(oldPath, oldStatus) if err != nil { serveError(w, r, err.Error(), http.StatusInternalServerError) return diff --git a/nodeinfo.go b/nodeinfo.go index 963d33f..e471dda 100644 --- a/nodeinfo.go +++ b/nodeinfo.go @@ -44,7 +44,9 @@ func (r *nodeInfoResolver) IsOpenRegistration() (bool, error) { } func (r *nodeInfoResolver) Usage() (nodeinfo.Usage, error) { - postCount, _ := countPosts(&postsRequestConfig{}) + postCount, _ := countPosts(&postsRequestConfig{ + status: statusPublished, + }) u := nodeinfo.Usage{ Users: nodeinfo.UsageUsers{ Total: len(appConfig.Blogs), diff --git a/posts.go b/posts.go index 8c1c3a3..53a361b 100644 --- a/posts.go +++ b/posts.go @@ -25,12 +25,21 @@ type post struct { Parameters map[string][]string `json:"parameters"` Blog string `json:"blog"` Section string `json:"section"` + Status postStatus `json:"status"` // Not persisted Slug string `json:"slug"` rendered template.HTML absoluteRendered template.HTML } +type postStatus string + +const ( + statusNil postStatus = "" + statusPublished postStatus = "published" + statusDraft postStatus = "draft" +) + func servePost(w http.ResponseWriter, r *http.Request) { as := strings.HasSuffix(r.URL.Path, ".as") if as { @@ -213,6 +222,7 @@ func serveIndex(ic *indexConfig) func(w http.ResponseWriter, r *http.Request) { publishedYear: ic.year, publishedMonth: ic.month, publishedDay: ic.day, + status: statusPublished, }}, appConfig.Blogs[ic.blog].Pagination) p.SetPage(pageNo) var posts []*post diff --git a/postsDb.go b/postsDb.go index d32709e..ec25d39 100644 --- a/postsDb.go +++ b/postsDb.go @@ -32,6 +32,10 @@ func (p *post) checkPost() (err error) { return err } } + // Check status + if p.Status == "" { + p.Status = statusPublished + } // Cleanup params for key, value := range p.Parameters { if value == nil { @@ -103,45 +107,75 @@ func (p *post) checkPost() (err error) { } func (p *post) create() error { - return p.createOrReplace(true) + return p.createOrReplace(&postCreationOptions{new: true}) } -func (p *post) replace() error { - return p.createOrReplace(false) +func (p *post) replace(oldPath string, oldStatus postStatus) error { + return p.createOrReplace(&postCreationOptions{new: false, oldPath: oldPath, oldStatus: oldStatus}) } -func (p *post) createOrReplace(new bool) error { +type postCreationOptions struct { + new bool + oldPath string + oldStatus postStatus +} + +func (p *post) createOrReplace(o *postCreationOptions) error { err := p.checkPost() if err != nil { return err } startWritingToDb() - postExists := postExists(p.Path) - if postExists && new { - finishWritingToDb() - return errors.New("post already exists at given path") - } + // Create transaction tx, err := appDb.Begin() if err != nil { finishWritingToDb() return err } - if postExists { - _, err := tx.Exec("delete from posts where path = @path", sql.Named("path", p.Path)) + if !o.new { + // Remove old post + path := p.Path + if o.oldPath != "" { + path = o.oldPath + } + _, err := tx.Exec("delete from posts where path = @path", sql.Named("path", path)) if err != nil { _ = tx.Rollback() finishWritingToDb() return err } } + // Check if new path exists + postExists := func(path string) (bool, error) { + result := 0 + row := tx.QueryRow("select exists(select 1 from posts where path = @path)", sql.Named("path", path)) + if err = row.Scan(&result); err != nil { + return false, err + } + return result == 1, nil + } + if exists, err := postExists(p.Path); err != nil { + _ = tx.Rollback() + finishWritingToDb() + return err + } else if exists { + _ = tx.Rollback() + finishWritingToDb() + return errors.New("post already exists at given path") + } + // Create new post _, err = tx.Exec( - "insert into posts (path, content, published, updated, blog, section) values (@path, @content, @published, @updated, @blog, @section)", - sql.Named("path", p.Path), sql.Named("content", p.Content), sql.Named("published", p.Published), sql.Named("updated", p.Updated), sql.Named("blog", p.Blog), sql.Named("section", p.Section)) + `insert into posts (path, content, published, updated, blog, section, status) + values (@path, @content, @published, @updated, @blog, @section, @status)`, + sql.Named("path", p.Path), sql.Named("content", p.Content), sql.Named("published", p.Published), + sql.Named("updated", p.Updated), sql.Named("blog", p.Blog), sql.Named("section", p.Section), + sql.Named("status", p.Status)) if err != nil { _ = tx.Rollback() finishWritingToDb() return err } + // Create parameters ppStmt, err := tx.Prepare("insert into post_parameters (path, parameter, value) values (@path, @parameter, @value)") if err != nil { _ = tx.Rollback() @@ -160,17 +194,18 @@ func (p *post) createOrReplace(new bool) error { } } } - err = tx.Commit() - if err != nil { + if tx.Commit() != nil { finishWritingToDb() return err } finishWritingToDb() rebuildFTSIndex() - if !postExists { - defer p.postPostHooks() - } else { - defer p.postUpdateHooks() + if p.Status == statusPublished { + if o.new || o.oldStatus == statusDraft { + defer p.postPostHooks() + } else { + defer p.postUpdateHooks() + } } return reloadRouter() } @@ -197,18 +232,6 @@ func rebuildFTSIndex() { return } -func postExists(path string) bool { - result := 0 - row, err := appDbQueryRow("select exists(select 1 from posts where path = @path)", sql.Named("path", path)) - if err != nil { - return false - } - if err = row.Scan(&result); err != nil { - return false - } - return result == 1 -} - func getPost(path string) (*post, error) { posts, err := getPosts(&postsRequestConfig{path: path}) if err != nil { @@ -226,6 +249,7 @@ type postsRequestConfig struct { limit int offset int sections []string + status postStatus taxonomy *taxonomy taxonomyValue string parameter string @@ -235,12 +259,16 @@ type postsRequestConfig struct { func buildQuery(config *postsRequestConfig) (query string, args []interface{}) { args = []interface{}{} - defaultSelection := "select p.path as path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), coalesce(blog, ''), coalesce(section, ''), coalesce(parameter, ''), coalesce(value, '') " + defaultSelection := "select p.path as path, coalesce(content, '') as content, coalesce(published, '') as published, coalesce(updated, '') as updated, coalesce(blog, '') as blog, coalesce(section, '') as section, coalesce(status, '') as status, coalesce(parameter, '') as parameter, coalesce(value, '') as value " postsTable := "posts" if config.search != "" { postsTable = "posts_fts(@search)" args = append(args, sql.Named("search", config.search)) } + if config.status != "" && config.status != statusNil { + postsTable = "(select * from " + postsTable + " where status = @status)" + args = append(args, sql.Named("status", config.status)) + } if config.blog != "" { postsTable = "(select * from " + postsTable + " where blog = @blog)" args = append(args, sql.Named("blog", config.blog)) @@ -310,7 +338,7 @@ func getPosts(config *postsRequestConfig) (posts []*post, err error) { for rows.Next() { p := &post{} var parameterName, parameterValue string - err = rows.Scan(&p.Path, &p.Content, &p.Published, &p.Updated, &p.Blog, &p.Section, ¶meterName, ¶meterValue) + err = rows.Scan(&p.Path, &p.Content, &p.Published, &p.Updated, &p.Blog, &p.Section, &p.Status, ¶meterName, ¶meterValue) if err != nil { return nil, err } @@ -342,9 +370,9 @@ func countPosts(config *postsRequestConfig) (count int, err error) { return } -func allPostPaths() ([]string, error) { +func allPostPaths(status postStatus) ([]string, error) { var postPaths []string - rows, err := appDbQuery("select path from posts") + rows, err := appDbQuery("select path from posts where status = @status", sql.Named("status", status)) if err != nil { return nil, err } @@ -375,7 +403,7 @@ type publishedDate struct { } func allPublishedDates(blog string) (dates []publishedDate, err error) { - rows, err := appDbQuery("select distinct substr(published, 1, 4) as year, substr(published, 6, 2) as month, substr(published, 9, 2) as day from posts where blog = @blog and year != '' and month != '' and day != ''", sql.Named("blog", blog)) + rows, err := appDbQuery("select distinct substr(published, 1, 4) as year, substr(published, 6, 2) as month, substr(published, 9, 2) as day from posts where blog = @blog and status = @status and year != '' and month != '' and day != ''", sql.Named("blog", blog), sql.Named("status", statusPublished)) if err != nil { return nil, err } diff --git a/postsFuncs.go b/postsFuncs.go index d5e8337..4dfc60f 100644 --- a/postsFuncs.go +++ b/postsFuncs.go @@ -103,5 +103,5 @@ func (p *post) translations() []*post { } func (p *post) isPublishedSectionPost() bool { - return p.Published != "" && p.Section != "" + return p.Published != "" && p.Section != "" && p.Status == statusPublished } diff --git a/sitemap.go b/sitemap.go index 8fe1705..ff830b9 100644 --- a/sitemap.go +++ b/sitemap.go @@ -11,7 +11,9 @@ import ( const sitemapPath = "/sitemap.xml" func serveSitemap(w http.ResponseWriter, r *http.Request) { - posts, err := getPosts(&postsRequestConfig{}) + posts, err := getPosts(&postsRequestConfig{ + status: statusPublished, + }) if err != nil { serveError(w, r, err.Error(), http.StatusInternalServerError) return diff --git a/templates/assets/css/styles.css b/templates/assets/css/styles.css index 7f9d5c1..db8aea0 100644 --- a/templates/assets/css/styles.css +++ b/templates/assets/css/styles.css @@ -1,4 +1,4 @@ -.sans-serif, input, textarea, button, .button, body { +.sans-serif, input, textarea, select, button, .button, body { font-family: sans-serif; } @@ -52,7 +52,7 @@ img { width: 100%; } -input, textarea, button, .button { +input, textarea, select, button, .button { border: 1px solid #000; border: 1px solid var(--primary, #000); background: #fff; @@ -66,7 +66,7 @@ input, textarea, button, .button { font-size: 1rem; } -form input, form textarea { +form input, form textarea, form select { margin-bottom: 5px; } diff --git a/templates/assets/css/styles.scss b/templates/assets/css/styles.scss index ef6b10d..fc0a4ea 100644 --- a/templates/assets/css/styles.scss +++ b/templates/assets/css/styles.scss @@ -77,7 +77,7 @@ img { width: 100%; } -input, textarea, button, .button { +input, textarea, select, button, .button { @include color-border(border, 1px, solid, primary); @include color(background, background); @include color(color, primary); @@ -89,7 +89,7 @@ input, textarea, button, .button { font-size: 1rem; } -form input, form textarea { +form input, form textarea, form select { margin-bottom: 5px; } diff --git a/templates/editor.gohtml b/templates/editor.gohtml index c207a56..e640c0f 100644 --- a/templates/editor.gohtml +++ b/templates/editor.gohtml @@ -41,6 +41,16 @@ blog: +

{{ string .Blog.Lang "drafts" }}

+
+ + + +
{{ end }} diff --git a/templates/strings/default.yaml b/templates/strings/default.yaml index 993e0a5..a4bf5fb 100644 --- a/templates/strings/default.yaml +++ b/templates/strings/default.yaml @@ -33,4 +33,5 @@ password: "Password" shorturl: "Short URL:" year: "Year" count: "Count" -total: "Total" \ No newline at end of file +total: "Total" +drafts: "Drafts" \ No newline at end of file diff --git a/webmention.go b/webmention.go index 362cf7e..b12fbee 100644 --- a/webmention.go +++ b/webmention.go @@ -33,7 +33,9 @@ type mention struct { func initWebmention() error { // Add hooks hookFunc := func(p *post) { - p.sendWebmentions() + if p.Status == statusPublished { + p.sendWebmentions() + } } postHooks[postPostHook] = append(postHooks[postPostHook], hookFunc) postHooks[postUpdateHook] = append(postHooks[postUpdateHook], hookFunc)