From a850fa14cd1bdded6997859a37333de3d5c0c96d Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 6 Apr 2019 13:23:22 -0400 Subject: [PATCH] Move instance page editing to dedicated section This adds a "Pages" section to the admin part of the site, and enables admins to edit the pre-defined About and Privacy pages there, instead of on the dashboard itself. It also restructures how these pages get sent around in the backend and lays the groundwork for dynamically adding static pages. The backend changes were made with more customization in mind, such as an instance-wide custom stylesheet (T563). Ref T566 --- admin.go | 118 ++++++++++++++++++++++++---- app.go | 13 ++- database.go | 43 ++++++++-- pages.go | 51 +++++++----- routes.go | 2 + templates/user/admin.tmpl | 32 -------- templates/user/admin/pages.tmpl | 25 ++++++ templates/user/admin/view-page.tmpl | 34 ++++++++ templates/user/include/header.tmpl | 5 +- 9 files changed, 243 insertions(+), 80 deletions(-) create mode 100644 templates/user/admin/pages.tmpl create mode 100644 templates/user/admin/view-page.tmpl diff --git a/admin.go b/admin.go index 297fc6c..805d98a 100644 --- a/admin.go +++ b/admin.go @@ -78,6 +78,23 @@ type inspectedCollection struct { LastPost string } +type instanceContent struct { + ID string + Type string + Title string + Content string + Updated time.Time +} + +func (c instanceContent) UpdatedFriendly() string { + /* + // TODO: accept a locale in this method and use that for the format + var loc monday.Locale = monday.LocaleEnUS + return monday.Format(u.Created, monday.DateTimeFormatsByLocale[loc], loc) + */ + return c.Updated.Format("January 2, 2006, 3:04 PM") +} + func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error { updateAppStats() p := struct { @@ -86,8 +103,6 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque Config config.AppCfg Message, ConfigMessage string - - AboutPage, PrivacyPage string }{ UserPage: NewUserPage(app, r, u, "Admin", nil), SysStatus: sysStatus, @@ -97,17 +112,6 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque ConfigMessage: r.FormValue("cm"), } - var err error - p.AboutPage, err = getAboutPage(app) - if err != nil { - return err - } - - p.PrivacyPage, _, err = getPrivacyPage(app) - if err != nil { - return err - } - showUserPage(w, "admin", p) return nil } @@ -224,6 +228,92 @@ func handleViewAdminUser(app *app, u *User, w http.ResponseWriter, r *http.Reque return nil } +func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Request) error { + p := struct { + *UserPage + Config config.AppCfg + Message string + + Pages []*instanceContent + }{ + UserPage: NewUserPage(app, r, u, "Pages", nil), + Config: app.cfg.App, + Message: r.FormValue("m"), + } + + var err error + p.Pages, err = app.db.GetInstancePages() + if err != nil { + return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get pages: %v", err)} + } + + // Add in default pages + var hasAbout, hasPrivacy bool + for _, c := range p.Pages { + if hasAbout && hasPrivacy { + break + } + if c.ID == "about" { + hasAbout = true + } else if c.ID == "privacy" { + hasPrivacy = true + } + } + if !hasAbout { + p.Pages = append(p.Pages, &instanceContent{ + ID: "about", + Content: defaultAboutPage(app.cfg), + Updated: defaultPageUpdatedTime, + }) + } + if !hasPrivacy { + p.Pages = append(p.Pages, &instanceContent{ + ID: "privacy", + Content: defaultPrivacyPolicy(app.cfg), + Updated: defaultPageUpdatedTime, + }) + } + + showUserPage(w, "pages", p) + return nil +} + +func handleViewAdminPage(app *app, u *User, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + slug := vars["slug"] + if slug == "" { + return impart.HTTPError{http.StatusFound, "/admin/pages"} + } + + p := struct { + *UserPage + Config config.AppCfg + Message string + + Content *instanceContent + }{ + Config: app.cfg.App, + Message: r.FormValue("m"), + } + + var err error + // Get pre-defined pages, or select slug + if slug == "about" { + p.Content, err = getAboutPage(app) + } else if slug == "privacy" { + p.Content, err = getPrivacyPage(app) + } else { + p.Content, err = app.db.GetDynamicContent(slug) + } + 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) + + showUserPage(w, "view-page", p) + return nil +} + func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) id := vars["page"] @@ -239,7 +329,7 @@ func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Req if err != nil { m = "?m=" + err.Error() } - return impart.HTTPError{http.StatusFound, "/admin" + m + "#page-" + id} + return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m} } func handleAdminUpdateConfig(app *app, u *User, w http.ResponseWriter, r *http.Request) error { diff --git a/app.go b/app.go index 7ea5b04..62fbd1a 100644 --- a/app.go +++ b/app.go @@ -124,8 +124,7 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te StaticPage: pageForReq(app, r), } if r.URL.Path == "/about" || r.URL.Path == "/privacy" { - var c string - var updated *time.Time + var c *instanceContent var err error if r.URL.Path == "/about" { @@ -136,16 +135,16 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te p.AboutStats.NumPosts, _ = app.db.GetTotalPosts() p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections() } else { - c, updated, err = getPrivacyPage(app) + c, err = getPrivacyPage(app) } if err != nil { return err } - p.Content = template.HTML(applyMarkdown([]byte(c), "")) - p.PlainContent = shortPostDescription(stripmd.Strip(c)) - if updated != nil { - p.Updated = updated.Format("January 2, 2006") + p.Content = template.HTML(applyMarkdown([]byte(c.Content), "")) + p.PlainContent = shortPostDescription(stripmd.Strip(c.Content)) + if !c.Updated.IsZero() { + p.Updated = c.Updated.Format("January 2, 2006") } } diff --git a/database.go b/database.go index af6ae96..3f0c967 100644 --- a/database.go +++ b/database.go @@ -115,7 +115,7 @@ type writestore interface { GetUsersInvitedCount(id string) int64 CreateInvitedUser(inviteID string, userID int64) error - GetDynamicContent(id string) (string, *time.Time, error) + GetDynamicContent(id string) (*instanceContent, error) UpdateDynamicContent(id, content string) error GetAllUsers(page uint) (*[]User, error) GetAllUsersCount() int64 @@ -2262,18 +2262,45 @@ func (db *datastore) CreateInvitedUser(inviteID string, userID int64) error { return err } -func (db *datastore) GetDynamicContent(id string) (string, *time.Time, error) { - var c string - var u *time.Time - err := db.QueryRow("SELECT content, updated FROM appcontent WHERE id = ?", id).Scan(&c, &u) +func (db *datastore) GetInstancePages() ([]*instanceContent, error) { + rows, err := db.Query("SELECT id, content, updated FROM appcontent") + if err != nil { + log.Error("Failed selecting from appcontent: %v", err) + return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve instance pages."} + } + defer rows.Close() + + pages := []*instanceContent{} + for rows.Next() { + c := &instanceContent{} + err = rows.Scan(&c.ID, &c.Content, &c.Updated) + if err != nil { + log.Error("Failed scanning row: %v", err) + break + } + pages = append(pages, c) + } + err = rows.Err() + if err != nil { + log.Error("Error after Next() on rows: %v", err) + } + + return pages, nil +} + +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) switch { case err == sql.ErrNoRows: - return "", nil, nil + return nil, nil case err != nil: log.Error("Couldn't SELECT FROM appcontent for id '%s': %v", id, err) - return "", nil, err + return nil, err } - return c, u, nil + return c, nil } func (db *datastore) UpdateDynamicContent(id, content string) error { diff --git a/pages.go b/pages.go index 044701d..72a9d44 100644 --- a/pages.go +++ b/pages.go @@ -11,39 +11,54 @@ package writefreely import ( + "github.com/writeas/writefreely/config" "time" ) -func getAboutPage(app *app) (string, error) { - c, _, err := app.db.GetDynamicContent("about") +var defaultPageUpdatedTime = time.Date(2018, 11, 8, 12, 0, 0, 0, time.Local) + +func getAboutPage(app *app) (*instanceContent, error) { + c, err := app.db.GetDynamicContent("about") if err != nil { - return "", err + return nil, err } - if c == "" { - if app.cfg.App.Federation { - c = `_` + app.cfg.App.SiteName + `_ is an interconnected place for you to write and publish, powered by WriteFreely and ActivityPub.` - } else { - c = `_` + app.cfg.App.SiteName + `_ is a place for you to write and publish, powered by WriteFreely.` + if c == nil { + c = &instanceContent{ + ID: "about", + Content: defaultAboutPage(app.cfg), } } return c, nil } -func getPrivacyPage(app *app) (string, *time.Time, error) { - c, updated, err := app.db.GetDynamicContent("privacy") +func getPrivacyPage(app *app) (*instanceContent, error) { + c, err := app.db.GetDynamicContent("privacy") if err != nil { - return "", nil, err + return nil, err + } + if c == nil { + c = &instanceContent{ + ID: "privacy", + Content: defaultPrivacyPolicy(app.cfg), + Updated: defaultPageUpdatedTime, + } + } + return c, nil +} + +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.` } - if c == "" { - c = `[Write Freely](https://writefreely.org), the software that powers this site, is built to enforce your right to privacy by default. + return `_` + cfg.App.SiteName + `_ is a place for you to write and publish, powered by WriteFreely.` +} + +func defaultPrivacyPolicy(cfg *config.Config) string { + return `[Write Freely](https://writefreely.org), the software that powers this site, is built to enforce your right to privacy by default. It retains as little data about you as possible, not even requiring an email address to sign up. However, if you _do_ give us your email address, it is stored encrypted in our database. We salt and hash your account's password. We store log files, or data about what happens on our servers. We also use cookies to keep you logged in to your account. -Beyond this, it's important that you trust whoever runs **` + app.cfg.App.SiteName + `**. Software can only do so much to protect you -- your level of privacy protections will ultimately fall on the humans that run this particular service.` - defaultTime := time.Date(2018, 11, 8, 12, 0, 0, 0, time.Local) - updated = &defaultTime - } - return c, updated, nil +Beyond this, it's important that you trust whoever runs **` + cfg.App.SiteName + `**. Software can only do so much to protect you -- your level of privacy protections will ultimately fall on the humans that run this particular service.` } diff --git a/routes.go b/routes.go index b6d3cae..bd0136f 100644 --- a/routes.go +++ b/routes.go @@ -128,6 +128,8 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET") write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET") + write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET") + write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET") write.HandleFunc("/admin/update/config", handler.Admin(handleAdminUpdateConfig)).Methods("POST") write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST") diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl index a6e1389..0a8b049 100644 --- a/templates/user/admin.tmpl +++ b/templates/user/admin.tmpl @@ -34,14 +34,6 @@ form dt { } - -
{{template "admin-header" .}} @@ -49,10 +41,6 @@ function savePage(el) {

On this page