diff options
-rw-r--r-- | content/item.go (renamed from system/db/item.go) | 5 | ||||
-rw-r--r-- | content/post.go | 14 | ||||
-rw-r--r-- | content/types.go | 18 | ||||
-rw-r--r-- | management/manager/manager.go | 10 | ||||
-rw-r--r-- | server.go | 78 | ||||
-rw-r--r-- | system/db/query.go | 156 | ||||
-rw-r--r-- | system/server.go | 85 |
7 files changed, 266 insertions, 100 deletions
diff --git a/system/db/item.go b/content/item.go index 23e8553..8b834e4 100644 --- a/system/db/item.go +++ b/content/item.go @@ -1,8 +1,7 @@ -// Package db ... -package db +package content // Item should only be embedded into content type structs. // Helper for DB-related actions type Item struct { - ID int `json:"_id"` + ID int `json:"id"` } diff --git a/content/post.go b/content/post.go index 1dfb8fb..7bb36f6 100644 --- a/content/post.go +++ b/content/post.go @@ -18,7 +18,7 @@ type Post struct { } func init() { - Types["Post"] = &Post{} + Types["Post"] = func() interface{} { return new(Post) } } // ContentID partially implements editor.Editable @@ -28,30 +28,30 @@ func (p *Post) ContentID() int { return p.ID } func (p *Post) Editor() *editor.Editor { return &p.editor } // MarshalEditor writes a buffer of html to edit a Post and partially implements editor.Editable -func (p Post) MarshalEditor() ([]byte, error) { - view, err := editor.New(&p, +func (p *Post) MarshalEditor() ([]byte, error) { + view, err := editor.New(p, editor.Field{ - View: editor.Input("Title", &p, map[string]string{ + View: editor.Input("Title", p, map[string]string{ "label": "Post Title", "type": "text", "placeholder": "Enter your Post Title here", }), }, editor.Field{ - View: editor.Textarea("Content", &p, map[string]string{ + View: editor.Textarea("Content", p, map[string]string{ "label": "Content", "placeholder": "Add the content of your post here", }), }, editor.Field{ - View: editor.Input("Author", &p, map[string]string{ + View: editor.Input("Author", p, map[string]string{ "label": "Author", "type": "text", "placeholder": "Enter the author name here", }), }, editor.Field{ - View: editor.Input("Timestamp", &p, map[string]string{ + View: editor.Input("Timestamp", p, map[string]string{ "label": "Publish Date", "type": "date", }), diff --git a/content/types.go b/content/types.go index 778f742..ede2b58 100644 --- a/content/types.go +++ b/content/types.go @@ -1,7 +1,21 @@ package content -import "github.com/nilslice/cms/management/editor" +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() { + 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]editor.Editable) +var Types = make(map[string]func() interface{}) diff --git a/management/manager/manager.go b/management/manager/manager.go index 83ed63a..7bdebbf 100644 --- a/management/manager/manager.go +++ b/management/manager/manager.go @@ -4,15 +4,17 @@ import ( "bytes" "fmt" "html/template" - "reflect" "github.com/nilslice/cms/management/editor" ) var html = ` +<a href="/admin/edit?type={{.Kind}}" class="button">New {{.Kind}}</a> <div class="manager"> - <form method="post" action="/admin/edit?type={{.Kind}}&contentId={{.ID}}"> + <form method="post" action="/admin/edit"> {{.Editor}} + <input type="hidden" name="id" value="{{.ID}}"/> + <input type="hidden" name="type" value="{{.Kind}}"/> <input type="submit" value="Save"/> </form> </div> @@ -25,7 +27,7 @@ type form struct { } // Manage ... -func Manage(e editor.Editable) ([]byte, error) { +func Manage(e editor.Editable, typeName string) ([]byte, error) { v, err := e.MarshalEditor() if err != nil { return nil, fmt.Errorf("Couldn't marshal editor for content %T. %s", e, err.Error()) @@ -33,7 +35,7 @@ func Manage(e editor.Editable) ([]byte, error) { f := form{ ID: e.ContentID(), - Kind: reflect.TypeOf(e).Name(), + Kind: typeName, Editor: template.HTML(v), } diff --git a/server.go b/server.go deleted file mode 100644 index d7605f6..0000000 --- a/server.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - - "github.com/nilslice/cms/content" - "github.com/nilslice/cms/management/manager" -) - -const ( - // ErrTypeNotRegistered means content type isn't registered (not found in content.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() { - Types["%[1]s"] = %[1]s{} -} - ---------------------------------+ -` -) - -func main() { - // p := content.Post{ - // Title: []byte("Profound introduction"), - // Content: []byte("<h3>H</h3>ello. My name is <em>Steve</em>."), - // Author: []byte("Steve Manuel"), - // Timestamp: []byte("2016-09-16"), - // } - // p.ID = 1 - - http.HandleFunc("/admin/edit", func(res http.ResponseWriter, req *http.Request) { - switch req.Method { - case http.MethodGet: - err := req.ParseForm() - if err != nil { - res.WriteHeader(http.StatusBadRequest) - return - } - - t := req.FormValue("type") - contentType, ok := content.Types[t] - if !ok { - fmt.Fprintf(res, ErrTypeNotRegistered, t) - return - } - view, err := manager.Manage(contentType) - if err != nil { - res.WriteHeader(http.StatusInternalServerError) - return - } - res.Header().Set("Content-Type", "text/html") - res.Write(view) - - case http.MethodPost: - err := req.ParseForm() - if err != nil { - res.WriteHeader(http.StatusBadRequest) - return - } - - id := req.FormValue("contentId") - if id == "0" { - res.Write([]byte("This would create a new post")) - return - } - - res.Write([]byte("Updated post " + id)) - } - }) - - http.ListenAndServe(":8080", nil) - -} diff --git a/system/db/query.go b/system/db/query.go index c9cf37c..06e9c62 100644 --- a/system/db/query.go +++ b/system/db/query.go @@ -1,17 +1,161 @@ package db +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/url" + "strconv" + "strings" + + "github.com/boltdb/bolt" + "github.com/gorilla/schema" + "github.com/nilslice/cms/content" +) + +var store *bolt.DB + +func init() { + var err error + store, err = bolt.Open("store.db", 0666, nil) + if err != nil { + log.Fatal(err) + } +} + // Set inserts or updates values in the database. -// The `key` argument is a string made up of namespace:id (string:int) -func Set(key string) error { +// The `target` argument is a string made up of namespace:id (string:int) +func Set(target string, data url.Values) (int, error) { + t := strings.Split(target, ":") + ns, id := t[0], t[1] - return nil + // check if content has an id, and if not get new one from target bucket + if len(id) == 0 { + return insert(ns, data) + } + + return update(ns, id, data) +} + +func update(ns, id string, data url.Values) (int, error) { + cid, err := strconv.Atoi(id) + if err != nil { + return 0, err + } + + err = store.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte(ns)) + if err != nil { + return err + } + + j, err := toJSON(ns, data) + if err != nil { + return err + } + + err = b.Put([]byte(fmt.Sprintf("%d", cid)), j) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return 0, nil + } + + return cid, nil +} + +func insert(ns string, data url.Values) (int, error) { + var effectedID int + err := store.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte(ns)) + if err != nil { + return err + } + + // get the next available ID and convert to string + // also set effectedID to int of ID + id, err := b.NextSequence() + if err != nil { + return err + } + cid := strconv.FormatUint(id, 10) + effectedID, err = strconv.Atoi(cid) + if err != nil { + return err + } + data.Add("id", cid) + + j, err := toJSON(ns, data) + if err != nil { + return err + } + + err = b.Put([]byte(cid), j) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return 0, err + } + + return effectedID, nil +} + +func toJSON(ns string, data url.Values) ([]byte, error) { + // find the content type and decode values into it + t, ok := content.Types[ns] + if !ok { + return nil, fmt.Errorf(content.ErrTypeNotRegistered, ns) + } + post := t() + + dec := schema.NewDecoder() + dec.SetAliasTag("json") // allows simpler struct tagging when creating a content type + dec.IgnoreUnknownKeys(true) // will skip over form values submitted, but not in struct + err := dec.Decode(post, data) + if err != nil { + return nil, err + } + + // marshall content struct to json for db storage + j, err := json.Marshal(post) + if err != nil { + return nil, err + } + + return j, nil } // Get retrives one item from the database. Non-existent values will return an empty []byte -// The `key` argument is a string made up of namespace:id (string:int) -func Get(key string) []byte { +// The `target` argument is a string made up of namespace:id (string:int) +func Get(target string) ([]byte, error) { + t := strings.Split(target, ":") + ns, id := t[0], t[1] - return nil + val := &bytes.Buffer{} + err := store.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(ns)) + _, err := val.Write(b.Get([]byte(id))) + if err != nil { + fmt.Println(err) + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return val.Bytes(), nil } // GetAll retrives all items from the database within the provided namespace diff --git a/system/server.go b/system/server.go new file mode 100644 index 0000000..4a83706 --- /dev/null +++ b/system/server.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/nilslice/cms/content" + "github.com/nilslice/cms/management/editor" + "github.com/nilslice/cms/management/manager" + "github.com/nilslice/cms/system/db" +) + +func main() { + http.HandleFunc("/admin/edit", func(res http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + q := req.URL.Query() + i := q.Get("id") + t := q.Get("type") + contentType, ok := content.Types[t] + if !ok { + fmt.Fprintf(res, content.ErrTypeNotRegistered, t) + return + } + post := contentType() + + if i != "" { + fmt.Println("Need to show post id:", i, "(", t, ")") + + data, err := db.Get(t + ":" + i) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + err = json.Unmarshal(data, post) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + } + + view, err := manager.Manage(post.(editor.Editable), t) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + res.Header().Set("Content-Type", "text/html") + res.Write(view) + + case http.MethodPost: + err := req.ParseForm() + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusBadRequest) + return + } + + cid := req.FormValue("id") + t := req.FormValue("type") + fmt.Println("query data: t=", t, "id=", cid) + + id, err := db.Set(t+":"+cid, req.PostForm) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + fmt.Println(t, "post created:", id) + scheme := req.URL.Scheme + host := req.URL.Host + path := req.URL.Path + desURL := scheme + host + path + "?type=" + t + "&id=" + fmt.Sprintf("%d", id) + http.Redirect(res, req, desURL, http.StatusFound) + } + }) + + http.ListenAndServe(":8080", nil) + +} |