summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--management/editor/repeaters.go64
-rw-r--r--system/api/search.go12
-rw-r--r--system/db/content.go66
-rw-r--r--system/db/init.go3
-rw-r--r--system/item/item.go8
-rw-r--r--system/item/types.go3
-rw-r--r--system/search/search.go (renamed from system/db/search.go)53
8 files changed, 174 insertions, 36 deletions
diff --git a/README.md b/README.md
index c79c6c6..0aa531a 100644
--- a/README.md
+++ b/README.md
@@ -295,6 +295,7 @@ $ ponzu --dev -fork=github.com/nilslice/ponzu new /path/to/new/project
- [golang.org/x/text/transform](https://golang.org/x/text/transform)
- [golang.org/x/crypto/bcrypt](https://golang.org/x/crypto/bcrypt)
- [golang.org/x/net/http2](https://golang.org/x/net/http2)
+- [github.com/blevesearch/bleve](https://github.com/blevesearch/bleve)
- [github.com/nilslice/jwt](https://github.com/nilslice/jwt)
- [github.com/nilslice/email](https://github.com/nilslice/email)
- [github.com/gorilla/schema](https://github.com/gorilla/schema)
diff --git a/management/editor/repeaters.go b/management/editor/repeaters.go
index 250f2d2..bfdd335 100644
--- a/management/editor/repeaters.go
+++ b/management/editor/repeaters.go
@@ -215,8 +215,27 @@ func FileRepeater(fieldName string, p interface{}, attrs map[string]string) []by
clip = preview.find('.img-clip'),
reset = document.createElement('div'),
img = document.createElement('img'),
+ video = document.createElement('video'),
+ unknown = document.createElement('div'),
+ viewLink = document.createElement('a'),
+ viewLinkText = document.createTextNode('Download / View '),
+ iconLaunch = document.createElement('i'),
+ iconLaunchText = document.createTextNode('launch'),
uploadSrc = store.val();
+ video.setAttribute
preview.hide();
+ viewLink.setAttribute('href', '%[3]s');
+ viewLink.setAttribute('target', '_blank');
+ viewLink.appendChild(viewLinkText);
+ viewLink.style.display = 'block';
+ viewLink.style.marginRight = '10px';
+ viewLink.style.textAlign = 'right';
+ iconLaunch.className = 'material-icons tiny';
+ iconLaunch.style.position = 'relative';
+ iconLaunch.style.top = '3px';
+ iconLaunch.appendChild(iconLaunchText);
+ viewLink.appendChild(iconLaunch);
+ preview.append(viewLink);
// when %[2]s input changes (file is selected), remove
// the 'name' and 'value' attrs from the hidden store input.
@@ -226,15 +245,52 @@ func FileRepeater(fieldName string, p interface{}, attrs map[string]string) []by
});
if (uploadSrc.length > 0) {
- $(img).attr('src', store.val());
- clip.append(img);
+ var ext = uploadSrc.substring(uploadSrc.lastIndexOf('.'));
+ ext = ext.toLowerCase();
+ switch (ext) {
+ case '.jpg':
+ case '.jpeg':
+ case '.webp':
+ case '.gif':
+ case '.png':
+ $(img).attr('src', store.val());
+ clip.append(img);
+ break;
+ case '.mp4':
+ case '.webm':
+ $(video)
+ .attr('src', store.val())
+ .attr('type', 'video/'+ext.substring(1))
+ .attr('controls', true)
+ .css('width', '100%%');
+ clip.append(video);
+ break;
+ default:
+ $(img).attr('src', '/admin/static/dashboard/img/ponzu-file.png');
+ $(unknown)
+ .css({
+ position: 'absolute',
+ top: '10px',
+ left: '10px',
+ border: 'solid 1px #ddd',
+ padding: '7px 7px 5px 12px',
+ fontWeight: 'bold',
+ background: '#888',
+ color: '#fff',
+ textTransform: 'uppercase',
+ letterSpacing: '2px'
+ })
+ .text(ext);
+ clip.append(img);
+ clip.append(unknown);
+ clip.css('maxWidth', '200px');
+ }
preview.show();
$(reset).addClass('reset %[2]s btn waves-effect waves-light grey');
$(reset).html('<i class="material-icons tiny">clear<i>');
$(reset).on('click', function(e) {
e.preventDefault();
- var preview = $(this).parent().closest('.preview');
preview.animate({"opacity": 0.1}, 200, function() {
preview.slideUp(250, function() {
resetImage();
@@ -274,7 +330,7 @@ func FileRepeater(fieldName string, p interface{}, attrs map[string]string) []by
return nil
}
- _, err = html.WriteString(fmt.Sprintf(script, nameidx, className))
+ _, err = html.WriteString(fmt.Sprintf(script, nameidx, className, val))
if err != nil {
log.Println("Error writing HTML string to FileRepeater buffer")
return nil
diff --git a/system/api/search.go b/system/api/search.go
index ae6ac1c..9c7f0ae 100644
--- a/system/api/search.go
+++ b/system/api/search.go
@@ -8,6 +8,7 @@ import (
"github.com/ponzu-cms/ponzu/system/db"
"github.com/ponzu-cms/ponzu/system/item"
+ "github.com/ponzu-cms/ponzu/system/search"
)
func searchContentHandler(res http.ResponseWriter, req *http.Request) {
@@ -42,9 +43,9 @@ func searchContentHandler(res http.ResponseWriter, req *http.Request) {
}
// execute search for query provided, if no index for type send 404
- matches, err := db.SearchType(t, q)
- if err == db.ErrNoSearchIndex {
- res.WriteHeader(http.StatusBadRequest)
+ matches, err := search.TypeQuery(t, q)
+ if err == search.ErrNoIndex {
+ res.WriteHeader(http.StatusNotFound)
return
}
if err != nil {
@@ -61,6 +62,11 @@ func searchContentHandler(res http.ResponseWriter, req *http.Request) {
return
}
+ // if we have matches, push the first as its matched by relevance
+ if len(bb) > 0 {
+ push(res, req, it, bb[0])
+ }
+
var result = []json.RawMessage{}
for i := range bb {
result = append(result, bb[i])
diff --git a/system/db/content.go b/system/db/content.go
index 49cba87..b503a60 100644
--- a/system/db/content.go
+++ b/system/db/content.go
@@ -9,8 +9,11 @@ import (
"sort"
"strconv"
"strings"
+ "sync"
+ "time"
"github.com/ponzu-cms/ponzu/system/item"
+ "github.com/ponzu-cms/ponzu/system/search"
"github.com/boltdb/bolt"
"github.com/gorilla/schema"
@@ -124,9 +127,9 @@ func update(ns, id string, data url.Values, existingContent *[]byte) (int, error
go func() {
// update data in search index
target := fmt.Sprintf("%s:%s", ns, id)
- err = UpdateSearchIndex(target, string(j))
+ err = search.UpdateIndex(target, j)
if err != nil {
- log.Println("[search] UpdateSearchIndex Error:", err)
+ log.Println("[search] UpdateIndex Error:", err)
}
}()
@@ -252,9 +255,9 @@ func insert(ns string, data url.Values) (int, error) {
go func() {
// add data to seach index
target := fmt.Sprintf("%s:%s", ns, cid)
- err = UpdateSearchIndex(target, string(j))
+ err = search.UpdateIndex(target, j)
if err != nil {
- log.Println("[search] UpdateSearchIndex Error:", err)
+ log.Println("[search] UpdateIndex Error:", err)
}
}()
@@ -321,9 +324,9 @@ func DeleteContent(target string) error {
// delete indexed data from search index
if !strings.Contains(ns, "__") {
target = fmt.Sprintf("%s:%s", ns, id)
- err = DeleteSearchIndex(target)
+ err = search.DeleteIndex(target)
if err != nil {
- log.Println("[search] DeleteSearchIndex Error:", err)
+ log.Println("[search] DeleteIndex Error:", err)
}
}
}()
@@ -563,10 +566,61 @@ func Query(namespace string, opts QueryOptions) (int, [][]byte) {
return total, posts
}
+var sortContentCalls = make(map[string]time.Time)
+var waitDuration = time.Millisecond * 2000
+var sortMutex = &sync.Mutex{}
+
+func setLastInvocation(key string) {
+ sortMutex.Lock()
+ sortContentCalls[key] = time.Now()
+ sortMutex.Unlock()
+}
+
+func lastInvocation(key string) (time.Time, bool) {
+ sortMutex.Lock()
+ last, ok := sortContentCalls[key]
+ sortMutex.Unlock()
+ return last, ok
+}
+
+func enoughTime(key string) bool {
+ last, ok := lastInvocation(key)
+ if !ok {
+ // no invocation yet
+ // track next invocation
+ setLastInvocation(key)
+ return true
+ }
+
+ // if our required wait time has been met, return true
+ if time.Now().After(last.Add(waitDuration)) {
+ setLastInvocation(key)
+ return true
+ }
+
+ // dispatch a delayed invocation in case no additional one follows
+ go func() {
+ lastInvocationBeforeTimer, _ := lastInvocation(key) // zero value can be handled, no need for ok
+ enoughTimer := time.NewTimer(waitDuration)
+ <-enoughTimer.C
+ lastInvocationAfterTimer, _ := lastInvocation(key)
+ if !lastInvocationAfterTimer.After(lastInvocationBeforeTimer) {
+ SortContent(key)
+ }
+ }()
+
+ return false
+}
+
// 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) {
+ // wait if running too frequently per namespace
+ if !enoughTime(namespace) {
+ return
+ }
+
// only sort main content types i.e. Post
if strings.Contains(namespace, "__") {
return
diff --git a/system/db/init.go b/system/db/init.go
index 4e9c3cf..3fc35f5 100644
--- a/system/db/init.go
+++ b/system/db/init.go
@@ -4,6 +4,7 @@ import (
"log"
"github.com/ponzu-cms/ponzu/system/item"
+ "github.com/ponzu-cms/ponzu/system/search"
"github.com/boltdb/bolt"
"github.com/nilslice/jwt"
@@ -80,7 +81,7 @@ func Init() {
go func() {
for t := range item.Types {
- err := MapSearchIndex(t)
+ err := search.MapIndex(t)
if err != nil {
log.Fatalln(err)
return
diff --git a/system/item/item.go b/system/item/item.go
index 4750ef5..227cd26 100644
--- a/system/item/item.go
+++ b/system/item/item.go
@@ -211,7 +211,7 @@ func (i Item) AfterReject(res http.ResponseWriter, req *http.Request) error {
}
// SearchMapping returns a default implementation of a Bleve IndexMappingImpl
-// partially implements db.Searchable
+// partially implements search.Searchable
func (i Item) SearchMapping() (*mapping.IndexMappingImpl, error) {
mapping := bleve.NewIndexMapping()
mapping.StoreDynamic = false
@@ -219,6 +219,12 @@ func (i Item) SearchMapping() (*mapping.IndexMappingImpl, error) {
return mapping, nil
}
+// IndexContent determines if a type should be indexed for searching
+// partially implements search.Searchable
+func (i Item) IndexContent() bool {
+ return false
+}
+
// Slug returns a URL friendly string from the title of a post item
func Slug(i Identifiable) (string, error) {
// get the name of the post item
diff --git a/system/item/types.go b/system/item/types.go
index dbf13af..bcae58a 100644
--- a/system/item/types.go
+++ b/system/item/types.go
@@ -26,9 +26,6 @@ var (
// if requested by a valid admin or user
ErrAllowHiddenItem = errors.New(`Allow hidden item`)
- // ErrNoSearchMapping can be used to tell the system not to create an index mapping
- ErrNoSearchMapping = errors.New(`No search mapping for item`)
-
// Types is a map used to reference a type name to its actual Editable type
// mainly for lookups in /admin route based utilities
Types map[string]func() interface{}
diff --git a/system/db/search.go b/system/search/search.go
index 3e7a9d6..6d538ed 100644
--- a/system/db/search.go
+++ b/system/search/search.go
@@ -1,4 +1,4 @@
-package db
+package search
import (
"errors"
@@ -9,6 +9,8 @@ import (
"github.com/ponzu-cms/ponzu/system/item"
+ "encoding/json"
+
"github.com/blevesearch/bleve"
"github.com/blevesearch/bleve/mapping"
)
@@ -17,37 +19,40 @@ var (
// Search tracks all search indices to use throughout system
Search map[string]bleve.Index
- // ErrNoSearchIndex is for failed checks for an index in Search map
- ErrNoSearchIndex = errors.New("No search index found for type provided")
+ // ErrNoIndex is for failed checks for an index in Search map
+ ErrNoIndex = errors.New("No search index found for type provided")
)
// Searchable ...
type Searchable interface {
SearchMapping() (*mapping.IndexMappingImpl, error)
+ IndexContent() bool
}
func init() {
Search = make(map[string]bleve.Index)
}
-// MapSearchIndex creates the mapping for a type and tracks the index to be used within
+// MapIndex creates the mapping for a type and tracks the index to be used within
// the system for adding/deleting/checking data
-func MapSearchIndex(typeName string) error {
+func MapIndex(typeName string) error {
// type assert for Searchable, get configuration (which can be overridden)
// by Ponzu user if defines own SearchMapping()
it, ok := item.Types[typeName]
if !ok {
- return fmt.Errorf("[search] MapSearchIndex Error: Failed to MapIndex for %s, type doesn't exist", typeName)
+ return fmt.Errorf("[search] MapIndex Error: Failed to MapIndex for %s, type doesn't exist", typeName)
}
s, ok := it().(Searchable)
if !ok {
- return fmt.Errorf("[search] MapSearchIndex Error: Item type %s doesn't implement db.Searchable", typeName)
+ return fmt.Errorf("[search] MapIndex Error: Item type %s doesn't implement search.Searchable", typeName)
}
- mapping, err := s.SearchMapping()
- if err == item.ErrNoSearchMapping {
+ // skip setting or using index for types that shouldn't be indexed
+ if !s.IndexContent() {
return nil
}
+
+ mapping, err := s.SearchMapping()
if err != nil {
return err
}
@@ -88,25 +93,37 @@ func MapSearchIndex(typeName string) error {
return nil
}
-// UpdateSearchIndex sets data into a content type's search index at the given
+// UpdateIndex sets data into a content type's search index at the given
// identifier
-func UpdateSearchIndex(id string, data interface{}) error {
+func UpdateIndex(id string, data interface{}) error {
// check if there is a search index to work with
target := strings.Split(id, ":")
ns := target[0]
idx, ok := Search[ns]
if ok {
+ // unmarshal json to struct, error if not registered
+ it, ok := item.Types[ns]
+ if !ok {
+ return fmt.Errorf("[search] UpdateIndex Error: type '%s' doesn't exist", ns)
+ }
+
+ p := it()
+ err := json.Unmarshal(data.([]byte), &p)
+ if err != nil {
+ return err
+ }
+
// add data to search index
- return idx.Index(id, data)
+ return idx.Index(id, p)
}
return nil
}
-// DeleteSearchIndex removes data from a content type's search index at the
+// DeleteIndex removes data from a content type's search index at the
// given identifier
-func DeleteSearchIndex(id string) error {
+func DeleteIndex(id string) error {
// check if there is a search index to work with
target := strings.Split(id, ":")
ns := target[0]
@@ -120,13 +137,13 @@ func DeleteSearchIndex(id string) error {
return nil
}
-// SearchType conducts a search and returns a set of Ponzu "targets", Type:ID pairs,
+// TypeQuery conducts a search and returns a set of Ponzu "targets", Type:ID pairs,
// and an error. If there is no search index for the typeName (Type) provided,
-// db.ErrNoSearchIndex will be returned as the error
-func SearchType(typeName, query string) ([]string, error) {
+// db.ErrNoIndex will be returned as the error
+func TypeQuery(typeName, query string) ([]string, error) {
idx, ok := Search[typeName]
if !ok {
- return nil, ErrNoSearchIndex
+ return nil, ErrNoIndex
}
q := bleve.NewQueryStringQuery(query)