diff options
author | Steve <nilslice@gmail.com> | 2017-03-11 13:15:16 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-03-11 13:15:16 -0800 |
commit | 88d0ebf9d6dd764c82a4a8ac702064e7f504bc25 (patch) | |
tree | 1dbf6c9a405c81db780dba16727dea0465070732 | |
parent | a69060728b5f99bb41c9562dcd294f6fc74d4b22 (diff) | |
parent | 7ea4aac0ec47e3f04e8d5ffc40433885fe11e207 (diff) |
Merge pull request #97 from kkeuning/updateable-example
Updateable example added
-rw-r--r-- | examples/README.md | 1 | ||||
-rw-r--r-- | examples/updateable/README.md | 31 | ||||
-rw-r--r-- | examples/updateable/content/song.go | 142 |
3 files changed, 174 insertions, 0 deletions
diff --git a/examples/README.md b/examples/README.md index e309559..f575db7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,4 +11,5 @@ for this directory. ### Table of Contents 1. [Add content via HTTP API using the `api.Externalable` interface](https://github.com/ponzu-cms/ponzu/tree/master/examples/externalable) +2. [Update content via HTTP API using the `api.Updateable` interface](https://github.com/ponzu-cms/ponzu/tree/master/examples/updateable) diff --git a/examples/updateable/README.md b/examples/updateable/README.md new file mode 100644 index 0000000..1cc50f8 --- /dev/null +++ b/examples/updateable/README.md @@ -0,0 +1,31 @@ +# Updateable + +This example shows how to enable outside clients to update content to your CMS. +All content submitted must be done through a POST request encoded as `multipart/form-data` +to the API endpoint `/api/content/update?type=<Type>&id=<id>` + +## Song example +Imagine an app that lets users add Spotify music to a global playlist, and you need them +to supply songs in the format: +```go +type Song struct { + item.Item + + Title string `json:"title"` + Artist string `json:"artist"` + Rating int `json:"rating"` + Opinion string `json:"opinion"` + SpotifyURL string `json:"spotify_url"` +} +``` + +See the file `content/song.go` and read the comments to understand the various +methods needed to satisfy required interfaces for this kind of activity. + +### Overview +1. Implement `api.Updateable` with the `AcceptUpdate(http.ResponseWriter, *http.Request)` method to allow outside POST requests. +2. Consistent with the externalable example, authentication can be validated in `BeforeAcceptUdate(http.ResponseWriter, *http.Request)` + +There are various validation and request checks shown in this example as well. +Please feel free to modify and submit a PR for updates or bug fixes! + diff --git a/examples/updateable/content/song.go b/examples/updateable/content/song.go new file mode 100644 index 0000000..75d51ca --- /dev/null +++ b/examples/updateable/content/song.go @@ -0,0 +1,142 @@ +package content + +import ( + "fmt" + "log" + "strings" + + "net/http" + + "github.com/ponzu-cms/ponzu/management/editor" + "github.com/ponzu-cms/ponzu/system/admin/user" + "github.com/ponzu-cms/ponzu/system/item" +) + +type Song struct { + item.Item + + Title string `json:"title"` + Artist string `json:"artist"` + Rating int `json:"rating"` + Opinion string `json:"opinion"` + SpotifyURL string `json:"spotify_url"` +} + +// MarshalEditor writes a buffer of html to edit a Song within the CMS +// and implements editor.Editable +func (s *Song) MarshalEditor() ([]byte, error) { + view, err := editor.Form(s, + // Take note that the first argument to these Input-like functions + // is the string version of each Song field, and must follow + // this pattern for auto-decoding and auto-encoding reasons: + editor.Field{ + View: editor.Input("Title", s, map[string]string{ + "label": "Title", + "type": "text", + "placeholder": "Enter the Title here", + }), + }, + editor.Field{ + View: editor.Input("Artist", s, map[string]string{ + "label": "Artist", + "type": "text", + "placeholder": "Enter the Artist here", + }), + }, + editor.Field{ + View: editor.Input("Rating", s, map[string]string{ + "label": "Rating", + "type": "text", + "placeholder": "Enter the Rating here", + }), + }, + editor.Field{ + View: editor.Richtext("Opinion", s, map[string]string{ + "label": "Opinion", + "placeholder": "Enter the Opinion here", + }), + }, + editor.Field{ + View: editor.Input("SpotifyURL", s, map[string]string{ + "label": "SpotifyURL", + "type": "text", + "placeholder": "Enter the SpotifyURL here", + }), + }, + ) + + if err != nil { + return nil, fmt.Errorf("Failed to render Song editor view: %s", err.Error()) + } + + return view, nil +} + +func init() { + item.Types["Song"] = func() interface{} { return new(Song) } +} + +// String defines the display name of a Song in the CMS list-view +func (s *Song) String() string { return s.Title } + +// AcceptUpdate is called after BeforeAccept and is where you may influence the +// merge process. For example, maybe you don't want an empty string for the Title +// or Artist field to be accepted by the update request. Updates will always merge +// with existing values, but by default will accept zero value as an update if sent. +func (s *Song) AcceptUpdate(res http.ResponseWriter, req *http.Request) error { + addr := req.RemoteAddr + log.Println("Song update sent by:", addr, "id:", req.URL.Query().Get("id")) + + // On update its fine if fields are missing, but we don't want + // title overwritten by a blank or empty string since that would + // break the display name. Artist is also required to be non-blank. + + var required = map[string]interface{}{ + "title": nil, + "artist": nil, + } + + for k, _ := range req.PostForm { + blank := (strings.TrimSpace(req.PostFormValue(k)) == "") + if _, ok := required[k]; ok && blank { + log.Println("Removing blank value for:", k) + // We'll just remove the blank values. + // Alternately we could return an error to + // reject the post. + req.PostForm.Del(k) + } + } + + return nil +} + +// BeforeAcceptUpdate is only called if the Song type implements api.Updateable +// It is called before AcceptUpdate, and returning an error will cancel the request +// causing the system to reject the data sent in the POST +func (s *Song) BeforeAcceptUpdate(res http.ResponseWriter, req *http.Request) error { + // do initial user authentication here on the request, checking for a + // token or cookie, or that certain form fields are set and valid + + // for example, this will check if the request was made by a CMS admin user: + if !user.IsValid(req) { + addr := req.RemoteAddr + err := fmt.Errorf("request rejected, invalid user. IP: %s", addr) + return err + } + + // you could then to data validation on the request post form, or do it in + // the Accept method, which is called after BeforeAccept + + return nil +} + +// AfterAcceptUpdate is called after AcceptUpdate, and is useful for logging or triggering +// notifications, etc. after the data is saved to the database, etc. +// The request has a context containing the databse 'target' affected by the +// request. +func (s *Song) AfterAcceptUpdate(res http.ResponseWriter, req *http.Request) error { + addr := req.RemoteAddr + log.Println("Song updated by:", addr) + + return nil +} |