Skip to content

Commit

Permalink
Merge pull request #142 from ponzu-cms/ponzu-dev
Browse files Browse the repository at this point in the history
[core] add CSVFormattable interface to export CSV formatted content
  • Loading branch information
nilslice authored May 15, 2017
2 parents 0cf8aa5 + 0ee16cc commit f176e7d
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 5 deletions.
9 changes: 9 additions & 0 deletions management/format/csv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Package format provides interfaces to format content into various kinds of
// data
package format

// CSVFormattable is implemented with the method FormatCSV, which must return the ordered
// slice of JSON struct tag names for the type implmenting it
type CSVFormattable interface {
FormatCSV() []string
}
142 changes: 142 additions & 0 deletions system/admin/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package admin

import (
"encoding/csv"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"

"github.com/ponzu-cms/ponzu/management/format"
"github.com/ponzu-cms/ponzu/system/db"
"github.com/ponzu-cms/ponzu/system/item"

"github.com/tidwall/gjson"
)

func exportHandler(res http.ResponseWriter, req *http.Request) {
// /admin/contents/export?type=Blogpost&format=csv
q := req.URL.Query()
t := q.Get("type")
f := strings.ToLower(q.Get("format"))

if t == "" || f == "" {
v, err := Error400()
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
}

res.WriteHeader(http.StatusBadRequest)
_, err = res.Write(v)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
}

}

pt, ok := item.Types[t]
if !ok {
res.WriteHeader(http.StatusBadRequest)
return
}

switch f {
case "csv":
csv, ok := pt().(format.CSVFormattable)
if !ok {
res.WriteHeader(http.StatusBadRequest)
return
}

fields := csv.FormatCSV()
exportCSV(res, req, pt, fields)

default:
res.WriteHeader(http.StatusBadRequest)
return
}
}

func exportCSV(res http.ResponseWriter, req *http.Request, pt func() interface{}, fields []string) {
tmpFile, err := ioutil.TempFile(os.TempDir(), "exportcsv-")
if err != nil {
log.Println("Failed to create tmp file for CSV export:", err)
res.WriteHeader(http.StatusInternalServerError)
return
}

err = os.Chmod(tmpFile.Name(), 0666)
if err != nil {
log.Println("chmod err:", err)
}

csvBuf := csv.NewWriter(tmpFile)

t := req.URL.Query().Get("type")

// get content data and loop through creating a csv row per result
bb := db.ContentAll(t)

// add field names to first row
err = csvBuf.Write(fields)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
log.Println("Failed to write column headers:", fields)
return
}

for row := range bb {
// unmarshal data and loop over fields
rowBuf := []string{}

for _, col := range fields {
// pull out each field as the column value
result := gjson.GetBytes(bb[row], col)

// append it to the buffer
rowBuf = append(rowBuf, result.String())
}

// write row to csv
err := csvBuf.Write(rowBuf)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
log.Println("Failed to write column headers:", fields)
return
}
}

csvBuf.Flush()

// write the buffer to a content-disposition response
fi, err := tmpFile.Stat()
if err != nil {
log.Println("Failed to read tmp file info for CSV export:", err)
res.WriteHeader(http.StatusInternalServerError)
return
}

err = tmpFile.Close()
if err != nil {
log.Println("Failed to close tmp file for CSV export:", err)
}

ts := time.Now().Unix()
disposition := `attachment; filename="export-%s-%d.csv"`

res.Header().Set("Content-Type", "text/csv")
res.Header().Set("Content-Disposition", fmt.Sprintf(disposition, t, ts))
res.Header().Set("Content-Length", fmt.Sprintf("%d", int(fi.Size())))

http.ServeFile(res, req, tmpFile.Name())

err = os.Remove(tmpFile.Name())
if err != nil {
log.Println("Failed to remove tmp file for CSV export:", err)
}
}
43 changes: 39 additions & 4 deletions system/admin/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/ponzu-cms/ponzu/management/editor"
"github.com/ponzu-cms/ponzu/management/format"
"github.com/ponzu-cms/ponzu/management/manager"
"github.com/ponzu-cms/ponzu/system/addon"
"github.com/ponzu-cms/ponzu/system/admin/config"
Expand Down Expand Up @@ -1492,8 +1493,20 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) {
</script>
`

btn := `<div class="col s3"><a href="/admin/edit?type=` + t + `" class="btn new-post waves-effect waves-light">New ` + t + `</a></div></div>`
html = html + b.String() + script + btn
btn := `<div class="col s3">
<a href="/admin/edit?type=` + t + `" class="btn new-post waves-effect waves-light">
New ` + t + `
</a>`

if _, ok := pt.(format.CSVFormattable); ok {
btn += `<br/>
<a href="/admin/contents/export?type=` + t + `&format=csv" class="green darken-4 btn export-post waves-effect waves-light">
<i class="material-icons left">system_update_alt</i>
CSV
</a>`
}

html += b.String() + script + btn + `</div></div>`

adminView, err := Admin([]byte(html))
if err != nil {
Expand Down Expand Up @@ -2422,7 +2435,15 @@ func searchHandler(res http.ResponseWriter, req *http.Request) {

posts := db.ContentAll(t + specifier)
b := &bytes.Buffer{}
p := item.Types[t]().(editor.Editable)
pt, ok := item.Types[t]
if !ok {
res.WriteHeader(http.StatusBadRequest)
return
}

post := pt()

p := post.(editor.Editable)

html := `<div class="col s9 card">
<div class="card-content">
Expand Down Expand Up @@ -2499,9 +2520,23 @@ func searchHandler(res http.ResponseWriter, req *http.Request) {
return
}

btn := `<div class="col s3"><a href="/admin/edit?type=` + t + `" class="btn new-post waves-effect waves-light">New ` + t + `</a></div></div>`
btn := `<div class="col s3">
<a href="/admin/edit?type=` + t + `" class="btn new-post waves-effect waves-light">
New ` + t + `
</a>`
html = html + b.String() + btn

if _, ok := post.(format.CSVFormattable); ok {
btn = `<br/>
<a href="/admin/contents/export?type=` + t + `&format=csv" class="green darken-4 btn export-post waves-effect waves-light">
<i class="material-icons left">system_update_alt</i>
CSV
</a>`
html = html + b.String() + btn
}

html += `</div></div>`

adminView, err := Admin([]byte(html))
if err != nil {
log.Println(err)
Expand Down
1 change: 1 addition & 0 deletions system/admin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func Run() {

http.HandleFunc("/admin/contents", user.Auth(contentsHandler))
http.HandleFunc("/admin/contents/search", user.Auth(searchHandler))
http.HandleFunc("/admin/contents/export", user.Auth(exportHandler))

http.HandleFunc("/admin/edit", user.Auth(editHandler))
http.HandleFunc("/admin/edit/delete", user.Auth(deleteHandler))
Expand Down
2 changes: 1 addition & 1 deletion system/admin/static/dashboard/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ li a:hover {
transition: color 0.3s ease;
}

a.new-post {
a.new-post, a.export-post {
margin: 0.5rem 0 1rem 0.75rem;
}

Expand Down

0 comments on commit f176e7d

Please sign in to comment.