diff options
author | Steve <nilslice@gmail.com> | 2016-10-19 00:09:28 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-10-19 00:09:28 -0700 |
commit | 0c77f3e89ac26913ce4b7de68fe9f5589ae77d8f (patch) | |
tree | 859e76f6d1335133ba8f62da72f068e79331880c | |
parent | 2f3985491363dc0658ad8cf3a415a77c1825a67a (diff) | |
parent | c74bcc2ec1be59ded3634de1a871c73d9dffba98 (diff) |
Merge pull request #3 from bosssauce/ponzu-dev
[fundamental feature] Content type posts are sorted by time and cached in Admin, API coming soon.
-rw-r--r-- | content/item.go | 15 | ||||
-rw-r--r-- | management/editor/editor.go | 11 | ||||
-rw-r--r-- | management/editor/elements.go | 3 | ||||
-rw-r--r-- | management/manager/manager.go | 21 | ||||
-rw-r--r-- | system/admin/config/config.go | 4 | ||||
-rw-r--r-- | system/admin/handlers.go | 159 | ||||
-rw-r--r-- | system/admin/static/dashboard/css/admin.css | 23 | ||||
-rw-r--r-- | system/admin/static/editor/js/materialNote.js | 4 | ||||
-rw-r--r-- | system/db/content.go | 94 | ||||
-rw-r--r-- | system/db/init.go | 14 |
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 |