diff options
author | Steve Manuel <nilslice@gmail.com> | 2017-03-09 11:53:58 -0800 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2017-03-09 11:53:58 -0800 |
commit | d5b7c62043b1ef5b284b094f4663c17e5abd33c7 (patch) | |
tree | b834fd0ef50c35d9ad910bc80d1a15c1373faace | |
parent | 242c6212d173dae622b374d38a4fa2a640a4f4c6 (diff) |
adding api.Externalable example, with Approve/AutoApprove and hooks
-rw-r--r-- | examples/LICENSE | 29 | ||||
-rw-r--r-- | examples/README.md | 14 | ||||
-rw-r--r-- | examples/externalable/README.md | 30 | ||||
-rw-r--r-- | examples/externalable/content/song.go | 165 |
4 files changed, 238 insertions, 0 deletions
diff --git a/examples/LICENSE b/examples/LICENSE new file mode 100644 index 0000000..720d6cd --- /dev/null +++ b/examples/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2016 Boss Sauce Creative, LLC. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..9789cab --- /dev/null +++ b/examples/README.md @@ -0,0 +1,14 @@ +## Examples + +Within this directory, you can find a number of different examples of how to use +Ponzu and its features. The goal of this directory is not to host full Ponzu +projects, but rather just a few files or a package to demonstrate interface +implementation, using hooks, adding new API endpoints, or creating/using addons. + +If you are making a contribution to the Ponzu project and are introducing a new +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/content/externalable) + diff --git a/examples/externalable/README.md b/examples/externalable/README.md new file mode 100644 index 0000000..5cd22a9 --- /dev/null +++ b/examples/externalable/README.md @@ -0,0 +1,30 @@ +# 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` + +## Song example +Imagine an app that lets users add Spotif 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 satify 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 new file mode 100644 index 0000000..f2b1b73 --- /dev/null +++ b/examples/externalable/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 } + +// 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 +} |