Search string parser similar to GitHub, see #2

This commit is contained in:
Michael Mayer 2019-05-15 21:51:00 +02:00
parent 1533f60a1a
commit 9a320c60df
6 changed files with 141 additions and 19 deletions

View file

@ -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();

View 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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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