People: Implement marker update API #22
This commit is contained in:
parent
525f9a1869
commit
49fd531420
|
@ -359,3 +359,7 @@ body.chrome #photoprism .search-results .result {
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#photoprism .face-results .invalid {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
</div>
|
</div>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
</v-card>
|
</v-card>
|
||||||
<v-layout row wrap class="search-results photo-results cards-view">
|
<v-layout row wrap class="search-results face-results cards-view">
|
||||||
<v-flex
|
<v-flex
|
||||||
v-for="(marker, index) in markers"
|
v-for="(marker, index) in markers"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
@ -22,12 +22,13 @@
|
||||||
<v-card tile
|
<v-card tile
|
||||||
:data-id="marker.ID"
|
:data-id="marker.ID"
|
||||||
style="user-select: none"
|
style="user-select: none"
|
||||||
|
:class="{invalid: marker.Invalid}"
|
||||||
class="result accent lighten-3">
|
class="result accent lighten-3">
|
||||||
<div class="card-background accent lighten-3"></div>
|
<div class="card-background accent lighten-3"></div>
|
||||||
<canvas :id="'face-' + marker.ID" :key="marker.ID" width="300" height="300" style="width: 100%" class="v-responsive v-image accent lighten-2"></canvas>
|
<canvas :id="'face-' + marker.ID" :key="marker.ID" width="300" height="300" style="width: 100%" class="v-responsive v-image accent lighten-2"></canvas>
|
||||||
|
|
||||||
<v-card-actions v-if="marker.Score < 30" class="card-details pa-0">
|
<v-card-actions class="card-details pa-0">
|
||||||
<v-layout row wrap align-center>
|
<v-layout v-if="marker.Score < 30" row wrap align-center>
|
||||||
<v-flex xs6 class="text-xs-center pa-1">
|
<v-flex xs6 class="text-xs-center pa-1">
|
||||||
<v-btn color="accent lighten-2"
|
<v-btn color="accent lighten-2"
|
||||||
small depressed dark block :round="false"
|
small depressed dark block :round="false"
|
||||||
|
@ -45,18 +46,24 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
|
<v-layout v-else row wrap align-center>
|
||||||
|
<v-flex xs12 class="text-xs-left pa-1">
|
||||||
|
<v-text-field
|
||||||
|
v-model="marker.Label"
|
||||||
|
:rules="[textRule]"
|
||||||
|
color="secondary-dark"
|
||||||
|
browser-autocomplete="off"
|
||||||
|
class="input-name pa-0 ma-1"
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
clearable
|
||||||
|
@click:clear="clearName(marker)"
|
||||||
|
@change="updateName(marker)"
|
||||||
|
@keyup.enter.native="updateName(marker)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
||||||
<!-- v-card-title primary-title class="pa-3 card-details" style="user-select: none;">
|
|
||||||
<div>
|
|
||||||
<h3 class="body-2 mb-2">
|
|
||||||
<button class="action-title-edit" :data-uid="marker.ID"
|
|
||||||
@click.exact="alert('Name')">
|
|
||||||
Jens Mander
|
|
||||||
</button>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</v-card-title -->
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
|
@ -73,11 +80,12 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
markers: this.model.getMarkers(),
|
markers: this.model.getMarkers(true),
|
||||||
imageUrl: this.model.thumbnailUrl("fit_720"),
|
imageUrl: this.model.thumbnailUrl("fit_720"),
|
||||||
disabled: !this.$config.feature("edit"),
|
disabled: !this.$config.feature("edit"),
|
||||||
config: this.$config.values,
|
config: this.$config.values,
|
||||||
readonly: this.$config.get("readonly"),
|
readonly: this.$config.get("readonly"),
|
||||||
|
textRule: v => v.length <= this.$config.get('clip') || this.$gettext("Text too long"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -115,10 +123,21 @@ export default {
|
||||||
refresh() {
|
refresh() {
|
||||||
},
|
},
|
||||||
reject(marker) {
|
reject(marker) {
|
||||||
this.$notify.warn("Work in progress");
|
marker.Invalid = true;
|
||||||
|
this.model.updateMarker(marker);
|
||||||
},
|
},
|
||||||
confirm(marker) {
|
confirm(marker) {
|
||||||
this.$notify.warn("Work in progress");
|
marker.Score = 100;
|
||||||
|
marker.Invalid = false;
|
||||||
|
this.model.updateMarker(marker);
|
||||||
|
},
|
||||||
|
clearName(marker) {
|
||||||
|
marker.Label = "";
|
||||||
|
marker.RefUID = "";
|
||||||
|
this.model.updateMarker(marker);
|
||||||
|
},
|
||||||
|
updateName(marker) {
|
||||||
|
this.model.updateMarker(marker);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -742,7 +742,7 @@ export class Photo extends RestModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMarkers() {
|
getMarkers(valid) {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
let file = this.Files.find((f) => !!f.Primary);
|
let file = this.Files.find((f) => !!f.Primary);
|
||||||
|
@ -752,7 +752,9 @@ export class Photo extends RestModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
file.Markers.forEach((m) => {
|
file.Markers.forEach((m) => {
|
||||||
|
if (!valid || !m.Invalid) {
|
||||||
result.push(m);
|
result.push(m);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -828,6 +830,26 @@ export class Photo extends RestModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMarker(marker) {
|
||||||
|
if (!marker || !marker.ID) {
|
||||||
|
return Promise.reject("invalid marker id");
|
||||||
|
}
|
||||||
|
|
||||||
|
marker.MarkerSrc = SrcManual;
|
||||||
|
|
||||||
|
const file = this.mainFile();
|
||||||
|
|
||||||
|
if (!file || !file.UID) {
|
||||||
|
return Promise.reject("invalid file uid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${this.getEntityResource()}/files/${file.UID}/markers/${marker.ID}`;
|
||||||
|
|
||||||
|
return Api.put(url, marker).then((resp) => {
|
||||||
|
return Promise.resolve(this.setValues(resp.data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static batchSize() {
|
static batchSize() {
|
||||||
return 60;
|
return 60;
|
||||||
}
|
}
|
||||||
|
|
113
internal/api/file_marker.go
Normal file
113
internal/api/file_marker.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PUT /api/v1/photos/:uid/files/:file_uid/markers/:id
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// uid: string Photo UID as returned by the API
|
||||||
|
// file_uid: string File UID as returned by the API
|
||||||
|
// id: int Marker ID as returned by the API
|
||||||
|
func UpdateFileMarker(router *gin.RouterGroup) {
|
||||||
|
router.PUT("/photos/:uid/files/:file_uid/markers/:id", func(c *gin.Context) {
|
||||||
|
s := Auth(SessionID(c), acl.ResourceFiles, acl.ActionUpdate)
|
||||||
|
|
||||||
|
if s.Invalid() {
|
||||||
|
AbortUnauthorized(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := service.Config()
|
||||||
|
|
||||||
|
if !conf.Settings().Features.Edit {
|
||||||
|
AbortFeatureDisabled(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
photoUID := c.Param("uid")
|
||||||
|
fileUID := c.Param("file_uid")
|
||||||
|
markerID := txt.UInt(c.Param("id"))
|
||||||
|
|
||||||
|
if photoUID == "" || fileUID == "" || markerID < 1 {
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := query.FileByUID(fileUID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("photo: %s (update marker)", err)
|
||||||
|
AbortEntityNotFound(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !file.FilePrimary {
|
||||||
|
log.Errorf("photo: can't update markers for non-primary files")
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
} else if file.PhotoUID != photoUID {
|
||||||
|
log.Errorf("photo: file uid doesn't match")
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
marker, err := query.MarkerByID(markerID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("photo: %s (update marker)", err)
|
||||||
|
AbortEntityNotFound(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
markerForm, err := form.NewMarker(marker)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("photo: %s (new marker form)", err)
|
||||||
|
AbortSaveFailed(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&markerForm); err != nil {
|
||||||
|
log.Errorf("photo: %s (update marker form)", err)
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := marker.SaveForm(markerForm); err != nil {
|
||||||
|
log.Errorf("photo: %s (save marker form)", err)
|
||||||
|
AbortSaveFailed(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.SuccessMsg(i18n.MsgChangesSaved)
|
||||||
|
|
||||||
|
if p, err := query.PhotoPreloadByUID(photoUID); err != nil {
|
||||||
|
AbortEntityNotFound(c)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if faceCount := file.FaceCount(); p.PhotoFaces == faceCount {
|
||||||
|
// Do nothing.
|
||||||
|
} else if err := p.Update("PhotoFaces", faceCount); err != nil {
|
||||||
|
log.Errorf("photo: %s (update face count)", err)
|
||||||
|
} else {
|
||||||
|
// Notify clients by publishing events.
|
||||||
|
PublishPhotoEvent(EntityUpdated, photoUID, c)
|
||||||
|
|
||||||
|
p.PhotoFaces = faceCount
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, p)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
62
internal/api/file_marker_test.go
Normal file
62
internal/api/file_marker_test.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateFileMarker(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
app, router, _ := NewApiTest()
|
||||||
|
|
||||||
|
GetPhoto(router)
|
||||||
|
UpdateFileMarker(router)
|
||||||
|
|
||||||
|
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0y11")
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
|
||||||
|
photoUID := gjson.Get(r.Body.String(), "UID").String()
|
||||||
|
fileUID := gjson.Get(r.Body.String(), "Files.0.UID").String()
|
||||||
|
markerID := gjson.Get(r.Body.String(), "Files.0.Markers.0.ID").String()
|
||||||
|
|
||||||
|
assert.NotEmpty(t, photoUID)
|
||||||
|
assert.NotEmpty(t, fileUID)
|
||||||
|
assert.NotEmpty(t, markerID)
|
||||||
|
|
||||||
|
u := fmt.Sprintf("/api/v1/photos/%s/files/%s/markers/%s", photoUID, fileUID, markerID)
|
||||||
|
|
||||||
|
var m = struct {
|
||||||
|
RefUID string
|
||||||
|
RefSrc string
|
||||||
|
MarkerSrc string
|
||||||
|
MarkerType string
|
||||||
|
MarkerScore int
|
||||||
|
MarkerInvalid bool
|
||||||
|
MarkerLabel string
|
||||||
|
}{
|
||||||
|
RefUID: "3h59wvth837b5vyiub35",
|
||||||
|
RefSrc: "meta",
|
||||||
|
MarkerSrc: "image",
|
||||||
|
MarkerType: "Face",
|
||||||
|
MarkerScore: 100,
|
||||||
|
MarkerInvalid: true,
|
||||||
|
MarkerLabel: "Foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
if b, err := json.Marshal(m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
t.Logf("PUT %s", u)
|
||||||
|
r = PerformRequestWithBody(app, "PUT", u, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
})
|
||||||
|
}
|
|
@ -313,7 +313,7 @@ func (m *Album) SetTitle(title string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saves the entity using form data and stores it in the database.
|
// SaveForm updates the entity using form data and stores it in the database.
|
||||||
func (m *Album) SaveForm(f form.Album) error {
|
func (m *Album) SaveForm(f form.Album) error {
|
||||||
if err := deepcopier.Copy(m).From(f); err != nil {
|
if err := deepcopier.Copy(m).From(f); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -412,6 +412,17 @@ func (m *File) AddFace(f face.Face, refUID string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FaceCount returns the current number of valid faces detected.
|
||||||
|
func (m *File) FaceCount() (c int) {
|
||||||
|
if err := Db().Model(Marker{}).Where("marker_invalid = 0 AND file_id = ?", m.ID).
|
||||||
|
Count(&c).Error; err != nil {
|
||||||
|
log.Errorf("file: %s (count faces)", err)
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PreloadMarkers loads existing file markers.
|
// PreloadMarkers loads existing file markers.
|
||||||
func (m *File) PreloadMarkers() {
|
func (m *File) PreloadMarkers() {
|
||||||
if res, err := FindMarkers(m.ID); err != nil {
|
if res, err := FindMarkers(m.ID); err != nil {
|
||||||
|
|
|
@ -447,3 +447,13 @@ func TestFile_AddFaces(t *testing.T) {
|
||||||
assert.NotEmpty(t, file.Markers)
|
assert.NotEmpty(t, file.Markers)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFile_FaceCount(t *testing.T) {
|
||||||
|
t.Run("FileFixturesExampleBridge", func(t *testing.T) {
|
||||||
|
file := FileFixturesExampleBridge
|
||||||
|
|
||||||
|
result := file.FaceCount()
|
||||||
|
|
||||||
|
assert.GreaterOrEqual(t, result, 3)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -24,4 +24,5 @@ func CreateTestFixtures() {
|
||||||
CreateFileShareFixtures()
|
CreateFileShareFixtures()
|
||||||
CreateFileSyncFixtures()
|
CreateFileSyncFixtures()
|
||||||
CreateLensFixtures()
|
CreateLensFixtures()
|
||||||
|
CreateMarkerFixtures()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
|
"github.com/ulule/deepcopier"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/face"
|
"github.com/photoprism/photoprism/internal/face"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,6 +22,7 @@ type Marker struct {
|
||||||
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
||||||
FileID uint `gorm:"index;" json:"-" yaml:"-"`
|
FileID uint `gorm:"index;" json:"-" yaml:"-"`
|
||||||
RefUID string `gorm:"type:VARBINARY(42);index;" json:"RefUID" yaml:"RefUID,omitempty"`
|
RefUID string `gorm:"type:VARBINARY(42);index;" json:"RefUID" yaml:"RefUID,omitempty"`
|
||||||
|
RefSrc string `gorm:"type:VARBINARY(8);default:'';" json:"RefSrc" yaml:"RefSrc,omitempty"`
|
||||||
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
||||||
MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"`
|
MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"`
|
||||||
MarkerScore int `gorm:"type:SMALLINT" json:"Score" yaml:"Score"`
|
MarkerScore int `gorm:"type:SMALLINT" json:"Score" yaml:"Score"`
|
||||||
|
@ -32,6 +37,9 @@ type Marker struct {
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnknownMarker can be used as a default for unknown markers.
|
||||||
|
var UnknownMarker = NewMarker(0, "", SrcAuto, MarkerUnknown, 0, 0, 0, 0)
|
||||||
|
|
||||||
// TableName returns the entity database table name.
|
// TableName returns the entity database table name.
|
||||||
func (Marker) TableName() string {
|
func (Marker) TableName() string {
|
||||||
return "markers_dev"
|
return "markers_dev"
|
||||||
|
@ -75,6 +83,23 @@ func (m *Marker) Update(attr string, value interface{}) error {
|
||||||
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
|
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveForm updates the entity using form data and stores it in the database.
|
||||||
|
func (m *Marker) SaveForm(f form.Marker) error {
|
||||||
|
if err := deepcopier.Copy(m).From(f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.MarkerLabel != "" {
|
||||||
|
m.MarkerLabel = txt.Title(txt.Clip(f.MarkerLabel, txt.ClipKeyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Save(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Save updates the existing or inserts a new row.
|
// Save updates the existing or inserts a new row.
|
||||||
func (m *Marker) Save() error {
|
func (m *Marker) Save() error {
|
||||||
if m.X == 0 || m.Y == 0 || m.X > 1 || m.Y > 1 || m.X < -1 || m.Y < -1 {
|
if m.X == 0 || m.Y == 0 || m.X > 1 || m.Y > 1 || m.X < -1 || m.Y < -1 {
|
||||||
|
|
61
internal/entity/marker_fixtures.go
Normal file
61
internal/entity/marker_fixtures.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type MarkerMap map[string]Marker
|
||||||
|
|
||||||
|
func (m MarkerMap) Get(name string) Marker {
|
||||||
|
if result, ok := m[name]; ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return *UnknownMarker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MarkerMap) Pointer(name string) *Marker {
|
||||||
|
if result, ok := m[name]; ok {
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnknownMarker
|
||||||
|
}
|
||||||
|
|
||||||
|
var MarkerFixtures = MarkerMap{
|
||||||
|
"1000003-1": Marker{
|
||||||
|
FileID: 1000003,
|
||||||
|
RefUID: "lt9k3pw1wowuy3c3",
|
||||||
|
MarkerSrc: SrcImage,
|
||||||
|
MarkerType: MarkerLabel,
|
||||||
|
X: 0.308333,
|
||||||
|
Y: 0.206944,
|
||||||
|
W: 0.355556,
|
||||||
|
H: .355556,
|
||||||
|
},
|
||||||
|
"1000003-2": Marker{
|
||||||
|
FileID: 1000003,
|
||||||
|
RefUID: "",
|
||||||
|
MarkerLabel: "Unknown",
|
||||||
|
MarkerSrc: SrcImage,
|
||||||
|
MarkerType: MarkerLabel,
|
||||||
|
X: 0.208333,
|
||||||
|
Y: 0.106944,
|
||||||
|
W: 0.05,
|
||||||
|
H: 0.05,
|
||||||
|
},
|
||||||
|
"1000003-3": Marker{
|
||||||
|
FileID: 1000003,
|
||||||
|
RefUID: "",
|
||||||
|
MarkerSrc: SrcImage,
|
||||||
|
MarkerType: MarkerLabel,
|
||||||
|
MarkerLabel: "Center",
|
||||||
|
X: 0.5,
|
||||||
|
Y: 0.5,
|
||||||
|
W: 0,
|
||||||
|
H: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMarkerFixtures inserts known entities into the database for testing.
|
||||||
|
func CreateMarkerFixtures() {
|
||||||
|
for _, entity := range MarkerFixtures {
|
||||||
|
Db().Create(&entity)
|
||||||
|
}
|
||||||
|
}
|
20
internal/form/marker.go
Normal file
20
internal/form/marker.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package form
|
||||||
|
|
||||||
|
import "github.com/ulule/deepcopier"
|
||||||
|
|
||||||
|
// Marker represents an image marker edit form.
|
||||||
|
type Marker struct {
|
||||||
|
RefUID string `json:"RefUID"`
|
||||||
|
RefSrc string `json:"RefSrc"`
|
||||||
|
MarkerSrc string `json:"Src"`
|
||||||
|
MarkerType string `json:"Type"`
|
||||||
|
MarkerScore int `json:"Score"`
|
||||||
|
MarkerInvalid bool `json:"Invalid"`
|
||||||
|
MarkerLabel string `json:"Label"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarker(m interface{}) (f Marker, err error) {
|
||||||
|
err = deepcopier.Copy(m).To(&f)
|
||||||
|
|
||||||
|
return f, err
|
||||||
|
}
|
43
internal/form/marker_test.go
Normal file
43
internal/form/marker_test.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewMarker(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
var m = struct {
|
||||||
|
RefUID string
|
||||||
|
RefSrc string
|
||||||
|
MarkerSrc string
|
||||||
|
MarkerType string
|
||||||
|
MarkerScore int
|
||||||
|
MarkerInvalid bool
|
||||||
|
MarkerLabel string
|
||||||
|
}{
|
||||||
|
RefUID: "3h59wvth837b5vyiub35",
|
||||||
|
RefSrc: "meta",
|
||||||
|
MarkerSrc: "image",
|
||||||
|
MarkerType: "Face",
|
||||||
|
MarkerScore: 100,
|
||||||
|
MarkerInvalid: true,
|
||||||
|
MarkerLabel: "Foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := NewMarker(m)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "3h59wvth837b5vyiub35", f.RefUID)
|
||||||
|
assert.Equal(t, "meta", f.RefSrc)
|
||||||
|
assert.Equal(t, "image", f.MarkerSrc)
|
||||||
|
assert.Equal(t, "Face", f.MarkerType)
|
||||||
|
assert.Equal(t, 100, f.MarkerScore)
|
||||||
|
assert.Equal(t, true, f.MarkerInvalid)
|
||||||
|
assert.Equal(t, "Foo", f.MarkerLabel)
|
||||||
|
})
|
||||||
|
}
|
15
internal/query/markers.go
Normal file
15
internal/query/markers.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarkerByID returns a Marker based on the ID.
|
||||||
|
func MarkerByID(id uint) (marker entity.Marker, err error) {
|
||||||
|
if err := UnscopedDb().Where("id = ?", id).
|
||||||
|
First(&marker).Error; err != nil {
|
||||||
|
return marker, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return marker, nil
|
||||||
|
}
|
|
@ -72,6 +72,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||||
api.GetMomentsTime(v1)
|
api.GetMomentsTime(v1)
|
||||||
api.GetFile(v1)
|
api.GetFile(v1)
|
||||||
api.DeleteFile(v1)
|
api.DeleteFile(v1)
|
||||||
|
api.UpdateFileMarker(v1)
|
||||||
api.PhotoPrimary(v1)
|
api.PhotoPrimary(v1)
|
||||||
api.PhotoUnstack(v1)
|
api.PhotoUnstack(v1)
|
||||||
|
|
||||||
|
|
|
@ -2,43 +2,12 @@ package txt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var UnknownCountryCode = "zz"
|
var UnknownCountryCode = "zz"
|
||||||
var CountryWordsRegexp = regexp.MustCompile("[\\p{L}]{2,}")
|
var CountryWordsRegexp = regexp.MustCompile("[\\p{L}]{2,}")
|
||||||
|
|
||||||
// Int returns a string as int or 0 if it can not be converted.
|
|
||||||
func Int(s string) int {
|
|
||||||
if s == "" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := strconv.ParseInt(s, 10, 32)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsUInt returns true if a string only contains an unsigned integer.
|
|
||||||
func IsUInt(s string) bool {
|
|
||||||
if s == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range s {
|
|
||||||
if r < 48 || r > 57 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountryCode tries to find a matching country code for a given string e.g. from a file oder directory name.
|
// CountryCode tries to find a matching country code for a given string e.g. from a file oder directory name.
|
||||||
func CountryCode(s string) (code string) {
|
func CountryCode(s string) (code string) {
|
||||||
code = UnknownCountryCode
|
code = UnknownCountryCode
|
||||||
|
|
|
@ -6,33 +6,6 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInt(t *testing.T) {
|
|
||||||
t.Run("empty", func(t *testing.T) {
|
|
||||||
result := Int("")
|
|
||||||
assert.Equal(t, 0, result)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non-numeric", func(t *testing.T) {
|
|
||||||
result := Int("Screenshot")
|
|
||||||
assert.Equal(t, 0, result)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("zero", func(t *testing.T) {
|
|
||||||
result := Int("0")
|
|
||||||
assert.Equal(t, 0, result)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("int", func(t *testing.T) {
|
|
||||||
result := Int("123")
|
|
||||||
assert.Equal(t, 123, result)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("negative int", func(t *testing.T) {
|
|
||||||
result := Int("-123")
|
|
||||||
assert.Equal(t, -123, result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCountryCode(t *testing.T) {
|
func TestCountryCode(t *testing.T) {
|
||||||
t.Run("London", func(t *testing.T) {
|
t.Run("London", func(t *testing.T) {
|
||||||
result := CountryCode("London")
|
result := CountryCode("London")
|
||||||
|
@ -134,9 +107,3 @@ func TestCountryCode(t *testing.T) {
|
||||||
assert.Equal(t, "it", result)
|
assert.Equal(t, "it", result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsUInt(t *testing.T) {
|
|
||||||
assert.False(t, IsUInt(""))
|
|
||||||
assert.False(t, IsUInt("12 3"))
|
|
||||||
assert.True(t, IsUInt("123"))
|
|
||||||
}
|
|
||||||
|
|
50
pkg/txt/int.go
Normal file
50
pkg/txt/int.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package txt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Int converts a string to a signed integer or 0 if invalid.
|
||||||
|
func Int(s string) int {
|
||||||
|
if s == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := strconv.ParseInt(s, 10, 32)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UInt converts a string to an unsigned integer or 0 if invalid.
|
||||||
|
func UInt(s string) uint {
|
||||||
|
if s == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := strconv.ParseInt(s, 10, 32)
|
||||||
|
|
||||||
|
if err != nil || result < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUInt tests if a string represents an unsigned integer.
|
||||||
|
func IsUInt(s string) bool {
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range s {
|
||||||
|
if r < 48 || r > 57 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
39
pkg/txt/int_test.go
Normal file
39
pkg/txt/int_test.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package txt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInt(t *testing.T) {
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
result := Int("")
|
||||||
|
assert.Equal(t, 0, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-numeric", func(t *testing.T) {
|
||||||
|
result := Int("Screenshot")
|
||||||
|
assert.Equal(t, 0, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero", func(t *testing.T) {
|
||||||
|
result := Int("0")
|
||||||
|
assert.Equal(t, 0, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("int", func(t *testing.T) {
|
||||||
|
result := Int("123")
|
||||||
|
assert.Equal(t, 123, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative int", func(t *testing.T) {
|
||||||
|
result := Int("-123")
|
||||||
|
assert.Equal(t, -123, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func TestIsUInt(t *testing.T) {
|
||||||
|
assert.False(t, IsUInt(""))
|
||||||
|
assert.False(t, IsUInt("12 3"))
|
||||||
|
assert.True(t, IsUInt("123"))
|
||||||
|
}
|
Loading…
Reference in a new issue