From 4cad074b44263f400c64eea810278c83473ad06f Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 10 Apr 2019 15:27:31 -0400 Subject: [PATCH 1/8] Link to version-specific writer's guide --- page/page.go | 5 +++++ templates/include/footer.tmpl | 4 ++-- templates/user/include/footer.tmpl | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/page/page.go b/page/page.go index 08e29d4..4560382 100644 --- a/page/page.go +++ b/page/page.go @@ -37,3 +37,8 @@ func (sp *StaticPage) SanitizeHost(cfg *config.Config) { sp.Host = cfg.Server.HiddenHost } } + +func (sp StaticPage) OfficialVersion() string { + p := strings.Split(sp.Version, "-") + return p[0] +} diff --git a/templates/include/footer.tmpl b/templates/include/footer.tmpl index b7291bf..e92878a 100644 --- a/templates/include/footer.tmpl +++ b/templates/include/footer.tmpl @@ -4,7 +4,7 @@ {{if .SingleUser}} From 9cb0f8092110735f20e0d7e50e4ee1f5ee7192a9 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 11 Apr 2019 13:56:07 -0400 Subject: [PATCH 2/8] Support changing instance page titles Now admins can choose a title for their About and Privacy pages; now editable through the instance page editor. This adds `title` and `content_type` fields to the `appcontent` table, requiring a migration by running `writefreely --migrate` The content_type field specifies that items we're currently storing in this table are all "page"s; queries for fetching these have been updated to filter for this type. In the future, this field will be used to indicate when an item is a stylesheet (ref T563) or other supported type. Ref T566 --- admin.go | 23 ++++++++++++--- app.go | 2 ++ database.go | 24 ++++++++++----- migrations/drivers.go | 14 +++++++++ migrations/migrations.go | 3 +- migrations/v2.go | 35 ++++++++++++++++++++++ pages.go | 17 +++++++++++ pages/about.tmpl | 4 +-- pages/privacy.tmpl | 4 +-- templates/user/admin/pages.tmpl | 10 +++++-- templates/user/admin/view-page.tmpl | 45 ++++++++++++++++++++++++----- 11 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 migrations/v2.go diff --git a/admin.go b/admin.go index 805d98a..29f3501 100644 --- a/admin.go +++ b/admin.go @@ -11,6 +11,7 @@ package writefreely import ( + "database/sql" "fmt" "github.com/gogits/gogs/pkg/tool" "github.com/gorilla/mux" @@ -81,7 +82,7 @@ type inspectedCollection struct { type instanceContent struct { ID string Type string - Title string + Title sql.NullString Content string Updated time.Time } @@ -249,19 +250,26 @@ func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Requ // Add in default pages var hasAbout, hasPrivacy bool - for _, c := range p.Pages { + for i, c := range p.Pages { if hasAbout && hasPrivacy { break } if c.ID == "about" { hasAbout = true + if !c.Title.Valid { + p.Pages[i].Title = defaultAboutTitle(app.cfg) + } } else if c.ID == "privacy" { hasPrivacy = true + if !c.Title.Valid { + p.Pages[i].Title = defaultPrivacyTitle() + } } } if !hasAbout { p.Pages = append(p.Pages, &instanceContent{ ID: "about", + Title: defaultAboutTitle(app.cfg), Content: defaultAboutPage(app.cfg), Updated: defaultPageUpdatedTime, }) @@ -269,6 +277,7 @@ func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Requ if !hasPrivacy { p.Pages = append(p.Pages, &instanceContent{ ID: "privacy", + Title: defaultPrivacyTitle(), Content: defaultPrivacyPolicy(app.cfg), Updated: defaultPageUpdatedTime, }) @@ -308,7 +317,13 @@ func handleViewAdminPage(app *app, u *User, w http.ResponseWriter, r *http.Reque if err != nil { return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get page: %v", err)} } - p.UserPage = NewUserPage(app, r, u, p.Content.ID, nil) + title := "New page" + if p.Content != nil { + title = "Edit " + p.Content.ID + } else { + p.Content = &instanceContent{} + } + p.UserPage = NewUserPage(app, r, u, title, nil) showUserPage(w, "view-page", p) return nil @@ -325,7 +340,7 @@ func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Req // Update page m := "" - err := app.db.UpdateDynamicContent(id, r.FormValue("content")) + err := app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page") if err != nil { m = "?m=" + err.Error() } diff --git a/app.go b/app.go index 62fbd1a..2c5972a 100644 --- a/app.go +++ b/app.go @@ -115,6 +115,7 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error { p := struct { page.StaticPage + ContentTitle string Content template.HTML PlainContent string Updated string @@ -141,6 +142,7 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te if err != nil { return err } + p.ContentTitle = c.Title.String p.Content = template.HTML(applyMarkdown([]byte(c.Content), "")) p.PlainContent = shortPostDescription(stripmd.Strip(c.Content)) if !c.Updated.IsZero() { diff --git a/database.go b/database.go index 3f0c967..4e64dbb 100644 --- a/database.go +++ b/database.go @@ -116,7 +116,7 @@ type writestore interface { CreateInvitedUser(inviteID string, userID int64) error GetDynamicContent(id string) (*instanceContent, error) - UpdateDynamicContent(id, content string) error + UpdateDynamicContent(id, title, content, contentType string) error GetAllUsers(page uint) (*[]User, error) GetAllUsersCount() int64 GetUserLastPostTime(id int64) (*time.Time, error) @@ -2263,7 +2263,17 @@ func (db *datastore) CreateInvitedUser(inviteID string, userID int64) error { } func (db *datastore) GetInstancePages() ([]*instanceContent, error) { - rows, err := db.Query("SELECT id, content, updated FROM appcontent") + return db.GetAllDynamicContent("page") +} + +func (db *datastore) GetAllDynamicContent(t string) ([]*instanceContent, error) { + where := "" + params := []interface{}{} + if t != "" { + where = " WHERE content_type = ?" + params = append(params, t) + } + rows, err := db.Query("SELECT id, title, content, updated, content_type FROM appcontent"+where, params...) if err != nil { log.Error("Failed selecting from appcontent: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve instance pages."} @@ -2273,7 +2283,7 @@ func (db *datastore) GetInstancePages() ([]*instanceContent, error) { pages := []*instanceContent{} for rows.Next() { c := &instanceContent{} - err = rows.Scan(&c.ID, &c.Content, &c.Updated) + err = rows.Scan(&c.ID, &c.Title, &c.Content, &c.Updated, &c.Type) if err != nil { log.Error("Failed scanning row: %v", err) break @@ -2292,7 +2302,7 @@ func (db *datastore) GetDynamicContent(id string) (*instanceContent, error) { c := &instanceContent{ ID: id, } - err := db.QueryRow("SELECT content, updated FROM appcontent WHERE id = ?", id).Scan(&c.Content, &c.Updated) + err := db.QueryRow("SELECT title, content, updated, content_type FROM appcontent WHERE id = ?", id).Scan(&c.Title, &c.Content, &c.Updated, &c.Type) switch { case err == sql.ErrNoRows: return nil, nil @@ -2303,12 +2313,12 @@ func (db *datastore) GetDynamicContent(id string) (*instanceContent, error) { return c, nil } -func (db *datastore) UpdateDynamicContent(id, content string) error { +func (db *datastore) UpdateDynamicContent(id, title, content, contentType string) error { var err error if db.driverName == driverSQLite { - _, err = db.Exec("INSERT OR REPLACE INTO appcontent (id, content, updated) VALUES (?, ?, "+db.now()+")", id, content) + _, err = db.Exec("INSERT OR REPLACE INTO appcontent (id, title, content, updated, content_type) VALUES (?, ?, ?, "+db.now()+", ?)", id, title, content, contentType) } else { - _, err = db.Exec("INSERT INTO appcontent (id, content, updated) VALUES (?, ?, "+db.now()+") "+db.upsert("id")+" content = ?, updated = "+db.now(), id, content, content) + _, err = db.Exec("INSERT INTO appcontent (id, title, content, updated, content_type) VALUES (?, ?, ?, "+db.now()+", ?) "+db.upsert("id")+" title = ?, content = ?, updated = "+db.now(), id, title, content, contentType, title, content) } if err != nil { log.Error("Unable to INSERT appcontent for '%s': %v", id, err) diff --git a/migrations/drivers.go b/migrations/drivers.go index 455cadc..59fe16f 100644 --- a/migrations/drivers.go +++ b/migrations/drivers.go @@ -47,6 +47,13 @@ func (db *datastore) typeChar(l int) string { return fmt.Sprintf("CHAR(%d)", l) } +func (db *datastore) typeVarChar(l int) string { + if db.driverName == driverSQLite { + return "TEXT" + } + return fmt.Sprintf("VARCHAR(%d)", l) +} + func (db *datastore) typeBool() string { if db.driverName == driverSQLite { return "INTEGER" @@ -58,6 +65,13 @@ func (db *datastore) typeDateTime() string { return "DATETIME" } +func (db *datastore) collateMultiByte() string { + if db.driverName == driverSQLite { + return "" + } + return " COLLATE utf8_bin" +} + func (db *datastore) engine() string { if db.driverName == driverSQLite { return "" diff --git a/migrations/migrations.go b/migrations/migrations.go index 1d6e0c4..9d73f86 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -55,7 +55,8 @@ func (m *migration) Migrate(db *datastore) error { } var migrations = []Migration{ - New("support user invites", supportUserInvites), // -> V1 (v0.8.0) + New("support user invites", supportUserInvites), // -> V1 (v0.8.0) + New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0) } // CurrentVer returns the current migration version the application is on diff --git a/migrations/v2.go b/migrations/v2.go new file mode 100644 index 0000000..b71ce70 --- /dev/null +++ b/migrations/v2.go @@ -0,0 +1,35 @@ +/* + * Copyright © 2019 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + +package migrations + +func supportInstancePages(db *datastore) error { + t, err := db.Begin() + + _, err = t.Exec(`ALTER TABLE appcontent ADD COLUMN title ` + db.typeVarChar(255) + db.collateMultiByte() + ` NULL`) + if err != nil { + t.Rollback() + return err + } + + _, err = t.Exec(`ALTER TABLE appcontent ADD COLUMN content_type ` + db.typeVarChar(36) + ` DEFAULT 'page' NOT NULL`) + if err != nil { + t.Rollback() + return err + } + + err = t.Commit() + if err != nil { + t.Rollback() + return err + } + + return nil +} diff --git a/pages.go b/pages.go index 72a9d44..de8f9b8 100644 --- a/pages.go +++ b/pages.go @@ -11,6 +11,7 @@ package writefreely import ( + "database/sql" "github.com/writeas/writefreely/config" "time" ) @@ -25,12 +26,20 @@ func getAboutPage(app *app) (*instanceContent, error) { if c == nil { c = &instanceContent{ ID: "about", + Type: "page", Content: defaultAboutPage(app.cfg), } } + if !c.Title.Valid { + c.Title = defaultAboutTitle(app.cfg) + } return c, nil } +func defaultAboutTitle(cfg *config.Config) sql.NullString { + return sql.NullString{String: "About " + cfg.App.SiteName, Valid: true} +} + func getPrivacyPage(app *app) (*instanceContent, error) { c, err := app.db.GetDynamicContent("privacy") if err != nil { @@ -39,13 +48,21 @@ func getPrivacyPage(app *app) (*instanceContent, error) { if c == nil { c = &instanceContent{ ID: "privacy", + Type: "page", Content: defaultPrivacyPolicy(app.cfg), Updated: defaultPageUpdatedTime, } } + if !c.Title.Valid { + c.Title = defaultPrivacyTitle() + } return c, nil } +func defaultPrivacyTitle() sql.NullString { + return sql.NullString{String: "Privacy Policy", Valid: true} +} + func defaultAboutPage(cfg *config.Config) string { if cfg.App.Federation { return `_` + cfg.App.SiteName + `_ is an interconnected place for you to write and publish, powered by WriteFreely and ActivityPub.` diff --git a/pages/about.tmpl b/pages/about.tmpl index 3328397..0fae12b 100644 --- a/pages/about.tmpl +++ b/pages/about.tmpl @@ -1,9 +1,9 @@ -{{define "head"}}About {{.SiteName}} +{{define "head"}}{{.ContentTitle}} — {{.SiteName}} {{end}} {{define "content"}}
-

About {{.SiteName}}

+

{{.ContentTitle}}

{{.Content}} diff --git a/pages/privacy.tmpl b/pages/privacy.tmpl index 9d0d177..578472a 100644 --- a/pages/privacy.tmpl +++ b/pages/privacy.tmpl @@ -1,8 +1,8 @@ -{{define "head"}}{{.SiteName}} Privacy Policy +{{define "head"}}{{.ContentTitle}} — {{.SiteName}} {{end}} {{define "content"}}
-

Privacy Policy

+

{{.ContentTitle}}

Last updated {{.Updated}}

{{.Content}} diff --git a/templates/user/admin/pages.tmpl b/templates/user/admin/pages.tmpl index 6e7eb30..9842287 100644 --- a/templates/user/admin/pages.tmpl +++ b/templates/user/admin/pages.tmpl @@ -1,6 +1,12 @@ {{define "pages"}} {{template "header" .}} + +
{{template "admin-header" .}} @@ -8,12 +14,12 @@ - + {{range .Pages}} - + {{end}} diff --git a/templates/user/admin/view-page.tmpl b/templates/user/admin/view-page.tmpl index 2ca68c9..3b1acbd 100644 --- a/templates/user/admin/view-page.tmpl +++ b/templates/user/admin/view-page.tmpl @@ -1,22 +1,53 @@ {{define "view-page"}} {{template "header" .}} +
{{template "admin-header" .}}

{{.Content.ID}} page

- {{if .Message}}

{{.Message}}

{{end}} - {{if eq .Content.ID "about"}} -

Describe what your instance is about. Accepts Markdown.

+

Describe what your instance is about.

{{else if eq .Content.ID "privacy"}} -

Outline your privacy policy. Accepts Markdown.

- {{else}} -

Accepts Markdown and HTML.

+

Outline your privacy policy.

{{end}} + {{if .Message}}

{{.Message}}

{{end}} +
- + + + + + + +

Accepts Markdown and HTML.

+ From 07ab0cdb9cfc8feb56ab6c3e0ec702c7c6863cb5 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 11 Apr 2019 20:37:01 -0400 Subject: [PATCH 3/8] Add `invites` flag in NodeInfo This indicates whether or not invites are enabled on this instance. It requires an upgrade to writefreely/go-nodeinfo v1.2.0 --- go.mod | 2 +- go.sum | 2 ++ nodeinfo.go | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d4f0505..5298af1 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/writeas/saturday v1.7.1 github.com/writeas/slug v1.2.0 github.com/writeas/web-core v1.0.0 - github.com/writefreely/go-nodeinfo v1.1.0 + github.com/writefreely/go-nodeinfo v1.2.0 golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f // indirect golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 // indirect diff --git a/go.sum b/go.sum index 3e6c186..cc5feb8 100644 --- a/go.sum +++ b/go.sum @@ -164,6 +164,8 @@ github.com/writeas/web-core v1.0.0 h1:5VKkCakQgdKZcbfVKJXtRpc5VHrkflusCl/KRCPzpQ github.com/writeas/web-core v1.0.0/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE= github.com/writefreely/go-nodeinfo v1.1.0 h1:dp/ieEu0/gTeNKFvJTYhzBBouyFn7aiWtWzkb8J1JLg= github.com/writefreely/go-nodeinfo v1.1.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg= +github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss= +github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f h1:ETU2VEl7TnT5bl7IvuKEzTDpplg5wzGYsOCAPhdoEIg= diff --git a/nodeinfo.go b/nodeinfo.go index 050dbc0..944a5df 100644 --- a/nodeinfo.go +++ b/nodeinfo.go @@ -50,6 +50,7 @@ func nodeInfoConfig(db *datastore, cfg *config.Config) *nodeinfo.Config { }, MaxBlogs: cfg.App.MaxBlogs, PublicReader: cfg.App.LocalTimeline, + Invites: cfg.App.UserInvites != "", }, Protocols: []nodeinfo.NodeProtocol{ nodeinfo.ProtocolActivityPub, From 51ac06a51b1bfbc4833b8a0d5bff144061f10a43 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 11 Apr 2019 20:44:44 -0400 Subject: [PATCH 4/8] Move Docker instructions to writefreely/documentation --- README.md | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/README.md b/README.md index e63d63e..ab814c4 100644 --- a/README.md +++ b/README.md @@ -120,36 +120,7 @@ make run # Runs the application ## Docker -### Using Docker for Development - -If you'd like to use Docker as a base for working on a site's styles and such, -you can run the following from a Bash shell. - -*Note: This process is intended only for working on site styling. If you'd -like to run Write Freely in production as a Docker service, it'll require a -little more work.* - -The `docker-setup.sh` script will present you with a few questions to set up -your dev instance. You can hit enter for most of them, except for "Admin username" -and "Admin password." You'll probably have to wait a few seconds after running -`docker-compose up -d` for the Docker services to come up before running the -bash script. - -``` -docker-compose up -d -./docker-setup.sh -``` - -Now you should be able to navigate to http://localhost:8080 and start working! - -When you're completely done working, you can run `docker-compose down` to destroy -your virtual environment, including your database data. Otherwise, `docker-compose stop` -will shut down your environment without destroying your data. - -### Using Docker for Production - -Write Freely doesn't yet provide an official Docker pathway to production. We're -working on it, though! +Read about using Docker in the [documentation](https://writefreely.org/docs/latest/admin/docker). ## Contributing From 7ab5350a94a644e407fedc902842d814074df918 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 11 Apr 2019 21:04:48 -0400 Subject: [PATCH 5/8] Bump version to 0.9.0 --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 2c5972a..0b42ec5 100644 --- a/app.go +++ b/app.go @@ -54,7 +54,7 @@ var ( debugging bool // Software version can be set from git env using -ldflags - softwareVer = "0.8.1" + softwareVer = "0.9.0" // DEPRECATED VARS // TODO: pass app.cfg into GetCollection* calls so we can get these values From af0f6302a2dcb745e6e1793c7c515586347b624e Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 11 Apr 2019 21:10:45 -0400 Subject: [PATCH 6/8] Replace --reset-pass instructions with admin guide link --- templates/user/admin.tmpl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl index 0a8b049..5ce11a2 100644 --- a/templates/user/admin.tmpl +++ b/templates/user/admin.tmpl @@ -32,6 +32,9 @@ form dt { .invisible { display: none; } +p.docs { + font-size: 0.86em; +}
@@ -41,21 +44,19 @@ form dt {

On this page

-
- -

Users

- -

reset password

-
writefreely --reset-pass <username>
+

Resources

+

App Configuration

+

Read more in the configuration docs.

{{if .ConfigMessage}}

{{.ConfigMessage}}

{{end}} From 63a2225b7fb2611785d5591d255ef2c8223dce58 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 11 Apr 2019 21:11:30 -0400 Subject: [PATCH 7/8] Add admin dashboard link to dropdown navs --- templates/user/include/header.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl index 03dd020..312d0b8 100644 --- a/templates/user/include/header.tmpl +++ b/templates/user/include/header.tmpl @@ -24,6 +24,7 @@
  • Customize
  • Stats

  • + {{if .IsAdmin}}
  • Admin
  • {{end}}
  • Settings
  • Export

  • @@ -41,6 +42,7 @@
    PagesPage Last Modified
    {{.ID}}{{if .Title.Valid}}{{.Title.String}}{{else}}{{.ID}}{{end}} {{.UpdatedFriendly}}