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
This commit is contained in:
Matt Baer 2019-04-06 13:23:22 -04:00
parent 00ed2990eb
commit a850fa14cd
9 changed files with 243 additions and 80 deletions

118
admin.go
View File

@ -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 {

13
app.go
View File

@ -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")
}
}

View File

@ -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 {

View File

@ -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 == "" {
c = `[Write Freely](https://writefreely.org), the software that powers this site, is built to enforce your right to privacy by default.
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.`
}
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.`
}

View File

@ -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")

View File

@ -34,14 +34,6 @@ form dt {
}
</style>
<script>
function savePage(el) {
var $btn = el.querySelector('input[type=submit]');
$btn.value = 'Saving...';
$btn.disabled = true;
}
</script>
<div class="content-container snug">
{{template "admin-header" .}}
@ -49,10 +41,6 @@ function savePage(el) {
<h2>On this page</h2>
<ul class="pagenav">
{{if not .SingleUser}}
<li><a href="#page-about">Edit About page</a></li>
<li><a href="#page-privacy">Edit Privacy page</a></li>
{{end}}
<li><a href="#reset-pass">Reset user password</a></li>
<li><a href="#config">Configuration</a></li>
<li><a href="#monitor">Application monitor</a></li>
@ -60,26 +48,6 @@ function savePage(el) {
<hr />
{{if not .SingleUser}}
<h2>Site</h2>
<h3 id="page-about">About page</h3>
<p>Describe what your instance is <a href="/about" target="page">about</a>. <em>Accepts Markdown</em>.</p>
<form method="post" action="/admin/update/about" onsubmit="savePage(this)">
<textarea id="about-editor" class="section codable norm edit-page" name="content">{{.AboutPage}}</textarea>
<input type="submit" value="Save" />
</form>
<h3 id="page-privacy">Privacy page</h3>
<p>Outline your <a href="/privacy" target="page">privacy policy</a>. <em>Accepts Markdown</em>.</p>
<form method="post" action="/admin/update/privacy" onsubmit="savePage(this)">
<textarea id="privacy-editor" class="section codable norm edit-page" name="content">{{.PrivacyPage}}</textarea>
<input type="submit" value="Save" />
</form>
<hr />
{{end}}
<h2>Users</h2>
<h3><a name="reset-pass"></a>reset password</h3>

View File

@ -0,0 +1,25 @@
{{define "pages"}}
{{template "header" .}}
<div class="snug content-container">
{{template "admin-header" .}}
<h2 id="posts-header" style="display: flex; justify-content: space-between;">Pages</h2>
<table class="classy export" style="width:100%">
<tr>
<th>Pages</th>
<th>Last Modified</th>
</tr>
{{range .Pages}}
<tr>
<td><a href="/admin/page/{{.ID}}">{{.ID}}</a></td>
<td style="text-align:right">{{.UpdatedFriendly}}</td>
</tr>
{{end}}
</table>
</div>
{{template "footer" .}}
{{end}}

View File

@ -0,0 +1,34 @@
{{define "view-page"}}
{{template "header" .}}
<div class="snug content-container">
{{template "admin-header" .}}
<h2 id="posts-header">{{.Content.ID}} page</h2>
{{if .Message}}<p>{{.Message}}</p>{{end}}
{{if eq .Content.ID "about"}}
<p>Describe what your instance is <a href="/about" target="page">about</a>. <em>Accepts Markdown</em>.</p>
{{else if eq .Content.ID "privacy"}}
<p>Outline your <a href="/privacy" target="page">privacy policy</a>. <em>Accepts Markdown</em>.</p>
{{else}}
<p><em>Accepts Markdown and HTML</em>.</p>
{{end}}
<form method="post" action="/admin/update/{{.Content.ID}}" onsubmit="savePage(this)">
<textarea id="about-editor" class="section codable norm edit-page" name="content">{{.Content.Content}}</textarea>
<input type="submit" value="Save" />
</form>
</div>
<script>
function savePage(el) {
var $btn = el.querySelector('input[type=submit]');
$btn.value = 'Saving...';
$btn.disabled = true;
}
</script>
{{template "footer" .}}
{{end}}

View File

@ -64,7 +64,10 @@
<h1>Admin</h1>
<nav id="admin">
<a href="/admin" {{if eq .Path "/admin"}}class="selected"{{end}}>Dashboard</a>
{{if not .SingleUser}}<a href="/admin/users" {{if eq .Path "/admin/users"}}class="selected"{{end}}>Users</a>{{end}}
{{if not .SingleUser}}
<a href="/admin/users" {{if eq .Path "/admin/users"}}class="selected"{{end}}>Users</a>
<a href="/admin/pages" {{if eq .Path "/admin/pages"}}class="selected"{{end}}>Pages</a>
{{end}}
</nav>
</header>
{{end}}