commit
e05c4bef82
|
@ -12,6 +12,7 @@ jobs:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
database: ['mysql', 'postgres', 'sqlite']
|
database: ['mysql', 'postgres', 'sqlite']
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,41 @@ func Album(db *gorm.DB, user *models.User, id int) (*models.Album, error) {
|
||||||
return &album, nil
|
return &album, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AlbumPath(db *gorm.DB, user *models.User, album *models.Album) ([]*models.Album, error) {
|
||||||
|
var album_path []*models.Album
|
||||||
|
|
||||||
|
err := db.Raw(`
|
||||||
|
WITH recursive path_albums AS (
|
||||||
|
SELECT * FROM albums anchor WHERE anchor.id = ?
|
||||||
|
UNION
|
||||||
|
SELECT parent.* FROM path_albums child JOIN albums parent ON parent.id = child.parent_album_id
|
||||||
|
)
|
||||||
|
SELECT * FROM path_albums WHERE id != ?
|
||||||
|
`, album.ID, album.ID).Scan(&album_path).Error
|
||||||
|
|
||||||
|
// Make sure to only return albums this user owns
|
||||||
|
for i := len(album_path) - 1; i >= 0; i-- {
|
||||||
|
album := album_path[i]
|
||||||
|
|
||||||
|
owns, err := user.OwnsAlbum(db, album)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !owns {
|
||||||
|
album_path = album_path[i+1:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return album_path, nil
|
||||||
|
}
|
||||||
|
|
||||||
func SetAlbumCover(db *gorm.DB, user *models.User, mediaID int) (*models.Album, error) {
|
func SetAlbumCover(db *gorm.DB, user *models.User, mediaID int) (*models.Album, error) {
|
||||||
var media models.Media
|
var media models.Media
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,36 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestAlbumPath(t *testing.T) {
|
||||||
|
db := test_utils.DatabaseTest(t)
|
||||||
|
|
||||||
|
album := models.Album{
|
||||||
|
Title: "Three",
|
||||||
|
Path: "/one/two/three",
|
||||||
|
ParentAlbum: &models.Album{
|
||||||
|
Title: "Two",
|
||||||
|
Path: "/one/two",
|
||||||
|
ParentAlbum: &models.Album{
|
||||||
|
Title: "One",
|
||||||
|
Path: "/one",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, db.Save(&album).Error)
|
||||||
|
|
||||||
|
user, err := models.RegisterUser(db, "user", nil, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
db.Model(&user).Association("Albums").Append(album.ParentAlbum.ParentAlbum)
|
||||||
|
|
||||||
|
albumPath, err := actions.AlbumPath(db, user, &album)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, albumPath, 2)
|
||||||
|
assert.Equal(t, "Two", albumPath[0].Title)
|
||||||
|
assert.Equal(t, "One", albumPath[1].Title)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAlbumCover(t *testing.T) {
|
func TestAlbumCover(t *testing.T) {
|
||||||
db := test_utils.DatabaseTest(t)
|
db := test_utils.DatabaseTest(t)
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,54 @@
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MyTimeline(db *gorm.DB, user *models.User, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error) {
|
||||||
|
|
||||||
|
query := db.
|
||||||
|
Joins("JOIN albums ON media.album_id = albums.id").
|
||||||
|
Where("albums.id IN (?)", db.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID))
|
||||||
|
|
||||||
|
switch db.Dialector.Name() {
|
||||||
|
case "postgres":
|
||||||
|
query = query.
|
||||||
|
Order("DATE_TRUNC('year', date_shot) DESC").
|
||||||
|
Order("DATE_TRUNC('month', date_shot) DESC").
|
||||||
|
Order("DATE_TRUNC('day', date_shot) DESC").
|
||||||
|
Order("albums.title ASC").
|
||||||
|
Order("media.date_shot DESC")
|
||||||
|
case "sqlite":
|
||||||
|
query = query.
|
||||||
|
Order("strftime('%j', media.date_shot) DESC"). // convert to day of year 001-366
|
||||||
|
Order("albums.title ASC").
|
||||||
|
Order("TIME(media.date_shot) DESC")
|
||||||
|
default:
|
||||||
|
query = query.
|
||||||
|
Order("YEAR(media.date_shot) DESC").
|
||||||
|
Order("MONTH(media.date_shot) DESC").
|
||||||
|
Order("DAY(media.date_shot) DESC").
|
||||||
|
Order("albums.title ASC").
|
||||||
|
Order("TIME(media.date_shot) DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromDate != nil {
|
||||||
|
query = query.Where("media.date_shot < ?", fromDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if onlyFavorites != nil && *onlyFavorites {
|
||||||
|
query = query.Where("media.id IN (?)", db.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite"))
|
||||||
|
}
|
||||||
|
|
||||||
|
query = models.FormatSQL(query, nil, paginate)
|
||||||
|
|
||||||
|
var media []*models.Media
|
||||||
|
if err := query.Find(&media).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return media, nil
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package actions_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
|
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||||
|
"github.com/photoview/photoview/api/test_utils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMyTimeline(t *testing.T) {
|
||||||
|
db := test_utils.DatabaseTest(t)
|
||||||
|
|
||||||
|
password := "1234"
|
||||||
|
user, err := models.RegisterUser(db, "user", &password, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
rootAlbum := models.Album{
|
||||||
|
Title: "root",
|
||||||
|
Path: "/photos",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, db.Save(&rootAlbum).Error)
|
||||||
|
|
||||||
|
childAlbum := models.Album{
|
||||||
|
Title: "subalbum",
|
||||||
|
Path: "/photos/subalbum",
|
||||||
|
ParentAlbumID: &rootAlbum.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, db.Save(&childAlbum).Error)
|
||||||
|
|
||||||
|
assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum))
|
||||||
|
assert.NoError(t, db.Model(&user).Association("Albums").Append(&childAlbum))
|
||||||
|
|
||||||
|
media := []models.Media{
|
||||||
|
{
|
||||||
|
Title: "pic1",
|
||||||
|
Path: "/photos/pic1",
|
||||||
|
AlbumID: rootAlbum.ID,
|
||||||
|
DateShot: time.Unix(1632758400, 0), // Sep 27 2021 16:00:00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "pic2",
|
||||||
|
Path: "/photos/pic2",
|
||||||
|
AlbumID: rootAlbum.ID,
|
||||||
|
DateShot: time.Unix(1628762400, 0), // Aug 12 2021 10:00:00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "pic3",
|
||||||
|
Path: "/photos/subalbum/pic3",
|
||||||
|
AlbumID: childAlbum.ID,
|
||||||
|
DateShot: time.Unix(1632763800, 0), // Sep 27 2021 17:30:00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "pic4",
|
||||||
|
Path: "/photos/subalbum/pic4",
|
||||||
|
AlbumID: childAlbum.ID,
|
||||||
|
DateShot: time.Unix(1628775900, 0), // Aug 12 2021 13:45:00
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, db.Save(&media).Error)
|
||||||
|
|
||||||
|
_, err = user.FavoriteMedia(db, media[0].ID, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Add media not owned by first user
|
||||||
|
anotherUser, err := models.RegisterUser(db, "user2", &password, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
anotherAlbum := models.Album{
|
||||||
|
Title: "AnotherAlbum",
|
||||||
|
Path: "/another",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, db.Save(&anotherAlbum).Error)
|
||||||
|
|
||||||
|
anotherMedia := models.Media{
|
||||||
|
Title: "anotherPic",
|
||||||
|
Path: "/another/anotherPic",
|
||||||
|
AlbumID: anotherAlbum.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, db.Save(&anotherMedia).Error)
|
||||||
|
assert.NoError(t, db.Model(&anotherUser).Association("Albums").Append(&anotherAlbum))
|
||||||
|
|
||||||
|
t.Run("MyTimeline with no filters", func(t *testing.T) {
|
||||||
|
timelineMedia, err := actions.MyTimeline(db, user, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, timelineMedia, 4)
|
||||||
|
|
||||||
|
for i, title := range []string{"pic1", "pic3", "pic2", "pic4"} {
|
||||||
|
assert.Equalf(t, timelineMedia[i].Title, title, "Element %d didn't match: got %s expected %s", i, timelineMedia[i].Title, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MyTimeline with only favorites", func(t *testing.T) {
|
||||||
|
favorites := true
|
||||||
|
timelineMedia, err := actions.MyTimeline(db, user, nil, &favorites, nil)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, timelineMedia, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MyTimeline before date", func(t *testing.T) {
|
||||||
|
beforeDate := time.Unix(1629792000, 0) // Aug 24 2021 08:00:00
|
||||||
|
timelineMedia, err := actions.MyTimeline(db, user, nil, nil, &beforeDate)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, timelineMedia, 2)
|
||||||
|
})
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
|
@ -185,3 +186,23 @@ func (user *User) OwnsAlbum(db *gorm.DB, album *Album) (bool, error) {
|
||||||
|
|
||||||
return len(ownedParents) > 0, nil
|
return len(ownedParents) > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FavoriteMedia sets/clears a media as favorite for the user
|
||||||
|
func (user *User) FavoriteMedia(db *gorm.DB, mediaID int, favorite bool) (*Media, error) {
|
||||||
|
userMediaData := UserMediaData{
|
||||||
|
UserID: user.ID,
|
||||||
|
MediaID: mediaID,
|
||||||
|
Favorite: favorite,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&userMediaData).Error; err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "update user favorite media in database")
|
||||||
|
}
|
||||||
|
|
||||||
|
var media Media
|
||||||
|
if err := db.First(&media, mediaID).Error; err != nil {
|
||||||
|
return nil, errors.Wrap(err, "get media from database after favorite update")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &media, nil
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoview/photoview/api/dataloader"
|
||||||
"github.com/photoview/photoview/api/graphql/models"
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
"github.com/photoview/photoview/api/test_utils"
|
"github.com/photoview/photoview/api/test_utils"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -183,3 +184,48 @@ func TestUserOwnsAlbum(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, owns)
|
assert.False(t, owns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserFavoriteMedia(t *testing.T) {
|
||||||
|
db := test_utils.DatabaseTest(t)
|
||||||
|
|
||||||
|
user, err := models.RegisterUser(db, "user1", nil, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
rootAlbum := models.Album{
|
||||||
|
Title: "root",
|
||||||
|
Path: "/photos",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, db.Save(&rootAlbum).Error)
|
||||||
|
assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum))
|
||||||
|
|
||||||
|
media := models.Media{
|
||||||
|
Title: "pic1",
|
||||||
|
Path: "/photos/pic1",
|
||||||
|
AlbumID: rootAlbum.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, db.Save(&media).Error)
|
||||||
|
|
||||||
|
// test that it starts out being false
|
||||||
|
favourite, err := dataloader.NewUserFavoriteLoader(db).Load(&models.UserMediaData{
|
||||||
|
UserID: user.ID,
|
||||||
|
MediaID: media.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, favourite)
|
||||||
|
|
||||||
|
favMedia, err := user.FavoriteMedia(db, media.ID, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, favMedia)
|
||||||
|
|
||||||
|
// test that it is now true
|
||||||
|
favourite, err = dataloader.NewUserFavoriteLoader(db).Load(&models.UserMediaData{
|
||||||
|
UserID: user.ID,
|
||||||
|
MediaID: media.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, favourite)
|
||||||
|
}
|
||||||
|
|
|
@ -131,38 +131,7 @@ func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.
|
||||||
return empty, nil
|
return empty, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var album_path []*models.Album
|
return actions.AlbumPath(r.Database, user, obj)
|
||||||
|
|
||||||
err := r.Database.Raw(`
|
|
||||||
WITH recursive path_albums AS (
|
|
||||||
SELECT * FROM albums anchor WHERE anchor.id = ?
|
|
||||||
UNION
|
|
||||||
SELECT parent.* FROM path_albums child JOIN albums parent ON parent.id = child.parent_album_id
|
|
||||||
)
|
|
||||||
SELECT * FROM path_albums WHERE id != ?
|
|
||||||
`, obj.ID, obj.ID).Scan(&album_path).Error
|
|
||||||
|
|
||||||
// Make sure to only return albums this user owns
|
|
||||||
for i := len(album_path) - 1; i >= 0; i-- {
|
|
||||||
album := album_path[i]
|
|
||||||
|
|
||||||
owns, err := user.OwnsAlbum(r.Database, album)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !owns {
|
|
||||||
album_path = album_path[i+1:]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return album_path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Takes album_id, resets album.cover_id to 0 (null)
|
// Takes album_id, resets album.cover_id to 0 (null)
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"github.com/photoview/photoview/api/graphql/models/actions"
|
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||||
"github.com/photoview/photoview/api/scanner/face_detection"
|
"github.com/photoview/photoview/api/scanner/face_detection"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) {
|
func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) {
|
||||||
|
@ -198,22 +197,7 @@ func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favor
|
||||||
return nil, auth.ErrUnauthorized
|
return nil, auth.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
userMediaData := models.UserMediaData{
|
return user.FavoriteMedia(r.Database, mediaID, favorite)
|
||||||
UserID: user.ID,
|
|
||||||
MediaID: mediaID,
|
|
||||||
Favorite: favorite,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.Database.Clauses(clause.OnConflict{UpdateAll: true}).Create(&userMediaData).Error; err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "update user favorite media in database")
|
|
||||||
}
|
|
||||||
|
|
||||||
var media models.Media
|
|
||||||
if err := r.Database.First(&media, mediaID).Error; err != nil {
|
|
||||||
return nil, errors.Wrap(err, "get media from database after favorite update")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &media, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mediaResolver) Faces(ctx context.Context, media *models.Media) ([]*models.ImageFace, error) {
|
func (r *mediaResolver) Faces(ctx context.Context, media *models.Media) ([]*models.ImageFace, error) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/photoview/photoview/api/graphql/auth"
|
"github.com/photoview/photoview/api/graphql/auth"
|
||||||
"github.com/photoview/photoview/api/graphql/models"
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
|
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error) {
|
func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error) {
|
||||||
|
@ -14,29 +15,5 @@ func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Paginat
|
||||||
return nil, auth.ErrUnauthorized
|
return nil, auth.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
query := r.Database.
|
return actions.MyTimeline(r.Database, user, paginate, onlyFavorites, fromDate)
|
||||||
Joins("JOIN albums ON media.album_id = albums.id").
|
|
||||||
Where("albums.id IN (?)", r.Database.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID)).
|
|
||||||
Order("YEAR(media.date_shot) DESC").
|
|
||||||
Order("MONTH(media.date_shot) DESC").
|
|
||||||
Order("DAY(media.date_shot) DESC").
|
|
||||||
Order("albums.title ASC").
|
|
||||||
Order("TIME(media.date_shot) DESC")
|
|
||||||
|
|
||||||
if fromDate != nil {
|
|
||||||
query = query.Where("media.date_shot < ?", fromDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if onlyFavorites != nil && *onlyFavorites == true {
|
|
||||||
query = query.Where("media.id IN (?)", r.Database.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
|
|
||||||
}
|
|
||||||
|
|
||||||
query = models.FormatSQL(query, nil, paginate)
|
|
||||||
|
|
||||||
var media []*models.Media
|
|
||||||
if err := query.Find(&media).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return media, nil
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue