summaryrefslogtreecommitdiff
path: root/system
diff options
context:
space:
mode:
authorSteve <nilslice@gmail.com>2016-12-19 11:19:53 -0800
committerGitHub <noreply@github.com>2016-12-19 11:19:53 -0800
commit3791fadda7b761ffba38c567da29e2e71acd1dfb (patch)
tree79d810f9aafa1868ee0760983937470d0eea3db8 /system
parentb20c5bdee38682edc851e646d815a34689c3c923 (diff)
[addons] Creating foundation for plugin-like system "Addons" (#24)
* adding addons dir and sample addon which enables the use of a new input element in forms for referencing other content. "addons" is a conceptual plugin-like feature, similar to wordpress "plugins" dir, but not as sophisticated
Diffstat (limited to 'system')
-rw-r--r--system/addon/api.go47
-rw-r--r--system/admin/admin.go12
-rw-r--r--system/admin/config/config.go14
-rw-r--r--system/admin/handlers.go56
-rw-r--r--system/api/external.go8
-rw-r--r--system/api/handlers.go10
-rw-r--r--system/api/server.go4
-rw-r--r--system/db/cache.go35
-rw-r--r--system/db/config.go44
-rw-r--r--system/db/content.go20
-rw-r--r--system/db/init.go6
-rw-r--r--system/item/item.go209
-rw-r--r--system/item/types.go21
13 files changed, 390 insertions, 96 deletions
diff --git a/system/addon/api.go b/system/addon/api.go
new file mode 100644
index 0000000..4c3152b
--- /dev/null
+++ b/system/addon/api.go
@@ -0,0 +1,47 @@
+// Package addon provides an API for Ponzu addons to interface with the system
+package addon
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/bosssauce/ponzu/system/db"
+)
+
+// ContentAll retrives all items from the HTTP API within the provided namespace
+func ContentAll(namespace string) []byte {
+ host := db.ConfigCache("domain")
+ port := db.ConfigCache("http_port")
+ endpoint := "http://%s:%s/api/contents?type=%s"
+ buf := []byte{}
+ r := bytes.NewReader(buf)
+ url := fmt.Sprintf(endpoint, host, port, namespace)
+
+ req, err := http.NewRequest(http.MethodGet, url, r)
+ if err != nil {
+ log.Println("Error creating request for reference of:", namespace)
+ return nil
+ }
+
+ c := http.Client{
+ Timeout: time.Duration(time.Second * 5),
+ }
+ res, err := c.Do(req)
+ if err != nil {
+ log.Println("Error making HTTP request for reference of:", namespace)
+ return nil
+ }
+ defer res.Body.Close()
+
+ j, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ log.Println("Error reading request body for reference of:", namespace)
+ return nil
+ }
+
+ return j
+}
diff --git a/system/admin/admin.go b/system/admin/admin.go
index 9ddff84..78f607d 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),
}
@@ -169,17 +169,17 @@ var initAdminHTML = `
func Init() ([]byte, error) {
html := startAdminHTML + initAdminHTML + endAdminHTML
- cfg, err := db.Config("name")
+ name, err := db.Config("name")
if err != nil {
return nil, err
}
- if cfg == nil {
- cfg = []byte("")
+ if name == nil {
+ name = []byte("")
}
a := admin{
- Logo: string(cfg),
+ Logo: string(name),
}
buf := &bytes.Buffer{}
diff --git a/system/admin/config/config.go b/system/admin/config/config.go
index b898b49..ab17425 100644
--- a/system/admin/config/config.go
+++ b/system/admin/config/config.go
@@ -1,24 +1,25 @@
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
+// Config represents the confirgurable options of the system
type Config struct {
- content.Item
+ item.Item
editor editor.Editor
Name string `json:"name"`
Domain string `json:"domain"`
+ HTTPPort string `json:"http_port"`
AdminEmail string `json:"admin_email"`
ClientSecret string `json:"client_secret"`
Etag string `json:"etag"`
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
@@ -40,6 +41,11 @@ func (c *Config) MarshalEditor() ([]byte, error) {
}),
},
editor.Field{
+ View: editor.Input("HTTPPort", c, map[string]string{
+ "type": "hidden",
+ }),
+ },
+ editor.Field{
View: editor.Input("AdminEmail", c, map[string]string{
"label": "Adminstrator Email (will be notified of internal system information)",
}),
diff --git a/system/admin/handlers.go b/system/admin/handlers.go
index 1e6a26c..266535a 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"
@@ -71,6 +71,7 @@ func initHandler(res http.ResponseWriter, req *http.Request) {
etag := db.NewEtag()
req.Form.Set("etag", etag)
+ // create and save admin user
email := strings.ToLower(req.FormValue("email"))
password := req.FormValue("password")
usr := user.NewUser(email, password)
@@ -82,6 +83,10 @@ func initHandler(res http.ResponseWriter, req *http.Request) {
return
}
+ // set HTTP port which should be previously added to config cache
+ port := db.ConfigCache("http_port")
+ req.Form.Set("http_port", port)
+
// set initial user email as admin_email and make config
req.Form.Set("admin_email", email)
err = db.SetConfig(req.Form)
@@ -754,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 {
@@ -765,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 {
@@ -1048,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.(editor.Sortable)
+ s, ok := e.(item.Sortable)
if !ok {
- log.Println("Content type", typeName, "doesn't implement editor.Sortable")
- post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces. (editor.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)
}
@@ -1122,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 {
@@ -1262,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()
@@ -1288,7 +1293,6 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
}
if len(data) < 1 || data == nil {
- fmt.Println(string(data))
res.WriteHeader(http.StatusNotFound)
errView, err := Error404()
if err != nil {
@@ -1312,12 +1316,12 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
return
}
} else {
- s, ok := post.(content.Identifiable)
+ item, ok := post.(item.Identifiable)
if !ok {
- log.Println("Content type", t, "doesn't implement editor.Identifiable")
+ log.Println("Content type", t, "doesn't implement item.Identifiable")
return
}
- s.SetItemID(-1)
+ item.SetItemID(-1)
}
m, err := manager.Manage(post.(editor.Editable), t)
@@ -1414,7 +1418,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)
@@ -1428,9 +1432,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 {
@@ -1525,9 +1529,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 {
@@ -1539,9 +1543,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 {
@@ -1656,7 +1660,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 7a2073d..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
}
@@ -224,7 +224,7 @@ func SendJSON(res http.ResponseWriter, j map[string]interface{}) {
sendData(res, data, 200)
}
-// CORS wraps a HandleFunc to response to OPTIONS requests properly
+// CORS wraps a HandleFunc to respond to OPTIONS requests properly
func CORS(next http.HandlerFunc) http.HandlerFunc {
return db.CacheControl(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodOptions {
diff --git a/system/api/server.go b/system/api/server.go
index 823ec16..f31a748 100644
--- a/system/api/server.go
+++ b/system/api/server.go
@@ -1,8 +1,6 @@
package api
-import (
- "net/http"
-)
+import "net/http"
// Run adds Handlers to default http listener for API
func Run() {
diff --git a/system/db/cache.go b/system/db/cache.go
index 5f2dd03..30ecf5a 100644
--- a/system/db/cache.go
+++ b/system/db/cache.go
@@ -2,10 +2,8 @@ package db
import (
"encoding/base64"
- "encoding/json"
"fmt"
"net/http"
- "net/url"
"strings"
"time"
)
@@ -39,41 +37,10 @@ func NewEtag() string {
// InvalidateCache sets a new Etag for http responses
func InvalidateCache() error {
- kv := make(map[string]interface{})
-
- c, err := ConfigAll()
+ err := PutConfig("etag", NewEtag())
if err != nil {
return err
}
- err = json.Unmarshal(c, &kv)
- if err != nil {
- return err
- }
-
- kv["etag"] = NewEtag()
-
- data := make(url.Values)
- for k, v := range kv {
- switch v.(type) {
- case string:
- data.Set(k, v.(string))
- case []string:
- vv := v.([]string)
- for i := range vv {
- if i == 0 {
- data.Set(k, vv[i])
- } else {
- data.Add(k, vv[i])
- }
- }
- }
- }
-
- err = SetConfig(data)
- if err != nil {
-
- }
-
return nil
}
diff --git a/system/db/config.go b/system/db/config.go
index b5a07e4..4bbf29b 100644
--- a/system/db/config.go
+++ b/system/db/config.go
@@ -3,6 +3,7 @@ package db
import (
"bytes"
"encoding/json"
+ "fmt"
"net/url"
"strings"
@@ -117,7 +118,50 @@ func ConfigAll() ([]byte, error) {
return val.Bytes(), nil
}
+// PutConfig updates a single k/v in the config
+func PutConfig(key string, value interface{}) error {
+ kv := make(map[string]interface{})
+
+ c, err := ConfigAll()
+ if err != nil {
+ return err
+ }
+
+ err = json.Unmarshal(c, &kv)
+ if err != nil {
+ return err
+ }
+
+ // set k/v from params to decoded map
+ kv[key] = value
+
+ data := make(url.Values)
+ for k, v := range kv {
+ switch v.(type) {
+ case string:
+ data.Set(k, v.(string))
+
+ case []string:
+ vv := v.([]string)
+ for i := range vv {
+ data.Add(k, vv[i])
+ }
+
+ default:
+ data.Set(k, fmt.Sprintf("%v", v))
+ }
+ }
+
+ err = SetConfig(data)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
// ConfigCache is a in-memory cache of the Configs for quicker lookups
+// 'key' is the JSON tag associated with the config field
func ConfigCache(key string) string {
return configCache.Get(key)
}
diff --git a/system/db/content.go b/system/db/content.go
index 87b3e69..535b601 100644
--- a/system/db/content.go
+++ b/system/db/content.go
@@ -10,9 +10,7 @@ import (
"strconv"
"strings"
- "github.com/bosssauce/ponzu/content"
- "github.com/bosssauce/ponzu/management/editor"
- "github.com/bosssauce/ponzu/management/manager"
+ "github.com/bosssauce/ponzu/system/item"
"github.com/boltdb/bolt"
"github.com/gorilla/schema"
@@ -305,7 +303,7 @@ func Query(namespace string, opts QueryOptions) (int, [][]byte) {
// correct bad input rather than return nil or error
// similar to default case for opts.Order switch below
if opts.Count < 0 {
- opts.Count = 0
+ opts.Count = -1
}
if opts.Offset < 0 {
@@ -420,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 {
@@ -428,7 +426,7 @@ func SortContent(namespace string) {
return
}
- posts = append(posts, post.(editor.Sortable))
+ posts = append(posts, post.(item.Sortable))
}
// sort posts
@@ -469,7 +467,7 @@ func SortContent(namespace string) {
}
-type sortableContent []editor.Sortable
+type sortableContent []item.Sortable
func (s sortableContent) Len() int {
return len(s)
@@ -485,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()
@@ -502,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 := manager.Slug(post.(content.Identifiable))
+ slug, err := item.Slug(post.(item.Identifiable))
if err != nil {
return nil, err
}
@@ -512,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{})