From 95c7e73e2b8acf048ba61b3feab76dd5f46ac955 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Wed, 15 Mar 2017 13:24:25 -0700 Subject: adding deleteable example, rename and modify externalable -> createable --- examples/README.md | 2 +- examples/createable/README.md | 31 +++++++ examples/createable/content/song.go | 165 ++++++++++++++++++++++++++++++++++ examples/deleteable/README.md | 31 +++++++ examples/deleteable/content/song.go | 117 ++++++++++++++++++++++++ examples/externalable/README.md | 31 ------- examples/externalable/content/song.go | 165 ---------------------------------- examples/updateable/README.md | 4 +- examples/updateable/content/song.go | 16 ++-- 9 files changed, 355 insertions(+), 207 deletions(-) create mode 100644 examples/createable/README.md create mode 100644 examples/createable/content/song.go create mode 100644 examples/deleteable/README.md create mode 100644 examples/deleteable/content/song.go delete mode 100644 examples/externalable/README.md delete mode 100644 examples/externalable/content/song.go (limited to 'examples') diff --git a/examples/README.md b/examples/README.md index f575db7..cd070b1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,6 +10,6 @@ feature, it would be very helpful to include an example with a concrete use-case 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) +1. [Add content via HTTP API using the `api.Createable` interface](https://github.com/ponzu-cms/ponzu/tree/master/examples/createable) 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/createable/README.md b/examples/createable/README.md new file mode 100644 index 0000000..44ba03c --- /dev/null +++ b/examples/createable/README.md @@ -0,0 +1,31 @@ +# Createable + +This example shows how to enable outside clients to submit 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/create?type=` + +## 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.Createable` with the `Create(http.ResponseWriter, *http.Request)` method to allow outside POST requests +2. Implement `editor.Mergeable` with the `Approve(http.ResponseWriter, *http.Request)` method so you can control the Approval / Rejection of submitted content OR +3. Implement `api.Trustable` with the `AutoApprove(http.ResponseWriter, *http.Request)` method to bypass `Approve` and auto-approve and publish submitted content + +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! \ No newline at end of file diff --git a/examples/createable/content/song.go b/examples/createable/content/song.go new file mode 100644 index 0000000..74a6d4a --- /dev/null +++ b/examples/createable/content/song.go @@ -0,0 +1,165 @@ +package content + +import ( + "fmt" + "log" + + "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 } + +// Create implements api.Createable, and allows external POST requests from clients +// to add content as long as the request contains the json tag names of the Song +// struct fields, and is multipart encoded +func (s *Song) Create(res http.ResponseWriter, req *http.Request) error { + // do form data validation for required fields + required := []string{ + "title", + "artist", + "rating", + "opinion", + "spotify_url", + } + + for _, r := range required { + if req.PostFormValue(r) == "" { + err := fmt.Errorf("request missing required field: %s", r) + return err + } + } + + return nil +} + +// BeforeAPICreate is only called if the Song type implements api.Createable +// It is called before Create, and returning an error will cancel the request +// causing the system to reject the data sent in the POST +func (s *Song) BeforeAPICreate(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 Create method, which is called after BeforeAPICreate + + return nil +} + +// AfterAPICreate is called after Create, 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. Ex. Song__pending:3 or Song:8 depending if Song implements api.Trustable +func (s *Song) AfterAPICreate(res http.ResponseWriter, req *http.Request) error { + addr := req.RemoteAddr + log.Println("Song sent by:", addr, "titled:", req.PostFormValue("title")) + + return nil +} + +// Approve implements editor.Mergeable, which enables content supplied by external +// clients to be approved and thus added to the public content API. Before content +// is approved, it is waiting in the Pending bucket, and can only be approved in +// the CMS if the Mergeable interface is satisfied. If not, you will not see this +// content show up in the CMS. +func (s *Song) Approve(res http.ResponseWriter, req *http.Request) error { + return nil +} + +/* + NOTICE: if AutoApprove (seen below) is implemented, the Approve method above will have no + effect, except to add the Public / Pending toggle in the CMS UI. Though, no + Song content would be in Pending, since all externally submitting Song data + is immediately approved. +*/ + +// AutoApprove implements api.Trustable, and will automatically approve content +// that has been submitted by an external client via api.Createable. Be careful +// when using AutoApprove, because content will immediately be available through +// your public content API. If the Trustable interface is satisfied, the AfterApprove +// method is bypassed. The +func (s *Song) AutoApprove(res http.ResponseWriter, req *http.Request) error { + // Use AutoApprove to check for trust-specific headers or whitelisted IPs, + // etc. Remember, you will not be able to Approve or Reject content that + // is auto-approved. You could add a field to Song, i.e. + // AutoApproved bool `json:auto_approved` + // and set that data here, as it is called before the content is saved, but + // after the BeforeSave hook. + + return nil +} diff --git a/examples/deleteable/README.md b/examples/deleteable/README.md new file mode 100644 index 0000000..06f4188 --- /dev/null +++ b/examples/deleteable/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=&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 `Update(http.ResponseWriter, *http.Request)` method to allow outside POST requests. +2. Consistent with the createable example, authentication can be validated in `BeforeAPIUpdate(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/deleteable/content/song.go b/examples/deleteable/content/song.go new file mode 100644 index 0000000..2198e33 --- /dev/null +++ b/examples/deleteable/content/song.go @@ -0,0 +1,117 @@ +package content + +import ( + "fmt" + "log" + + "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 } + +// BeforeAPIDelete is only called if the Song type implements api.Deleteable +// It is called before Delete, and returning an error will cancel the request +// causing the system to reject the data sent in the POST +func (s *Song) BeforeAPIDelete(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 Delete method, which is called after BeforeAPIDelete + + return nil +} + +// Delete is called after BeforeAPIDelete and implements api.Deleteable. All +// other delete-based hooks are only called if this is implemented. +func (s *Song) Delete(res http.ResponseWriter, req *http.Request) error { + // See BeforeAPIDelete above, how we have checked the request for some + // form of auth. This could be done here instead, but if it is done once + // above, it means the request is valid here too. + return nil +} + +// AfterAPIDelete is called after Delete, and is useful for logging or triggering +// notifications, etc. after the data is deleted frm the database, etc. +func (s *Song) AfterAPIDelete(res http.ResponseWriter, req *http.Request) error { + addr := req.RemoteAddr + log.Println("Song deleted by:", addr, "id:", req.URL.Query().Get("id")) + + return nil +} diff --git a/examples/externalable/README.md b/examples/externalable/README.md deleted file mode 100644 index 025e7fb..0000000 --- a/examples/externalable/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Externalable - -This example shows how to enable outside clients to submit 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/external?type=` - -## 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.Externalable` with the `Accept(http.ResponseWriter, *http.Request)` method to allow outside POST requests -2. Implement `editor.Mergeable` with the `Approve(http.ResponseWriter, *http.Request)` method so you can control the Approval / Rejection of submitted content OR -3. Implement `api.Trustable` with the `AutoApprove(http.ResponseWriter, *http.Request)` method to bypass `Approve` and auto-approve and publish submitted content - -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! \ No newline at end of file diff --git a/examples/externalable/content/song.go b/examples/externalable/content/song.go deleted file mode 100644 index f2b1b73..0000000 --- a/examples/externalable/content/song.go +++ /dev/null @@ -1,165 +0,0 @@ -package content - -import ( - "fmt" - "log" - - "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 } - -// Accept implements api.Externalable, and allows external POST requests from clients -// to add content as long as the request contains the json tag names of the Song -// struct fields, and is multipart encoded -func (s *Song) Accept(res http.ResponseWriter, req *http.Request) error { - // do form data validation for required fields - required := []string{ - "title", - "artist", - "rating", - "opinion", - "spotify_url", - } - - for _, r := range required { - if req.PostFormValue(r) == "" { - err := fmt.Errorf("request missing required field: %s", r) - return err - } - } - - return nil -} - -// BeforeAccept is only called if the Song type implements api.Externalable -// It is called before Accept, and returning an error will cancel the request -// causing the system to reject the data sent in the POST -func (s *Song) BeforeAccept(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 -} - -// AfterAccept is called after Accept, 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. Ex. Song__pending:3 or Song:8 depending if Song implements api.Trustable -func (s *Song) AfterAccept(res http.ResponseWriter, req *http.Request) error { - addr := req.RemoteAddr - log.Println("Song sent by:", addr, "titled:", req.PostFormValue("title")) - - return nil -} - -// Approve implements editor.Mergeable, which enables content supplied by external -// clients to be approved and thus added to the public content API. Before content -// is approved, it is waiting in the Pending bucket, and can only be approved in -// the CMS if the Mergeable interface is satisfied. If not, you will not see this -// content show up in the CMS. -func (s *Song) Approve(res http.ResponseWriter, req *http.Request) error { - return nil -} - -/* - NOTICE: if AutoApprove (seen below) is implemented, the Approve method above will have no - effect, except to add the Public / Pending toggle in the CMS UI. Though, no - Song content would be in Pending, since all externally submitting Song data - is immediately approved. -*/ - -// AutoApprove implements api.Trustable, and will automatically approve content -// that has been submitted by an external client via api.Externalable. Be careful -// when using AutoApprove, because content will immediately be available through -// your public content API. If the Trustable interface is satisfied, the AfterApprove -// method is bypassed. The -func (s *Song) AutoApprove(res http.ResponseWriter, req *http.Request) error { - // Use AutoApprove to check for trust-specific headers or whitelisted IPs, - // etc. Remember, you will not be able to Approve or Reject content that - // is auto-approved. You could add a field to Song, i.e. - // AutoApproved bool `json:auto_approved` - // and set that data here, as it is called before the content is saved, but - // after the BeforeSave hook. - - return nil -} diff --git a/examples/updateable/README.md b/examples/updateable/README.md index 1cc50f8..06f4188 100644 --- a/examples/updateable/README.md +++ b/examples/updateable/README.md @@ -23,8 +23,8 @@ 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)` +1. Implement `api.Updateable` with the `Update(http.ResponseWriter, *http.Request)` method to allow outside POST requests. +2. Consistent with the createable example, authentication can be validated in `BeforeAPIUpdate(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 index 1ebe232..947821c 100644 --- a/examples/updateable/content/song.go +++ b/examples/updateable/content/song.go @@ -79,10 +79,10 @@ func init() { // String defines the display name of a Song in the CMS list-view func (s *Song) String() string { return s.Title } -// BeforeAcceptUpdate is only called if the Song type implements api.Updateable -// It is called before AcceptUpdate, and returning an error will cancel the request +// BeforeAPIUpdate is only called if the Song type implements api.Updateable +// It is called before Update, 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 { +func (s *Song) BeforeAPIUpdate(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 @@ -94,16 +94,16 @@ func (s *Song) BeforeAcceptUpdate(res http.ResponseWriter, req *http.Request) er } // you could then to data validation on the request post form, or do it in - // the Accept method, which is called after BeforeAccept + // the Update method, which is called after BeforeAPIUpdate return nil } -// AcceptUpdate is called after BeforeAccept and is where you may influence the +// Update is called after BeforeAPIUpdate 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 { +func (s *Song) Update(res http.ResponseWriter, req *http.Request) error { addr := req.RemoteAddr log.Println("Song update sent by:", addr, "id:", req.URL.Query().Get("id")) @@ -129,11 +129,11 @@ func (s *Song) AcceptUpdate(res http.ResponseWriter, req *http.Request) error { return nil } -// AfterAcceptUpdate is called after AcceptUpdate, and is useful for logging or triggering +// AfterAPIUpdate is called after Update, 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 { +func (s *Song) AfterAPIUpdate(res http.ResponseWriter, req *http.Request) error { addr := req.RemoteAddr log.Println("Song updated by:", addr, "id:", req.URL.Query().Get("id")) -- cgit v1.2.3