summaryrefslogtreecommitdiff
path: root/system
diff options
context:
space:
mode:
Diffstat (limited to 'system')
-rw-r--r--system/admin/admin.go4
-rw-r--r--system/admin/config/config.go6
-rw-r--r--system/admin/handlers.go48
-rw-r--r--system/api/external.go8
-rw-r--r--system/api/handlers.go8
-rw-r--r--system/db/content.go16
-rw-r--r--system/db/init.go6
-rw-r--r--system/item/item.go209
-rw-r--r--system/item/types.go21
9 files changed, 278 insertions, 48 deletions
diff --git a/system/admin/admin.go b/system/admin/admin.go
index 9ddff84..53948af 100644
--- a/system/admin/admin.go
+++ b/system/admin/admin.go
@@ -8,10 +8,10 @@ import (
"html/template"
"net/http"
- "github.com/bosssauce/ponzu/content"
"github.com/bosssauce/ponzu/system/admin/user"
"github.com/bosssauce/ponzu/system/api/analytics"
"github.com/bosssauce/ponzu/system/db"
+ "github.com/bosssauce/ponzu/system/item"
)
var startAdminHTML = `<!doctype html>
@@ -104,7 +104,7 @@ func Admin(view []byte) ([]byte, error) {
a := admin{
Logo: string(cfg),
- Types: content.Types,
+ Types: item.Types,
Subview: template.HTML(view),
}
diff --git a/system/admin/config/config.go b/system/admin/config/config.go
index 2e957ed..6c315f5 100644
--- a/system/admin/config/config.go
+++ b/system/admin/config/config.go
@@ -1,13 +1,13 @@
package config
import (
- "github.com/bosssauce/ponzu/content"
"github.com/bosssauce/ponzu/management/editor"
+ "github.com/bosssauce/ponzu/system/item"
)
//Config represents the confirgurable options of the system
type Config struct {
- content.Item
+ item.Item
editor editor.Editor
Name string `json:"name"`
@@ -19,7 +19,7 @@ type Config struct {
CacheInvalidate []string `json:"cache"`
}
-// String partially implements content.Identifiable and overrides Item's String()
+// String partially implements item.Identifiable and overrides Item's String()
func (c *Config) String() string { return c.Name }
// Editor partially implements editor.Editable
diff --git a/system/admin/handlers.go b/system/admin/handlers.go
index 55a5f30..85c1f8b 100644
--- a/system/admin/handlers.go
+++ b/system/admin/handlers.go
@@ -11,7 +11,6 @@ import (
"strings"
"time"
- "github.com/bosssauce/ponzu/content"
"github.com/bosssauce/ponzu/management/editor"
"github.com/bosssauce/ponzu/management/manager"
"github.com/bosssauce/ponzu/system/admin/config"
@@ -19,6 +18,7 @@ import (
"github.com/bosssauce/ponzu/system/admin/user"
"github.com/bosssauce/ponzu/system/api"
"github.com/bosssauce/ponzu/system/db"
+ "github.com/bosssauce/ponzu/system/item"
"github.com/gorilla/schema"
emailer "github.com/nilslice/email"
@@ -759,7 +759,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) {
status := q.Get("status")
- if _, ok := content.Types[t]; !ok {
+ if _, ok := item.Types[t]; !ok {
res.WriteHeader(http.StatusBadRequest)
errView, err := Error400()
if err != nil {
@@ -770,7 +770,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) {
return
}
- pt := content.Types[t]()
+ pt := item.Types[t]()
p, ok := pt.(editor.Editable)
if !ok {
@@ -1053,17 +1053,17 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) {
// p is the asserted post as an Editable, t is the Type of the post.
// specifier is passed to append a name to a namespace like __pending
func adminPostListItem(e editor.Editable, typeName, status string) []byte {
- s, ok := e.(content.Sortable)
+ s, ok := e.(item.Sortable)
if !ok {
- log.Println("Content type", typeName, "doesn't implement content.Sortable")
- post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces. (content.Sortable)</li>`
+ log.Println("Content type", typeName, "doesn't implement item.Sortable")
+ post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces. (item.Sortable)</li>`
return []byte(post)
}
- i, ok := e.(content.Identifiable)
+ i, ok := e.(item.Identifiable)
if !ok {
- log.Println("Content type", typeName, "doesn't implement content.Identifiable")
- post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces. (content.Identifiable)</li>`
+ log.Println("Content type", typeName, "doesn't implement item.Identifiable")
+ post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces. (item.Identifiable)</li>`
return []byte(post)
}
@@ -1127,12 +1127,12 @@ func approveContentHandler(res http.ResponseWriter, req *http.Request) {
t = strings.Split(t, "__")[0]
}
- post := content.Types[t]()
+ post := item.Types[t]()
// run hooks
- hook, ok := post.(content.Hookable)
+ hook, ok := post.(item.Hookable)
if !ok {
- log.Println("Type", t, "does not implement content.Hookable or embed content.Item.")
+ log.Println("Type", t, "does not implement item.Hookable or embed item.Item.")
res.WriteHeader(http.StatusBadRequest)
errView, err := Error400()
if err != nil {
@@ -1267,9 +1267,9 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
t := q.Get("type")
status := q.Get("status")
- contentType, ok := content.Types[t]
+ contentType, ok := item.Types[t]
if !ok {
- fmt.Fprintf(res, content.ErrTypeNotRegistered, t)
+ fmt.Fprintf(res, item.ErrTypeNotRegistered, t)
return
}
post := contentType()
@@ -1317,9 +1317,9 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
return
}
} else {
- item, ok := post.(content.Identifiable)
+ item, ok := post.(item.Identifiable)
if !ok {
- log.Println("Content type", t, "doesn't implement content.Identifiable")
+ log.Println("Content type", t, "doesn't implement item.Identifiable")
return
}
item.SetItemID(-1)
@@ -1419,7 +1419,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
t = strings.Split(t, "__")[0]
}
- p, ok := content.Types[t]
+ p, ok := item.Types[t]
if !ok {
log.Println("Type", t, "is not a content type. Cannot edit or save.")
res.WriteHeader(http.StatusBadRequest)
@@ -1433,9 +1433,9 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
}
post := p()
- hook, ok := post.(content.Hookable)
+ hook, ok := post.(item.Hookable)
if !ok {
- log.Println("Type", t, "does not implement content.Hookable or embed content.Item.")
+ log.Println("Type", t, "does not implement item.Hookable or embed item.Item.")
res.WriteHeader(http.StatusBadRequest)
errView, err := Error400()
if err != nil {
@@ -1530,9 +1530,9 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) {
ct = spec[0]
}
- p, ok := content.Types[ct]
+ p, ok := item.Types[ct]
if !ok {
- log.Println("Type", t, "does not implement content.Hookable or embed content.Item.")
+ log.Println("Type", t, "does not implement item.Hookable or embed item.Item.")
res.WriteHeader(http.StatusBadRequest)
errView, err := Error400()
if err != nil {
@@ -1544,9 +1544,9 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) {
}
post := p()
- hook, ok := post.(content.Hookable)
+ hook, ok := post.(item.Hookable)
if !ok {
- log.Println("Type", t, "does not implement content.Hookable or embed content.Item.")
+ log.Println("Type", t, "does not implement item.Hookable or embed item.Item.")
res.WriteHeader(http.StatusBadRequest)
errView, err := Error400()
if err != nil {
@@ -1661,7 +1661,7 @@ func searchHandler(res http.ResponseWriter, req *http.Request) {
posts := db.ContentAll(t + specifier)
b := &bytes.Buffer{}
- p := content.Types[t]().(editor.Editable)
+ p := item.Types[t]().(editor.Editable)
html := `<div class="col s9 card">
<div class="card-content">
diff --git a/system/api/external.go b/system/api/external.go
index 0d1ea03..8112e29 100644
--- a/system/api/external.go
+++ b/system/api/external.go
@@ -7,9 +7,9 @@ import (
"strings"
"time"
- "github.com/bosssauce/ponzu/content"
"github.com/bosssauce/ponzu/system/admin/upload"
"github.com/bosssauce/ponzu/system/db"
+ "github.com/bosssauce/ponzu/system/item"
)
// Externalable accepts or rejects external POST requests to endpoints such as:
@@ -44,7 +44,7 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
return
}
- p, found := content.Types[t]
+ p, found := item.Types[t]
if !found {
log.Println("[External] attempt to submit unknown type:", t, "from:", req.RemoteAddr)
res.WriteHeader(http.StatusNotFound)
@@ -105,9 +105,9 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
return
}
- hook, ok := post.(content.Hookable)
+ hook, ok := post.(item.Hookable)
if !ok {
- log.Println("[External] error: Type", t, "does not implement content.Hookable or embed content.Item.")
+ log.Println("[External] error: Type", t, "does not implement item.Hookable or embed item.Item.")
res.WriteHeader(http.StatusBadRequest)
return
}
diff --git a/system/api/handlers.go b/system/api/handlers.go
index fae3ef6..1e2f1e2 100644
--- a/system/api/handlers.go
+++ b/system/api/handlers.go
@@ -8,14 +8,14 @@ import (
"strconv"
"strings"
- "github.com/bosssauce/ponzu/content"
"github.com/bosssauce/ponzu/system/api/analytics"
"github.com/bosssauce/ponzu/system/db"
+ "github.com/bosssauce/ponzu/system/item"
)
func typesHandler(res http.ResponseWriter, req *http.Request) {
var types = []string{}
- for t := range content.Types {
+ for t := range item.Types {
types = append(types, string(t))
}
@@ -36,7 +36,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) {
return
}
- if _, ok := content.Types[t]; !ok {
+ if _, ok := item.Types[t]; !ok {
res.WriteHeader(http.StatusNotFound)
return
}
@@ -98,7 +98,7 @@ func contentHandler(res http.ResponseWriter, req *http.Request) {
return
}
- if _, ok := content.Types[t]; !ok {
+ if _, ok := item.Types[t]; !ok {
res.WriteHeader(http.StatusNotFound)
return
}
diff --git a/system/db/content.go b/system/db/content.go
index 3293ff4..535b601 100644
--- a/system/db/content.go
+++ b/system/db/content.go
@@ -10,7 +10,7 @@ import (
"strconv"
"strings"
- "github.com/bosssauce/ponzu/content"
+ "github.com/bosssauce/ponzu/system/item"
"github.com/boltdb/bolt"
"github.com/gorilla/schema"
@@ -418,7 +418,7 @@ func SortContent(namespace string) {
// decode each (json) into type to then sort
for i := range all {
j := all[i]
- post := content.Types[namespace]()
+ post := item.Types[namespace]()
err := json.Unmarshal(j, &post)
if err != nil {
@@ -426,7 +426,7 @@ func SortContent(namespace string) {
return
}
- posts = append(posts, post.(content.Sortable))
+ posts = append(posts, post.(item.Sortable))
}
// sort posts
@@ -467,7 +467,7 @@ func SortContent(namespace string) {
}
-type sortableContent []content.Sortable
+type sortableContent []item.Sortable
func (s sortableContent) Len() int {
return len(s)
@@ -483,9 +483,9 @@ func (s sortableContent) Swap(i, j int) {
func postToJSON(ns string, data url.Values) ([]byte, error) {
// find the content type and decode values into it
- t, ok := content.Types[ns]
+ t, ok := item.Types[ns]
if !ok {
- return nil, fmt.Errorf(content.ErrTypeNotRegistered, ns)
+ return nil, fmt.Errorf(item.ErrTypeNotRegistered, ns)
}
post := t()
@@ -500,7 +500,7 @@ func postToJSON(ns string, data url.Values) ([]byte, error) {
// if the content has no slug, and has no specifier, create a slug, check it
// for duplicates, and add it to our values
if data.Get("slug") == "" && data.Get("__specifier") == "" {
- slug, err := content.Slug(post.(content.Identifiable))
+ slug, err := item.Slug(post.(item.Identifiable))
if err != nil {
return nil, err
}
@@ -510,7 +510,7 @@ func postToJSON(ns string, data url.Values) ([]byte, error) {
return nil, err
}
- post.(content.Sluggable).SetSlug(slug)
+ post.(item.Sluggable).SetSlug(slug)
data.Set("slug", slug)
}
diff --git a/system/db/init.go b/system/db/init.go
index fa2c42e..9a0c3e5 100644
--- a/system/db/init.go
+++ b/system/db/init.go
@@ -4,8 +4,8 @@ import (
"encoding/json"
"log"
- "github.com/bosssauce/ponzu/content"
"github.com/bosssauce/ponzu/system/admin/config"
+ "github.com/bosssauce/ponzu/system/item"
"github.com/boltdb/bolt"
"github.com/nilslice/jwt"
@@ -32,7 +32,7 @@ func Init() {
err = store.Update(func(tx *bolt.Tx) error {
// initialize db with all content type buckets & sorted bucket for type
- for t := range content.Types {
+ for t := range item.Types {
_, err := tx.CreateBucketIfNotExists([]byte(t))
if err != nil {
return err
@@ -86,7 +86,7 @@ func Init() {
}
go func() {
- for t := range content.Types {
+ for t := range item.Types {
SortContent(t)
}
}()
diff --git a/system/item/item.go b/system/item/item.go
new file mode 100644
index 0000000..a813669
--- /dev/null
+++ b/system/item/item.go
@@ -0,0 +1,209 @@
+package item
+
+import (
+ "fmt"
+ "net/http"
+ "regexp"
+ "strings"
+ "unicode"
+
+ uuid "github.com/satori/go.uuid"
+ "golang.org/x/text/transform"
+ "golang.org/x/text/unicode/norm"
+)
+
+// Sluggable makes a struct locatable by URL with it's own path
+// As an Item implementing Sluggable, slugs may overlap. If this is an issue,
+// make your content struct (or one which imbeds Item) implement Sluggable
+// and it will override the slug created by Item's SetSlug with your struct's
+type Sluggable interface {
+ SetSlug(string)
+ ItemSlug() string
+}
+
+// Identifiable enables a struct to have its ID set/get. Typically this is done
+// to set an ID to -1 indicating it is new for DB inserts, since by default
+// a newly initialized struct would have an ID of 0, the int zero-value, and
+// BoltDB's starting key per bucket is 0, thus overwriting the first record.
+type Identifiable interface {
+ ItemID() int
+ SetItemID(int)
+ UniqueID() uuid.UUID
+ String() string
+}
+
+// Sortable ensures data is sortable by time
+type Sortable interface {
+ Time() int64
+ Touch() int64
+}
+
+// Hookable provides our user with an easy way to intercept or add functionality
+// to the different lifecycles/events a struct may encounter. Item implements
+// Hookable with no-ops so our user can override only whichever ones necessary.
+type Hookable interface {
+ BeforeSave(req *http.Request) error
+ AfterSave(req *http.Request) error
+
+ BeforeDelete(req *http.Request) error
+ AfterDelete(req *http.Request) error
+
+ BeforeApprove(req *http.Request) error
+ AfterApprove(req *http.Request) error
+
+ BeforeReject(req *http.Request) error
+ AfterReject(req *http.Request) error
+}
+
+// Item should only be embedded into content type structs.
+type Item struct {
+ UUID uuid.UUID `json:"uuid"`
+ ID int `json:"id"`
+ Slug string `json:"slug"`
+ Timestamp int64 `json:"timestamp"`
+ Updated int64 `json:"updated"`
+}
+
+// Time partially implements the Sortable interface
+func (i Item) Time() int64 {
+ return i.Timestamp
+}
+
+// Touch partially implements the Sortable interface
+func (i Item) Touch() int64 {
+ return i.Updated
+}
+
+// SetSlug sets the item's slug for its URL
+func (i *Item) SetSlug(slug string) {
+ i.Slug = slug
+}
+
+// ItemSlug sets the item's slug for its URL
+func (i *Item) ItemSlug() string {
+ return i.Slug
+}
+
+// ItemID gets the Item's ID field
+// partially implements the Identifiable interface
+func (i Item) ItemID() int {
+ return i.ID
+}
+
+// SetItemID sets the Item's ID field
+// partially implements the Identifiable interface
+func (i *Item) SetItemID(id int) {
+ i.ID = id
+}
+
+// UniqueID gets the Item's UUID field
+// partially implements the Identifiable interface
+func (i Item) UniqueID() uuid.UUID {
+ return i.UUID
+}
+
+// String formats an Item into a printable value
+// partially implements the Identifiable interface
+func (i Item) String() string {
+ return fmt.Sprintf("Item ID: %s", i.UniqueID())
+}
+
+// BeforeSave is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) BeforeSave(req *http.Request) error {
+ return nil
+}
+
+// AfterSave is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) AfterSave(req *http.Request) error {
+ return nil
+}
+
+// BeforeDelete is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) BeforeDelete(req *http.Request) error {
+ return nil
+}
+
+// AfterDelete is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) AfterDelete(req *http.Request) error {
+ return nil
+}
+
+// BeforeApprove is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) BeforeApprove(req *http.Request) error {
+ return nil
+}
+
+// AfterApprove is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) AfterApprove(req *http.Request) error {
+ return nil
+}
+
+// BeforeReject is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) BeforeReject(req *http.Request) error {
+ return nil
+}
+
+// AfterReject is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) AfterReject(req *http.Request) error {
+ return nil
+}
+
+// Slug returns a URL friendly string from the title of a post item
+func Slug(i Identifiable) (string, error) {
+ // get the name of the post item
+ name := strings.TrimSpace(i.String())
+
+ // filter out non-alphanumeric character or non-whitespace
+ slug, err := stringToSlug(name)
+ if err != nil {
+ return "", err
+ }
+
+ return slug, nil
+}
+
+func isMn(r rune) bool {
+ return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks
+}
+
+// modified version of: https://www.socketloop.com/tutorials/golang-format-strings-to-seo-friendly-url-example
+func stringToSlug(s string) (string, error) {
+ src := []byte(strings.ToLower(s))
+
+ // convert all spaces to dash
+ rx := regexp.MustCompile("[[:space:]]")
+ src = rx.ReplaceAll(src, []byte("-"))
+
+ // remove all blanks such as tab
+ rx = regexp.MustCompile("[[:blank:]]")
+ src = rx.ReplaceAll(src, []byte(""))
+
+ rx = regexp.MustCompile("[!/:-@[-`{-~]")
+ src = rx.ReplaceAll(src, []byte(""))
+
+ rx = regexp.MustCompile("/[^\x20-\x7F]/")
+ src = rx.ReplaceAll(src, []byte(""))
+
+ rx = regexp.MustCompile("`&(amp;)?#?[a-z0-9]+;`i")
+ src = rx.ReplaceAll(src, []byte("-"))
+
+ rx = regexp.MustCompile("`&([a-z])(acute|uml|circ|grave|ring|cedil|slash|tilde|caron|lig|quot|rsquo);`i")
+ src = rx.ReplaceAll(src, []byte("\\1"))
+
+ rx = regexp.MustCompile("`[^a-z0-9]`i")
+ src = rx.ReplaceAll(src, []byte("-"))
+
+ rx = regexp.MustCompile("`[-]+`")
+ src = rx.ReplaceAll(src, []byte("-"))
+
+ str := strings.Replace(string(src), "'", "", -1)
+ str = strings.Replace(str, `"`, "", -1)
+
+ t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
+ slug, _, err := transform.String(t, str)
+ if err != nil {
+ return "", err
+ }
+
+ return strings.TrimSpace(slug), nil
+}
diff --git a/system/item/types.go b/system/item/types.go
new file mode 100644
index 0000000..33e9ced
--- /dev/null
+++ b/system/item/types.go
@@ -0,0 +1,21 @@
+package item
+
+const (
+ // ErrTypeNotRegistered means content type isn't registered (not found in Types map)
+ ErrTypeNotRegistered = `Error:
+There is no type registered for %[1]s
+
+Add this to the file which defines %[1]s{} in the 'content' package:
+
+
+ func init() {
+ item.Types["%[1]s"] = func() interface{} { return new(%[1]s) }
+ }
+
+
+`
+)
+
+// Types is a map used to reference a type name to its actual Editable type
+// mainly for lookups in /admin route based utilities
+var Types = make(map[string]func() interface{})