Search string parser similar to GitHub, see #2
This commit is contained in:
parent
1533f60a1a
commit
9a320c60df
6 changed files with 141 additions and 19 deletions
|
@ -76,7 +76,7 @@ class Gallery {
|
|||
counterEl: false,
|
||||
arrowEl: true,
|
||||
preloaderEl: true,
|
||||
getImageURLForShare: function() { return gallery.currItem.download_url},
|
||||
getImageURLForShare: function() { return gallery.currItem.download_url;},
|
||||
};
|
||||
|
||||
let photosWithSizes = this.photosWithSizes();
|
||||
|
|
14
internal/forms/forms_test.go
Normal file
14
internal/forms/forms_test.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package forms
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
|
@ -1,21 +1,100 @@
|
|||
package forms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Query parameters for GET /api/v1/photos
|
||||
type PhotoSearchForm struct {
|
||||
Query string `form:"q"`
|
||||
Location bool `form:"location"`
|
||||
Tags string `form:"tags"`
|
||||
Cat string `form:"cat"`
|
||||
Country string `form:"country"`
|
||||
CameraID int `form:"camera"`
|
||||
Order string `form:"order"`
|
||||
Count int `form:"count" binding:"required"`
|
||||
Offset int `form:"offset"`
|
||||
Before time.Time `form:"before" time_format:"2006-01-02"`
|
||||
After time.Time `form:"after" time_format:"2006-01-02"`
|
||||
FavoritesOnly bool `form:"favorites"`
|
||||
Query string `form:"q"`
|
||||
Location bool `form:"location"`
|
||||
Tags string `form:"tags"`
|
||||
Cat string `form:"cat"`
|
||||
Country string `form:"country"`
|
||||
Color string `form:"color"`
|
||||
Camera int `form:"camera"`
|
||||
Order string `form:"order"`
|
||||
Count int `form:"count" binding:"required"`
|
||||
Offset int `form:"offset"`
|
||||
Before time.Time `form:"before" time_format:"2006-01-02"`
|
||||
After time.Time `form:"after" time_format:"2006-01-02"`
|
||||
Favorites bool `form:"favorites"`
|
||||
}
|
||||
|
||||
func (f *PhotoSearchForm) ParseQueryString() (result error) {
|
||||
var key, value []byte
|
||||
var escaped, isKeyValue bool
|
||||
|
||||
query := f.Query
|
||||
|
||||
f.Query = ""
|
||||
|
||||
formValues := reflect.ValueOf(f).Elem()
|
||||
|
||||
query = strings.TrimSpace(query) + "\n"
|
||||
|
||||
for _, char := range query {
|
||||
if unicode.IsSpace(char) && !escaped {
|
||||
if isKeyValue {
|
||||
fieldName := string(bytes.Title(bytes.ToLower(key)))
|
||||
|
||||
field := formValues.FieldByName(fieldName)
|
||||
valueString := string(bytes.ToLower(value))
|
||||
|
||||
if field.CanSet() {
|
||||
switch field.Interface().(type) {
|
||||
case int, int64:
|
||||
if i, err := strconv.Atoi(valueString); err == nil {
|
||||
field.SetInt(int64(i))
|
||||
} else {
|
||||
result = err
|
||||
}
|
||||
case uint, uint64:
|
||||
if i, err := strconv.Atoi(valueString); err == nil {
|
||||
field.SetUint(uint64(i))
|
||||
} else {
|
||||
result = err
|
||||
}
|
||||
case string:
|
||||
field.SetString(valueString)
|
||||
case bool:
|
||||
if valueString == "1" || valueString == "true" || valueString == "yes" {
|
||||
field.SetBool(true)
|
||||
} else if valueString == "0" || valueString == "false" || valueString == "no" {
|
||||
field.SetBool(false)
|
||||
} else {
|
||||
result = fmt.Errorf("not a bool value: %s", fieldName)
|
||||
}
|
||||
default:
|
||||
result = fmt.Errorf("unsupported field type: %s", fieldName)
|
||||
}
|
||||
} else {
|
||||
result = fmt.Errorf("unknown form field: %s", fieldName)
|
||||
}
|
||||
} else {
|
||||
f.Query = string(bytes.ToLower(key))
|
||||
}
|
||||
|
||||
escaped = false
|
||||
isKeyValue = false
|
||||
key = key[:0]
|
||||
value = value[:0]
|
||||
} else if char == ':' {
|
||||
isKeyValue = true
|
||||
} else if char == '"' {
|
||||
escaped = !escaped
|
||||
} else if isKeyValue {
|
||||
value = append(value, byte(char))
|
||||
} else {
|
||||
key = append(key, byte(char))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TestPhotoSearchForm(t *testing.T) {
|
||||
|
@ -11,3 +13,16 @@ func TestPhotoSearchForm(t *testing.T) {
|
|||
|
||||
assert.IsType(t, new(PhotoSearchForm), form)
|
||||
}
|
||||
|
||||
func TestParseQueryString(t *testing.T) {
|
||||
form := &PhotoSearchForm{Query: "tags:foo,bar query:\"fooBar baz\" camera:1"}
|
||||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "foo,bar", form.Tags)
|
||||
assert.Equal(t, "foobar baz", form.Query)
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/forms"
|
||||
"github.com/photoprism/photoprism/internal/models"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Search searches given an originals path and a db instance.
|
||||
|
@ -95,7 +97,13 @@ func NewSearch(originalsPath string, db *gorm.DB) *Search {
|
|||
}
|
||||
|
||||
// Photos searches for photos based on a Form and returns a PhotoSearchResult slice.
|
||||
func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error) {
|
||||
func (s *Search) Photos(form forms.PhotoSearchForm) (results []PhotoSearchResult, err error) {
|
||||
if err := form.ParseQueryString(); err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
log.Infof("%+v\n", form)
|
||||
|
||||
q := s.db.NewScope(nil).DB()
|
||||
q = q.Table("photos").
|
||||
Select(`SQL_CALC_FOUND_ROWS photos.*,
|
||||
|
@ -130,8 +138,16 @@ func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error)
|
|||
q = q.Where("tags.tag_label LIKE ? OR LOWER(photo_title) LIKE ? OR LOWER(files.file_main_color) LIKE ?", likeString, likeString, likeString)
|
||||
}
|
||||
|
||||
if form.CameraID > 0 {
|
||||
q = q.Where("photos.camera_id = ?", form.CameraID)
|
||||
if form.Camera > 0 {
|
||||
q = q.Where("photos.camera_id = ?", form.Camera)
|
||||
}
|
||||
|
||||
if form.Color != "" {
|
||||
q = q.Where("files.file_main_color = ?", form.Color)
|
||||
}
|
||||
|
||||
if form.Favorites {
|
||||
q = q.Where("photos.photo_favorite = 1")
|
||||
}
|
||||
|
||||
if form.Country != "" {
|
||||
|
@ -183,8 +199,6 @@ func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error)
|
|||
q = q.Limit(100).Offset(0)
|
||||
}
|
||||
|
||||
var results []PhotoSearchResult
|
||||
|
||||
if result := q.Scan(&results); result.Error != nil {
|
||||
return results, result.Error
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ func TestSearch_Photos_Camera(t *testing.T) {
|
|||
var form forms.PhotoSearchForm
|
||||
|
||||
form.Query = ""
|
||||
form.CameraID = 2
|
||||
form.Camera = 2
|
||||
form.Count = 3
|
||||
form.Offset = 0
|
||||
|
||||
|
|
Loading…
Reference in a new issue