commit
e05c4bef82
|
@ -12,6 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-20.04
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
database: ['mysql', 'postgres', 'sqlite']
|
||||
|
||||
|
|
|
@ -72,6 +72,41 @@ func Album(db *gorm.DB, user *models.User, id int) (*models.Album, error) {
|
|||
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) {
|
||||
var media models.Media
|
||||
|
||||
|
|
|
@ -9,6 +9,36 @@ import (
|
|||
"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) {
|
||||
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"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
|
@ -185,3 +186,23 @@ func (user *User) OwnsAlbum(db *gorm.DB, album *Album) (bool, error) {
|
|||
|
||||
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"
|
||||
"time"
|
||||
|
||||
"github.com/photoview/photoview/api/dataloader"
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -183,3 +184,48 @@ func TestUserOwnsAlbum(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
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
|
||||
}
|
||||
|
||||
var album_path []*models.Album
|
||||
|
||||
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
|
||||
return actions.AlbumPath(r.Database, user, obj)
|
||||
}
|
||||
|
||||
// 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/scanner/face_detection"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
userMediaData := models.UserMediaData{
|
||||
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
|
||||
return user.FavoriteMedia(r.Database, mediaID, favorite)
|
||||
}
|
||||
|
||||
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/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) {
|
||||
|
@ -14,29 +15,5 @@ func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Paginat
|
|||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
query := r.Database.
|
||||
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
|
||||
return actions.MyTimeline(r.Database, user, paginate, onlyFavorites, fromDate)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue