Skip to content

Commit

Permalink
Merge pull request #99 from ponzu-cms/ponzu-dev
Browse files Browse the repository at this point in the history
[core] Add api.Deleteable interface, rename Externalable to Createable and rename methods
  • Loading branch information
nilslice authored Mar 15, 2017
2 parents 0eaddb8 + 5ec811b commit 1a62c2b
Show file tree
Hide file tree
Showing 15 changed files with 404 additions and 90 deletions.
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Externalable
# 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/external?type=<Type>`
to the API endpoint `/api/content/create?type=<Type>`

## Song example
Imagine an app that lets users add Spotify music to a global playlist, and you need them
Expand All @@ -23,7 +23,7 @@ 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
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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/ponzu-cms/ponzu/management/editor"
"github.com/ponzu-cms/ponzu/system/admin/user"
"github.com/ponzu-cms/ponzu/system/api"
"github.com/ponzu-cms/ponzu/system/item"
)

Expand Down Expand Up @@ -78,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 }

// Accept implements api.Externalable, and allows external POST requests from clients
// 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) Accept(res http.ResponseWriter, req *http.Request) error {
func (s *Song) Create(res http.ResponseWriter, req *http.Request) error {
// do form data validation for required fields
required := []string{
"title",
Expand All @@ -101,31 +102,29 @@ func (s *Song) Accept(res http.ResponseWriter, req *http.Request) error {
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
// 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) BeforeAccept(res http.ResponseWriter, req *http.Request) error {
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
return api.ErrNoAuth
}

// you could then to data validation on the request post form, or do it in
// the Accept method, which is called after BeforeAccept
// the Create method, which is called after BeforeAPICreate

return nil
}

// AfterAccept is called after Accept, and is useful for logging or triggering
// 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) AfterAccept(res http.ResponseWriter, req *http.Request) error {
func (s *Song) AfterAPICreate(res http.ResponseWriter, req *http.Request) error {
addr := req.RemoteAddr
log.Println("Song sent by:", addr, "titled:", req.PostFormValue("title"))

Expand All @@ -149,7 +148,7 @@ func (s *Song) Approve(res http.ResponseWriter, req *http.Request) error {
*/

// AutoApprove implements api.Trustable, and will automatically approve content
// that has been submitted by an external client via api.Externalable. Be careful
// 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
Expand Down
31 changes: 31 additions & 0 deletions examples/deleteable/README.md
Original file line number Diff line number Diff line change
@@ -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 `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!

116 changes: 116 additions & 0 deletions examples/deleteable/content/song.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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/api"
"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) {
return api.ErrNoAuth
}

// 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
}
4 changes: 2 additions & 2 deletions examples/updateable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
21 changes: 10 additions & 11 deletions examples/updateable/content/song.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/ponzu-cms/ponzu/management/editor"
"github.com/ponzu-cms/ponzu/system/admin/user"
"github.com/ponzu-cms/ponzu/system/api"
"github.com/ponzu-cms/ponzu/system/item"
)

Expand Down Expand Up @@ -79,31 +80,29 @@ 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

// 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
return api.ErrNoAuth
}

// 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"))

Expand All @@ -129,11 +128,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"))

Expand Down
6 changes: 3 additions & 3 deletions system/admin/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) {
}

var hasExt bool
_, ok = pt.(api.Externalable)
_, ok = pt.(api.Createable)
if ok {
hasExt = true
}
Expand Down Expand Up @@ -1409,7 +1409,7 @@ func approveContentHandler(res http.ResponseWriter, req *http.Request) {
}

if pendingID != "" {
err = db.DeleteContent(req.FormValue("type")+":"+pendingID, req.Form)
err = db.DeleteContent(req.FormValue("type") + ":" + pendingID)
if err != nil {
log.Println("Failed to remove content after approval:", err)
}
Expand Down Expand Up @@ -1757,7 +1757,7 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) {
return
}

err = db.DeleteContent(t+":"+id, req.Form)
err = db.DeleteContent(t + ":" + id)
if err != nil {
log.Println(err)
res.WriteHeader(http.StatusInternalServerError)
Expand Down
Loading

0 comments on commit 1a62c2b

Please sign in to comment.