Implement batch approve #489

This commit is contained in:
Michael Mayer 2020-11-21 17:36:41 +01:00
parent 1ad2d53e16
commit ef316c98b7
10 changed files with 102 additions and 13 deletions

View file

@ -173,17 +173,18 @@
</div>
</div>
</v-card-title>
<v-card-actions v-if="photo.Quality < 3 && $config.feature('review')">
<v-card-actions v-if="photo.Quality < 3 && context === 'review'">
<v-layout row wrap align-center>
<v-flex xs12>
<div class="text-xs-center">
<v-btn color="secondary-dark" small flat dark @click.stop="photo.archive()"
class="action-approve text-xs-center">
class="action-archive text-xs-center">
<translate>Archive</translate>
</v-btn>
<v-btn color="secondary-dark" small depressed dark @click.stop="photo.approve()"
class="action-approve text-xs-center">
<translate>Approve</translate>
<v-icon right dark small>check</v-icon>
</v-btn>
</div>
</v-flex>
@ -206,6 +207,7 @@ export default {
openLocation: Function,
album: Object,
filter: Object,
context: String,
},
data() {
return {

View file

@ -25,11 +25,23 @@
color="share"
@click.stop="dialog.share = true"
:disabled="selection.length === 0"
v-if="context !== 'archive' && $config.feature('share')"
v-if="context !== 'archive' && context !== 'review' && $config.feature('share')"
class="action-share"
>
<v-icon>cloud</v-icon>
</v-btn>
<v-btn
fab dark small
:title="$gettext('Approve')"
color="share"
@click.stop="batchApprove"
:disabled="selection.length === 0"
v-if="context === 'review'"
class="action-approve"
>
<v-icon>check</v-icon>
</v-btn>
<v-btn
fab dark small
:title="$gettext('Edit')"
@ -153,6 +165,13 @@ export default {
this.$clipboard.clear();
this.expanded = false;
},
batchApprove() {
Api.post("batch/photos/approve", {"photos": this.selection}).then(() => this.onApproved());
},
onApproved() {
Notify.success(this.$gettext("Selection approved"));
this.clearClipboard();
},
batchArchivePhotos() {
this.dialog.archive = false;

View file

@ -116,6 +116,7 @@ export default {
openLocation: Function,
album: Object,
filter: Object,
context: String,
},
data() {
let m = this.$gettext("Couldn't find anything.");

View file

@ -133,6 +133,7 @@ export default {
editPhoto: Function,
album: Object,
filter: Object,
context: String,
},
data() {
return {

View file

@ -16,6 +16,7 @@
:album="model" context="album"></p-photo-clipboard>
<p-photo-mosaic v-if="settings.view === 'mosaic'"
context="album"
:photos="results"
:selection="selection"
:filter="filter"
@ -23,6 +24,7 @@
:edit-photo="editPhoto"
:open-photo="openPhoto"></p-photo-mosaic>
<p-photo-list v-else-if="settings.view === 'list'"
context="album"
:photos="results"
:selection="selection"
:filter="filter"
@ -31,6 +33,7 @@
:edit-photo="editPhoto"
:open-location="openLocation"></p-photo-list>
<p-photo-cards v-else
context="album"
:photos="results"
:selection="selection"
:filter="filter"

View file

@ -14,12 +14,14 @@
<p-photo-clipboard :refresh="refresh" :selection="selection" :context="context"></p-photo-clipboard>
<p-photo-mosaic v-if="settings.view === 'mosaic'"
:context="context"
:photos="results"
:selection="selection"
:filter="filter"
:edit-photo="editPhoto"
:open-photo="openPhoto"></p-photo-mosaic>
<p-photo-list v-else-if="settings.view === 'list'"
:context="context"
:photos="results"
:selection="selection"
:filter="filter"
@ -27,6 +29,7 @@
:edit-photo="editPhoto"
:open-location="openLocation"></p-photo-list>
<p-photo-cards v-else
:context="context"
:photos="results"
:selection="selection"
:filter="filter"
@ -129,7 +132,9 @@ export default {
return "photos";
}
if (this.staticFilter.archived) {
if (this.staticFilter.review) {
return "review";
} else if (this.staticFilter.archived) {
return "archive";
} else if (this.staticFilter.favorite) {
return "favorites";
@ -454,8 +459,14 @@ export default {
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
this.updateResult(this.results, values);
this.updateResult(this.viewer.results, values);
if (this.context === "review" && values.Quality >= 3) {
this.removeResult(this.results, values.UID);
this.removeResult(this.viewer.results, values.UID);
this.$clipboard.removeId(values.UID);
} else {
this.updateResult(this.results, values);
this.updateResult(this.viewer.results, values);
}
}
break;
case 'restored':

View file

@ -1,8 +1,6 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/acl"
@ -11,6 +9,7 @@ import (
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
"net/http"
)
// POST /api/v1/batch/photos/archive
@ -60,6 +59,56 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
})
}
// POST /api/v1/batch/photos/approve
func BatchPhotosApprove(router *gin.RouterGroup) {
router.POST("batch/photos/approve", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
AbortUnauthorized(c)
return
}
var f form.Selection
if err := c.BindJSON(&f); err != nil {
AbortBadRequest(c)
return
}
if len(f.Photos) == 0 {
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
return
}
log.Infof("photos: approving %s", f.String())
photos, err := query.PhotoSelection(f)
if err != nil {
AbortEntityNotFound(c)
return
}
var approved entity.Photos
for _, p := range photos {
if err := p.Approve(); err != nil {
log.Errorf("photo: %s (approve)", err.Error())
} else {
approved = append(approved, p)
SavePhotoAsYaml(p)
}
}
UpdateClientConfig()
event.EntitiesUpdated("photos", approved)
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSelectionApproved))
})
}
// POST /api/v1/batch/photos/restore
func BatchPhotosRestore(router *gin.RouterGroup) {
router.POST("/batch/photos/restore", func(c *gin.Context) {

View file

@ -228,11 +228,11 @@ func (m *File) Updates(values interface{}) error {
func (m *File) Rename(fileName, rootName, filePath, fileBase string) error {
// Update database row.
if err := m.Updates(map[string]interface{}{
"FileName": fileName,
"FileRoot": rootName,
"FileName": fileName,
"FileRoot": rootName,
"FileMissing": false,
"DeletedAt": nil,
}); err != nil {
"DeletedAt": nil,
}); err != nil {
return err
}
@ -263,7 +263,7 @@ func (m *File) Undelete() error {
// Update database row.
err := m.Updates(map[string]interface{}{
"FileMissing": false,
"DeletedAt": nil,
"DeletedAt": nil,
})
if err != nil {

View file

@ -60,6 +60,7 @@ const (
MsgLabelsDeleted
MsgLabelSaved
MsgFilesUploadedIn
MsgSelectionApproved
MsgSelectionArchived
MsgSelectionRestored
MsgSelectionProtected
@ -129,6 +130,7 @@ var Messages = MessageMap{
MsgLabelsDeleted: gettext("Labels deleted"),
MsgLabelSaved: gettext("Label saved"),
MsgFilesUploadedIn: gettext("%d files uploaded in %d s"),
MsgSelectionApproved: gettext("Selection approved"),
MsgSelectionArchived: gettext("Selection archived"),
MsgSelectionRestored: gettext("Selection restored"),
MsgSelectionProtected: gettext("Selection marked as private"),

View file

@ -76,6 +76,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
api.StartIndexing(v1)
api.CancelIndexing(v1)
api.BatchPhotosApprove(v1)
api.BatchPhotosArchive(v1)
api.BatchPhotosRestore(v1)
api.BatchPhotosPrivate(v1)