mirror of
https://github.com/writeas/writefreely
synced 2025-02-09 10:28:47 +01:00
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:
parent
00ed2990eb
commit
a850fa14cd
118
admin.go
118
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 {
|
||||
|
13
app.go
13
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
43
database.go
43
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 {
|
||||
|
51
pages.go
51
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 == "" {
|
||||
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.`
|
||||
}
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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>
|
||||
|
25
templates/user/admin/pages.tmpl
Normal file
25
templates/user/admin/pages.tmpl
Normal 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}}
|
34
templates/user/admin/view-page.tmpl
Normal file
34
templates/user/admin/view-page.tmpl
Normal 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}}
|
@ -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}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user