summaryrefslogtreecommitdiff
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
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
-rw-r--r--addons/reference/reference.go54
-rw-r--r--cmd/ponzu/contentType.tmpl5
-rw-r--r--cmd/ponzu/main.go12
-rw-r--r--cmd/ponzu/options.go114
-rw-r--r--content/doc.go6
-rw-r--r--management/editor/editor.go6
-rw-r--r--management/editor/elements.go2
-rw-r--r--management/manager/manager.go10
-rw-r--r--management/manager/process.go72
-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.go (renamed from content/item.go)73
-rw-r--r--system/item/types.go (renamed from content/types.go)4
22 files changed, 388 insertions, 226 deletions
diff --git a/addons/reference/reference.go b/addons/reference/reference.go
new file mode 100644
index 0000000..78e46eb
--- /dev/null
+++ b/addons/reference/reference.go
@@ -0,0 +1,54 @@
+// Package reference is a Ponzu addon to enable content editors to create
+// references to other content types which are stored as query strings within
+// the referencer's content DB
+package reference
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "log"
+
+ "github.com/bosssauce/ponzu/management/editor"
+ "github.com/bosssauce/ponzu/system/addon"
+)
+
+// Select returns the []byte of a <select> HTML element plus internal <options> with a label.
+// IMPORTANT:
+// The `fieldName` argument will cause a panic if it is not exactly the string
+// form of the struct field that this editor input is representing
+func Select(fieldName string, p interface{}, attrs map[string]string, contentType, tmplString string) []byte {
+ // decode all content type from db into options map
+ // map["?type=<contentType>&id=<id>"]t.String()
+ options := make(map[string]string)
+
+ var all map[string]interface{}
+ j := addon.ContentAll(contentType)
+
+ err := json.Unmarshal(j, &all)
+ if err != nil {
+ return nil
+ }
+
+ // make template for option html display
+ tmpl := template.Must(template.New(contentType).Parse(tmplString))
+
+ // make data something usable to iterate over and assign options
+ data := all["data"].([]interface{})
+
+ for i := range data {
+ item := data[i].(map[string]interface{})
+ k := fmt.Sprintf("?type=%s&id=%.0f", contentType, item["id"].(float64))
+ v := &bytes.Buffer{}
+ err := tmpl.Execute(v, item)
+ if err != nil {
+ log.Println("Error executing template for reference of:", contentType)
+ return nil
+ }
+
+ options[k] = v.String()
+ }
+
+ return editor.Select(fieldName, p, attrs, options)
+}
diff --git a/cmd/ponzu/contentType.tmpl b/cmd/ponzu/contentType.tmpl
index af60e57..1804555 100644
--- a/cmd/ponzu/contentType.tmpl
+++ b/cmd/ponzu/contentType.tmpl
@@ -4,10 +4,11 @@ import (
"fmt"
"github.com/bosssauce/ponzu/management/editor"
+ "github.com/bosssauce/ponzu/system/item"
)
type {{ .Name }} struct {
- Item
+ item.Item
editor editor.Editor
{{ range .Fields }}{{ .Name }} {{ .TypeName }} `json:"{{ .JSONName }}"`
@@ -38,7 +39,7 @@ func ({{ .Initial }} *{{ .Name }}) MarshalEditor() ([]byte, error) {
}
func init() {
- Types["{{ .Name }}"] = func() interface{} { return new({{ .Name }}) }
+ item.Types["{{ .Name }}"] = func() interface{} { return new({{ .Name }}) }
}
// Editor is a buffer of bytes for the Form function to write input views
diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go
index ebcbbad..b2b5e75 100644
--- a/cmd/ponzu/main.go
+++ b/cmd/ponzu/main.go
@@ -15,6 +15,9 @@ import (
"github.com/bosssauce/ponzu/system/api/analytics"
"github.com/bosssauce/ponzu/system/db"
"github.com/bosssauce/ponzu/system/tls"
+
+ // import registers content types
+ _ "github.com/bosssauce/ponzu/content"
)
var year = fmt.Sprintf("%d", time.Now().Year())
@@ -291,7 +294,14 @@ func main() {
tls.Enable()
}
- log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
+ // save the port the system is listening on so internal system can make
+ // HTTP api calls while in dev or production w/o adding more cli flags
+ err := db.PutConfig("http_port", fmt.Sprintf("%d", port))
+ if err != nil {
+ log.Fatalln("System failed to save config. Please try to run again.")
+ }
+
+ log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
case "":
fmt.Println(usage)
diff --git a/cmd/ponzu/options.go b/cmd/ponzu/options.go
index 7c93130..9c2767f 100644
--- a/cmd/ponzu/options.go
+++ b/cmd/ponzu/options.go
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -127,8 +126,8 @@ func createProjInDir(path string) error {
}
}
- // create a 'vendor' directory in $path/cmd/ponzu and move 'content',
- // 'management' and 'system' packages into it
+ // create an internal vendor directory in ./cmd/ponzu and move content,
+ // management and system packages into it
err = vendorCorePackages(path)
if err != nil {
return err
@@ -148,84 +147,121 @@ func vendorCorePackages(path string) error {
vendorPath := filepath.Join(path, "cmd", "ponzu", "vendor", "github.com", "bosssauce", "ponzu")
err := os.MkdirAll(vendorPath, os.ModeDir|os.ModePerm)
if err != nil {
- // TODO: rollback, remove ponzu project from path
return err
}
+ // // create a user content directory to be vendored
+ // contentPath := filepath.Join(path, "content")
+ // err = os.Mkdir(contentPath, os.ModeDir|os.ModePerm)
+ // if err != nil {
+ // return err
+ // }
+
dirs := []string{"content", "management", "system"}
for _, dir := range dirs {
err = os.Rename(filepath.Join(path, dir), filepath.Join(vendorPath, dir))
if err != nil {
- // TODO: rollback, remove ponzu project from path
return err
}
}
- // create a user 'content' package
+ // create a user content directory at project root
contentPath := filepath.Join(path, "content")
err = os.Mkdir(contentPath, os.ModeDir|os.ModePerm)
if err != nil {
- // TODO: rollback, remove ponzu project from path
return err
}
return nil
}
-func buildPonzuServer(args []string) error {
- // copy all ./content .go files to $vendor/content
- // check to see if any file exists, move on to next file if so,
- // and report this conflict to user for them to fix & re-run build
- pwd, err := os.Getwd()
+func copyFile(src, dst string) error {
+ noRoot := strings.Split(src, string(filepath.Separator))[1:]
+ path := filepath.Join(noRoot...)
+ dstFile, err := os.Create(filepath.Join(dst, path))
+ defer dstFile.Close()
if err != nil {
return err
}
- contentSrcPath := filepath.Join(pwd, "content")
- contentDstPath := filepath.Join(pwd, "cmd", "ponzu", "vendor", "github.com", "bosssauce", "ponzu", "content")
+ srcFile, err := os.Open(src)
+ defer srcFile.Close()
+ if err != nil {
+ return err
+ }
- srcFiles, err := ioutil.ReadDir(contentSrcPath)
+ _, err = io.Copy(dstFile, srcFile)
if err != nil {
return err
}
- var conflictFiles = []string{"item.go", "types.go"}
- var mustRenameFiles = []string{}
- for _, srcFileInfo := range srcFiles {
- // check srcFile exists in contentDstPath
- for _, conflict := range conflictFiles {
- if srcFileInfo.Name() == conflict {
- mustRenameFiles = append(mustRenameFiles, conflict)
- continue
- }
- }
+ return nil
+}
- dstFile, err := os.Create(filepath.Join(contentDstPath, srcFileInfo.Name()))
+func copyFilesWarnConflicts(srcDir, dstDir string, conflicts []string) error {
+ err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
- srcFile, err := os.Open(filepath.Join(contentSrcPath, srcFileInfo.Name()))
- if err != nil {
- return err
+ if info.IsDir() {
+ if len(path) > len(srcDir) {
+ path = path[len(srcDir)+1:]
+ }
+ dir := filepath.Join(dstDir, path)
+ err := os.MkdirAll(dir, os.ModeDir|os.ModePerm)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ for _, conflict := range conflicts {
+ if info.Name() == conflict {
+ fmt.Println("Ponzu couldn't fully build your project:")
+ fmt.Println("You must rename the following file, as it conflicts with Ponzu core:")
+ fmt.Println(path)
+ fmt.Println("")
+ fmt.Println("Once the files above have been renamed, run '$ ponzu build' to retry.")
+ return errors.New("Ponzu has very few internal conflicts, sorry for the inconvenience.")
+ }
}
- _, err = io.Copy(dstFile, srcFile)
+ err = copyFile(path, dstDir)
if err != nil {
return err
}
+
+ return nil
+ })
+ if err != nil {
+ return err
}
- if len(mustRenameFiles) > 1 {
- fmt.Println("Ponzu couldn't fully build your project:")
- fmt.Println("Some of your files in the content directory exist in the vendored directory.")
- fmt.Println("You must rename the following files, as they conflict with Ponzu core:")
- for _, file := range mustRenameFiles {
- fmt.Println(file)
- }
+ return nil
+}
- fmt.Println("Once the files above have been renamed, run '$ ponzu build' to retry.")
- return errors.New("Ponzu has very few internal conflicts, sorry for the inconvenience.")
+func buildPonzuServer(args []string) error {
+ pwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+
+ // copy all ./content files to internal vendor directory
+ src := "content"
+ dst := filepath.Join("cmd", "ponzu", "vendor", "github.com", "bosssauce", "ponzu", "content")
+ err = copyFilesWarnConflicts(src, dst, []string{"doc.go"})
+ if err != nil {
+ return err
+ }
+
+ // copy all ./addons files & dirs to internal vendor directory
+ src = "addons"
+ dst = filepath.Join("cmd", "ponzu", "vendor")
+ err = copyFilesWarnConflicts(src, dst, nil)
+ if err != nil {
+ return err
}
// execute go build -o ponzu-cms cmd/ponzu/*.go
diff --git a/content/doc.go b/content/doc.go
new file mode 100644
index 0000000..3e15b11
--- /dev/null
+++ b/content/doc.go
@@ -0,0 +1,6 @@
+// Package content contains all user-supplied content which the system is to
+// manage. Generate content types by using the Ponzu command line tool 'ponzu'
+// by running `$ ponzu generate <contentName> <fieldName:type...>`
+// Note: doc.go file is required to build the Ponzu command since main.go
+// imports content package to a blank identifier.
+package content
diff --git a/management/editor/editor.go b/management/editor/editor.go
index 6b55a38..7194c27 100644
--- a/management/editor/editor.go
+++ b/management/editor/editor.go
@@ -13,12 +13,6 @@ type Editable interface {
MarshalEditor() ([]byte, error)
}
-// Sortable ensures data is sortable by time
-type Sortable interface {
- Time() int64
- Touch() int64
-}
-
// Mergeable allows external post content to be approved and published through
// the public-facing API
type Mergeable interface {
diff --git a/management/editor/elements.go b/management/editor/elements.go
index 4a8ae55..bb2cb3f 100644
--- a/management/editor/elements.go
+++ b/management/editor/elements.go
@@ -242,8 +242,6 @@ func Select(fieldName string, p interface{}, attrs, options map[string]string) [
// find the field value in p to determine if an option is pre-selected
fieldVal := valueFromStructField(fieldName, p)
- // may need to alloc a buffer, as we will probably loop through options
- // and append the []byte from domElement() called for each option
attrs["class"] = "browser-default"
sel := newElement("select", attrs["label"], fieldName, p, attrs)
var opts []*element
diff --git a/management/manager/manager.go b/management/manager/manager.go
index 989eb98..5eafb9a 100644
--- a/management/manager/manager.go
+++ b/management/manager/manager.go
@@ -5,8 +5,8 @@ import (
"fmt"
"html/template"
- "github.com/bosssauce/ponzu/content"
"github.com/bosssauce/ponzu/management/editor"
+ "github.com/bosssauce/ponzu/system/item"
uuid "github.com/satori/go.uuid"
)
@@ -121,14 +121,14 @@ func Manage(e editor.Editable, typeName string) ([]byte, error) {
return nil, fmt.Errorf("Couldn't marshal editor for content %s. %s", typeName, err.Error())
}
- i, ok := e.(content.Identifiable)
+ i, ok := e.(item.Identifiable)
if !ok {
- return nil, fmt.Errorf("Content type %s does not implement content.Identifiable.", typeName)
+ return nil, fmt.Errorf("Content type %s does not implement item.Identifiable.", typeName)
}
- s, ok := e.(content.Sluggable)
+ s, ok := e.(item.Sluggable)
if !ok {
- return nil, fmt.Errorf("Content type %s does not implement content.Sluggable.", typeName)
+ return nil, fmt.Errorf("Content type %s does not implement item.Sluggable.", typeName)
}
m := manager{
diff --git a/management/manager/process.go b/management/manager/process.go
deleted file mode 100644
index ad6da94..0000000
--- a/management/manager/process.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package manager
-
-import (
- "regexp"
- "strings"
- "unicode"
-
- "github.com/bosssauce/ponzu/content"
-
- "golang.org/x/text/transform"
- "golang.org/x/text/unicode/norm"
-)
-
-// Slug returns a URL friendly string from the title of a post item
-func Slug(i content.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/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/content/item.go b/system/item/item.go
index eb79aa0..a813669 100644
--- a/content/item.go
+++ b/system/item/item.go
@@ -1,10 +1,15 @@
-package content
+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
@@ -27,6 +32,12 @@ type Identifiable interface {
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.
@@ -136,3 +147,63 @@ func (i Item) BeforeReject(req *http.Request) error {
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/content/types.go b/system/item/types.go
index 696d589..33e9ced 100644
--- a/content/types.go
+++ b/system/item/types.go
@@ -1,4 +1,4 @@
-package content
+package item
const (
// ErrTypeNotRegistered means content type isn't registered (not found in Types map)
@@ -9,7 +9,7 @@ Add this to the file which defines %[1]s{} in the 'content' package:
func init() {
- Types["%[1]s"] = func() interface{} { return new(%[1]s) }
+ item.Types["%[1]s"] = func() interface{} { return new(%[1]s) }
}