summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--content/item.go15
-rw-r--r--management/editor/editor.go11
-rw-r--r--management/editor/elements.go3
-rw-r--r--management/manager/manager.go21
-rw-r--r--system/admin/config/config.go4
-rw-r--r--system/admin/handlers.go159
-rw-r--r--system/admin/static/dashboard/css/admin.css23
-rw-r--r--system/admin/static/editor/js/materialNote.js4
-rw-r--r--system/db/content.go94
-rw-r--r--system/db/init.go14
10 files changed, 315 insertions, 33 deletions
diff --git a/content/item.go b/content/item.go
index 7b25b14..f4a5489 100644
--- a/content/item.go
+++ b/content/item.go
@@ -7,3 +7,18 @@ type Item struct {
Timestamp int64 `json:"timestamp"`
Updated int64 `json:"updated"`
}
+
+// Time partially implements the Sortable interface
+func (i Item) Time() int64 {
+ return i.Timestamp
+}
+
+// Touch partially implements the Sortable interface
+func (i Item) Touch() int64 {
+ return i.Updated
+}
+
+// ContentID partially implements the Sortable interface
+func (i Item) ContentID() int {
+ return i.ID
+}
diff --git a/management/editor/editor.go b/management/editor/editor.go
index dc6f181..3b26adb 100644
--- a/management/editor/editor.go
+++ b/management/editor/editor.go
@@ -16,6 +16,13 @@ type Editable interface {
MarshalEditor() ([]byte, error)
}
+// Sortable ensures data is sortable by time
+type Sortable interface {
+ Time() int64
+ Touch() int64
+ ContentID() int
+}
+
// Editor is a view containing fields to manage content
type Editor struct {
ViewBuf *bytes.Buffer
@@ -45,7 +52,7 @@ func Form(post Editable, fields ...Field) ([]byte, error) {
editor.ViewBuf.Write([]byte(`<tr class="col s4 default-fields"><td>`))
publishTime := `
-<div class="row">
+<div class="row content-only __ponzu">
<div class="input-field col s6">
<label class="active">MM</label>
<select class="month __ponzu browser-default">
@@ -73,7 +80,7 @@ func Form(post Editable, fields ...Field) ([]byte, error) {
</div>
</div>
-<div class="row">
+<div class="row content-only __ponzu">
<div class="input-field col s3">
<label class="active">HH</label>
<input value="" class="hour __ponzu" maxlength="2" type="text" placeholder="HH" />
diff --git a/management/editor/elements.go b/management/editor/elements.go
index 390d8df..4d829ad 100644
--- a/management/editor/elements.go
+++ b/management/editor/elements.go
@@ -128,7 +128,6 @@ func File(fieldName string, p interface{}, attrs map[string]string) []byte {
store.attr('name', '');
upload.attr('name', '` + name + `');
clip.empty();
- console.log('clicked');
}
});
</script>`
@@ -198,10 +197,8 @@ func Richtext(fieldName string, p interface{}, attrs map[string]string) []byte {
contentType: false,
processData: false,
success: function(resp) {
- console.log(resp);
var img = document.createElement('img');
img.setAttribute('src', resp.data[0].url);
- console.log(img);
_editor.materialnote('insertNode', img);
},
error: function(xhr, status, err) {
diff --git a/management/manager/manager.go b/management/manager/manager.go
index a8665ba..c0c5519 100644
--- a/management/manager/manager.go
+++ b/management/manager/manager.go
@@ -24,14 +24,14 @@ const managerHTML = `
});
var updateTimestamp = function(dt, $ts) {
- var year = dt.year.val(),
- month = dt.month.val()-1,
- day = dt.day.val(),
- hour = dt.hour.val(),
- minute = dt.minute.val();
-
- if (dt.period == "PM") {
- hours = hours + 12;
+ var year = parseInt(dt.year.val()),
+ month = parseInt(dt.month.val())-1,
+ day = parseInt(dt.day.val()),
+ hour = parseInt(dt.hour.val()),
+ minute = parseInt(dt.minute.val());
+
+ if (dt.period.val() === "PM") {
+ hour = hour + 12;
}
var date = new Date(year, month, day, hour, minute);
@@ -39,7 +39,7 @@ const managerHTML = `
$ts.val(date.getTime());
}
- var setDefaultTimeAndDate = function(dt, $ts, $up, unix) {
+ var setDefaultTimeAndDate = function(dt, unix) {
var time = getPartialTime(unix),
date = getPartialDate(unix);
@@ -79,7 +79,7 @@ const managerHTML = `
time = (new Date()).getTime();
}
- setDefaultTimeAndDate(getFields(), timestamp, updated, time);
+ setDefaultTimeAndDate(getFields(), time);
var timeUpdated = false;
$('form').on('submit', function(e) {
@@ -91,6 +91,7 @@ const managerHTML = `
e.preventDefault();
updateTimestamp(getFields(), timestamp);
+ updated.val((new Date()).getTime());
timeUpdated = true;
$('form').submit();
diff --git a/system/admin/config/config.go b/system/admin/config/config.go
index c5ef1cd..c83c311 100644
--- a/system/admin/config/config.go
+++ b/system/admin/config/config.go
@@ -97,6 +97,10 @@ func (c *Config) MarshalEditor() ([]byte, error) {
right: '0px'
});
+ var contentOnly = $('.content-only.__ponzu');
+ contentOnly.hide();
+ contentOnly.find('input, textarea, select').attr('name', '');
+
// adjust layout of td so save button is in same location as usual
fields.find('td').css('float', 'right');
diff --git a/system/admin/handlers.go b/system/admin/handlers.go
index edf351d..de340ae 100644
--- a/system/admin/handlers.go
+++ b/system/admin/handlers.go
@@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ "log"
"net/http"
"strings"
"time"
@@ -289,7 +290,9 @@ func postsHandler(res http.ResponseWriter, req *http.Request) {
return
}
- posts := db.ContentAll(t)
+ order := strings.ToLower(q.Get("order"))
+
+ posts := db.ContentAll(t + "_sorted")
b := &bytes.Buffer{}
p, ok := content.Types[t]().(editor.Editable)
if !ok {
@@ -306,29 +309,113 @@ func postsHandler(res http.ResponseWriter, req *http.Request) {
html := `<div class="col s9 card">
<div class="card-content">
<div class="row">
- <div class="card-title col s7">` + t + ` Items</div>
- <form class="col s5" action="/admin/posts/search" method="get">
+ <div class="col s8">
+ <div class="row">
+ <div class="card-title col s7">` + t + ` Items</div>
+ <div class="col s5 input-field inline">
+ <select class="browser-default __ponzu sort-order">
+ <option value="DESC">New to Old</option>
+ <option value="ASC">Old to New</option>
+ </select>
+ <label class="active">Sort:</label>
+ </div>
+ <script>
+ $(function() {
+ var getParam = function(param) {
+ var qs = window.location.search.substring(1);
+ var qp = qs.split('&');
+ var t = '';
+
+ for (var i = 0; i < qp.length; i++) {
+ var p = qp[i].split('=')
+ if (p[0] === param) {
+ t = p[1];
+ }
+ }
+
+ return t;
+ }
+
+ var sort = $('select.__ponzu.sort-order');
+
+ sort.on('change', function() {
+ var path = window.location.pathname;
+ var s = sort.val();
+ var t = getParam('type');
+
+ window.location.replace(path + '?type=' + t + '&order=' + s)
+ });
+
+ var order = getParam('order');
+ if (order !== '') {
+ sort.val(order);
+ }
+
+ });
+ </script>
+ </div>
+ </div>
+ <form class="col s4" action="/admin/posts/search" method="get">
<div class="input-field post-search inline">
+ <label class="active">Search:</label>
<i class="right material-icons search-icon">search</i>
- <input class="search" name="q" type="text" placeholder="Search for ` + t + ` content" class="search"/>
+ <input class="search" name="q" type="text" placeholder="Within all ` + t + ` fields" class="search"/>
<input type="hidden" name="type" value="` + t + `" />
</div>
</form>
</div>
<ul class="posts row">`
- for i := range posts {
- json.Unmarshal(posts[i], &p)
- post := `<li class="col s12"><a href="/admin/edit?type=` +
- t + `&id=` + fmt.Sprintf("%d", p.ContentID()) +
- `">` + p.ContentName() + `</a></li>`
- b.Write([]byte(post))
+ if order == "desc" || order == "" {
+ // keep natural order of posts slice, as returned from sorted bucket
+ for i := range posts {
+ err := json.Unmarshal(posts[i], &p)
+ if err != nil {
+ log.Println("Error unmarshal json into", t, err, posts[i])
+
+ post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
+ b.Write([]byte(post))
+ continue
+ }
+
+ post := adminPostListItem(p, t)
+ b.Write(post)
+ }
+
+ } else if order == "asc" {
+ // reverse the order of posts slice
+ for i := len(posts) - 1; i >= 0; i-- {
+ err := json.Unmarshal(posts[i], &p)
+ if err != nil {
+ log.Println("Error unmarshal json into", t, err, posts[i])
+
+ post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
+ b.Write([]byte(post))
+ continue
+ }
+
+ post := adminPostListItem(p, t)
+ b.Write(post)
+ }
}
b.Write([]byte(`</ul></div></div>`))
+ script := `
+ <script>
+ $(function() {
+ var del = $('.quick-delete-post.__ponzu span');
+ del.on('click', function(e) {
+ if (confirm("[Ponzu] Please confirm:\n\nAre you sure you want to delete this post?\nThis cannot be undone.")) {
+ $(e.target).parent().submit();
+ }
+ });
+ });
+ </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() + btn
+ html = html + b.String() + script + btn
adminView, err := Admin([]byte(html))
if err != nil {
@@ -341,6 +428,40 @@ func postsHandler(res http.ResponseWriter, req *http.Request) {
res.Write(adminView)
}
+// adminPostListItem is a helper to create the li containing a post.
+// p is the asserted post as an Editable, t is the Type of the post.
+func adminPostListItem(p editor.Editable, t string) []byte {
+ s, ok := p.(editor.Sortable)
+ if !ok {
+ log.Println("Content type", t, "doesn't implement editor.Sortable")
+ post := `<li class="col s12">Error retreiving data. Your data type doesn't implement necessary interfaces.</li>`
+ return []byte(post)
+ }
+
+ // use sort to get other info to display in admin UI post list
+ tsTime := time.Unix(int64(s.Time()/1000), 0)
+ upTime := time.Unix(int64(s.Touch()/1000), 0)
+ updatedTime := upTime.Format("01/02/06 03:04 PM")
+ publishTime := tsTime.Format("01/02/06")
+
+ cid := fmt.Sprintf("%d", p.ContentID())
+
+ post := `
+ <li class="col s12">
+ <a href="/admin/edit?type=` + t + `&id=` + cid + `">` + p.ContentName() + `</a>
+ <span class="post-detail">Updated: ` + updatedTime + `</span>
+ <span class="publish-date right">` + publishTime + `</span>
+
+ <form enctype="multipart/form-data" class="quick-delete-post __ponzu right" action="/admin/edit/delete" method="post">
+ <span>Delete</span>
+ <input type="hidden" name="id" value="` + cid + `" />
+ <input type="hidden" name="type" value="` + t + `" />
+ </form>
+ </li>`
+
+ return []byte(post)
+}
+
func editHandler(res http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
@@ -518,6 +639,7 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) {
err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
if err != nil {
+ fmt.Println("req.ParseMPF")
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -532,6 +654,7 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) {
err = db.DeleteContent(t + ":" + id)
if err != nil {
+ fmt.Println("db.DeleteContent")
res.WriteHeader(http.StatusInternalServerError)
return
}
@@ -594,10 +717,16 @@ func searchHandler(res http.ResponseWriter, req *http.Request) {
continue
}
- json.Unmarshal(posts[i], &p)
- post := `<li class="col s12"><a href="/admin/edit?type=` +
- t + `&id=` + fmt.Sprintf("%d", p.ContentID()) +
- `">` + p.ContentName() + `</a></li>`
+ err := json.Unmarshal(posts[i], &p)
+ if err != nil {
+ log.Println("Error unmarshal search result json into", t, err, posts[i])
+
+ post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
+ b.Write([]byte(post))
+ continue
+ }
+
+ post := adminPostListItem(p, t)
b.Write([]byte(post))
}
diff --git a/system/admin/static/dashboard/css/admin.css b/system/admin/static/dashboard/css/admin.css
index 7da4154..e23658e 100644
--- a/system/admin/static/dashboard/css/admin.css
+++ b/system/admin/static/dashboard/css/admin.css
@@ -158,6 +158,29 @@ footer p {
margin-left: 10px;
}
+span.post-detail {
+ font-size: 11px;
+ color: #9e9e9e;
+ font-style: italic;
+}
+
+.quick-delete-post {
+ display: none;
+}
+
+li:hover .quick-delete-post {
+ display: inline-block;
+}
+
+.quick-delete-post span {
+ cursor: pointer;
+ color: #F44336;
+ text-transform: uppercase;
+ font-size: 11px;
+ font-weight: bold;
+ margin-right: 20px;
+}
+
/* OVERRIDE Bootstrap + Materialize conflicts */
.iso-texteditor.input-field label {
diff --git a/system/admin/static/editor/js/materialNote.js b/system/admin/static/editor/js/materialNote.js
index a036053..a43fc05 100644
--- a/system/admin/static/editor/js/materialNote.js
+++ b/system/admin/static/editor/js/materialNote.js
@@ -6108,7 +6108,7 @@ var dom = (function() {
var hasDefaultFont = agent.isFontInstalled(options.defaultFontName);
var defaultFontName = (hasDefaultFont) ? options.defaultFontName : realFontList[0];
var label = '<div class="note-current-fontname">' + defaultFontName + '</div>';
- console.log('editing right file...')
+ // console.log('editing right file...')
return tplButton(label, {
title: lang.font.name,
className: 'note-fontname',
@@ -6886,7 +6886,7 @@ var dom = (function() {
var isFullscreen = $editor.hasClass('fullscreen');
if (isFullscreen) {
- console.log("fullscreen");
+ // console.log("fullscreen");
return false;
}
diff --git a/system/db/content.go b/system/db/content.go
index 1e5b95a..2d6e8ea 100644
--- a/system/db/content.go
+++ b/system/db/content.go
@@ -4,7 +4,9 @@ import (
"bytes"
"encoding/json"
"fmt"
+ "log"
"net/url"
+ "sort"
"strconv"
"strings"
@@ -63,6 +65,8 @@ func update(ns, id string, data url.Values) (int, error) {
return 0, nil
}
+ go SortContent(ns)
+
return cid, nil
}
@@ -103,6 +107,8 @@ func insert(ns string, data url.Values) (int, error) {
return 0, err
}
+ go SortContent(ns)
+
return effectedID, nil
}
@@ -152,6 +158,12 @@ func DeleteContent(target string) error {
return err
}
+ // exception to typical "run in goroutine" pattern:
+ // we want to have an updated admin view as soon as this is deleted, so
+ // in some cases, the delete and redirect is faster than the sort,
+ // thus still showing a deleted post in the admin view.
+ SortContent(ns)
+
return nil
}
@@ -199,3 +211,85 @@ func ContentAll(namespace string) [][]byte {
return posts
}
+
+// SortContent sorts all content of the type supplied as the namespace by time,
+// in descending order, from most recent to least recent
+// Should be called from a goroutine after SetContent is successful
+func SortContent(namespace string) {
+ all := ContentAll(namespace)
+
+ var posts sortablePosts
+ // decode each (json) into Editable
+ for i := range all {
+ j := all[i]
+ post := content.Types[namespace]()
+
+ err := json.Unmarshal(j, &post)
+ if err != nil {
+ log.Println("Error decoding json while sorting", namespace, ":", err)
+ return
+ }
+
+ posts = append(posts, post.(editor.Sortable))
+ }
+
+ // sort posts
+ sort.Sort(posts)
+
+ // store in <namespace>_sorted bucket, first delete existing
+ err := store.Update(func(tx *bolt.Tx) error {
+ err := tx.DeleteBucket([]byte(namespace + "_sorted"))
+ if err != nil {
+ return err
+ }
+
+ b, err := tx.CreateBucket([]byte(namespace + "_sorted"))
+ if err != nil {
+ err := tx.Rollback()
+ if err != nil {
+ return err
+ }
+
+ return err
+ }
+
+ // encode to json and store as 'i:post.Time()':post
+ for i := range posts {
+ j, err := json.Marshal(posts[i])
+ if err != nil {
+ return err
+ }
+
+ cid := fmt.Sprintf("%d:%d", i, posts[i].Time())
+ err = b.Put([]byte(cid), j)
+ if err != nil {
+ err := tx.Rollback()
+ if err != nil {
+ return err
+ }
+
+ return err
+ }
+ }
+
+ return nil
+ })
+ if err != nil {
+ log.Println("Error while updating db with sorted", namespace, err)
+ }
+
+}
+
+type sortablePosts []editor.Sortable
+
+func (s sortablePosts) Len() int {
+ return len(s)
+}
+
+func (s sortablePosts) Less(i, j int) bool {
+ return s[i].Time() > s[j].Time()
+}
+
+func (s sortablePosts) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
diff --git a/system/db/init.go b/system/db/init.go
index 2d73b35..1a5ed25 100644
--- a/system/db/init.go
+++ b/system/db/init.go
@@ -22,12 +22,17 @@ func Init() {
}
err = store.Update(func(tx *bolt.Tx) error {
- // initialize db with all content type buckets
+ // initialize db with all content type buckets & sorted bucket for type
for t := range content.Types {
_, err := tx.CreateBucketIfNotExists([]byte(t))
if err != nil {
return err
}
+
+ _, err = tx.CreateBucketIfNotExists([]byte(t + "_sorted"))
+ if err != nil {
+ return err
+ }
}
// init db with other buckets as needed
@@ -65,6 +70,13 @@ func Init() {
log.Fatal("Coudn't initialize db with buckets.", err)
}
+ // sort all content into type_sorted buckets
+ go func() {
+ for t := range content.Types {
+ SortContent(t)
+ }
+ }()
+
}
// SystemInitComplete checks if there is at least 1 admin user in the db which