Use AlbumType to distinguish between manual collections and moments #154

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-05-29 12:21:17 +02:00
parent 2efd419862
commit dcc610d7a9
23 changed files with 375 additions and 85 deletions

View file

@ -187,8 +187,8 @@
this.searchExpanded = false;
}
if (this.filter.order !== this.album.AlbumOrder) {
this.album.AlbumOrder = this.filter.order;
if (this.filter.order !== this.album.Order) {
this.album.Order = this.filter.order;
this.updateAlbum()
}
},

View file

@ -294,7 +294,7 @@
return this.model.find(this.uid).then(m => {
this.model = m;
this.filter.order = m.AlbumOrder;
this.filter.order = m.Order;
window.document.title = `PhotoPrism: ${this.model.Title}`;
return Promise.resolve(this.model)
@ -322,8 +322,8 @@
this.dirty = true;
this.scrollDisabled = false;
if (this.filter.order !== this.model.AlbumOrder) {
this.filter.order = this.model.AlbumOrder;
if (this.filter.order !== this.model.Order) {
this.filter.order = this.model.Order;
this.updateQuery();
} else {
this.loadMore();

View file

@ -41,12 +41,24 @@
<v-container grid-list-xs fluid class="pa-2 p-albums p-albums-cards">
<v-card v-if="results.length === 0" class="p-albums-empty secondary-light lighten-1 ma-1" flat>
<v-card-title primary-title>
<div>
<div v-if="staticFilter.type === 'moment'">
<h3 class="title mb-3">
{{$gettext("No albums matched your search")}}
<translate key=">No moments">No moments matched your search</translate>
</h3>
<div>
{{$gettext("Try again using a different term or create a new album from a selection in Photos.")}}
<translate key=">Wait until">Wait until PhotoPrism has analyzed your library or try
again using a different term.
</translate>
</div>
</div>
<div v-else>
<h3 class="title mb-3">
<translate key=">No albums">No albums matched your search</translate>
</h3>
<div>
<translate key="Try again">Try again using a different term or create a new album from a
selection in Photos.
</translate>
</div>
</div>
</v-card-title>
@ -246,7 +258,7 @@
ev.preventDefault();
ev.stopPropagation();
if(this.results[index]) {
if (this.results[index]) {
this.selectRange(index, this.results);
}
}

View file

@ -33,11 +33,19 @@ export default [
meta: {title: c.subtitle, auth: true},
props: {staticFilter: {photo: "true"}},
},
{
name: "moments",
path: "/moments",
component: Albums,
meta: {title: "Moments", auth: true},
props: {staticFilter: {type: "moment"}},
},
{
name: "albums",
path: "/albums",
component: Albums,
meta: {title: "Albums", auth: true},
props: {staticFilter: {type: "album"}},
},
{
name: "album",
@ -111,13 +119,6 @@ export default [
meta: {title: "All photos and videos", auth: true},
props: {staticFilter: {quality: 0}},
},
{
name: "moments",
path: "/moments",
component: Photos,
meta: {title: "Moments", auth: true},
props: {},
},
{
name: "people",
path: "/people",

View file

@ -87,7 +87,7 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
m := entity.NewAlbum(f.AlbumTitle, entity.TypeDefault)
m := entity.NewAlbum(f.AlbumTitle, entity.TypeAlbum)
m.AlbumFavorite = f.AlbumFavorite
log.Debugf("create album: %+v %+v", f, m)

View file

@ -18,7 +18,7 @@ func GetMomentsTime(router *gin.RouterGroup, conf *config.Config) {
return
}
result, err := query.GetMomentsTime()
result, err := query.MomentsTime(1)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})

View file

@ -19,7 +19,7 @@ type Album struct {
CoverUID string `gorm:"type:varbinary(36);" json:"CoverUID" yaml:"CoverUID,omitempty"`
FolderUID string `gorm:"type:varbinary(36);index;" json:"FolderUID" yaml:"FolderUID,omitempty"`
AlbumSlug string `gorm:"type:varbinary(255);index;" json:"Slug" yaml:"Slug"`
AlbumType string `gorm:"type:varbinary(8);" json:"Type" yaml:"Type,omitempty"`
AlbumType string `gorm:"type:varbinary(8);default:'album';" json:"Type" yaml:"Type,omitempty"`
AlbumTitle string `gorm:"type:varchar(255);" json:"Title" yaml:"Title"`
AlbumCategory string `gorm:"type:varchar(255);index;" json:"Category" yaml:"Category,omitempty"`
AlbumCaption string `gorm:"type:text;" json:"Caption" yaml:"Caption,omitempty"`
@ -52,6 +52,10 @@ func (m *Album) BeforeCreate(scope *gorm.Scope) error {
func NewAlbum(albumTitle, albumType string) *Album {
now := time.Now().UTC()
if albumType == "" {
albumType = TypeAlbum
}
result := &Album{
AlbumUID: rnd.PPID('a'),
AlbumOrder: SortOrderOldest,

View file

@ -11,7 +11,7 @@ func (m AlbumMap) Get(name string) Album {
return result
}
return *NewAlbum(name, TypeDefault)
return *NewAlbum(name, TypeAlbum)
}
func (m AlbumMap) Pointer(name string) *Album {
@ -19,7 +19,7 @@ func (m AlbumMap) Pointer(name string) *Album {
return &result
}
return NewAlbum(name, TypeDefault)
return NewAlbum(name, TypeAlbum)
}
var AlbumFixtures = AlbumMap{
@ -28,7 +28,7 @@ var AlbumFixtures = AlbumMap{
CoverUID: "",
AlbumUID: "at9lxuqxpogaaba7",
AlbumSlug: "christmas2030",
AlbumType: TypeDefault,
AlbumType: TypeAlbum,
AlbumTitle: "Christmas2030",
AlbumDescription: "Wonderful christmas",
AlbumNotes: "",
@ -45,7 +45,7 @@ var AlbumFixtures = AlbumMap{
CoverUID: "",
AlbumUID: "at9lxuqxpogaaba8",
AlbumSlug: "holiday-2030",
AlbumType: TypeDefault,
AlbumType: TypeAlbum,
AlbumTitle: "Holiday2030",
AlbumDescription: "Wonderful christmas",
AlbumNotes: "",
@ -62,7 +62,7 @@ var AlbumFixtures = AlbumMap{
CoverUID: "",
AlbumUID: "at9lxuqxpogaaba9",
AlbumSlug: "berlin-2019",
AlbumType: TypeDefault,
AlbumType: TypeAlbum,
AlbumTitle: "Berlin2019",
AlbumDescription: "Wonderful christmas",
AlbumNotes: "",

View file

@ -12,12 +12,12 @@ import (
func TestNewAlbum(t *testing.T) {
t.Run("name Christmas 2018", func(t *testing.T) {
album := NewAlbum("Christmas 2018", TypeDefault)
album := NewAlbum("Christmas 2018", TypeAlbum)
assert.Equal(t, "Christmas 2018", album.AlbumTitle)
assert.Equal(t, "christmas-2018", album.AlbumSlug)
})
t.Run("name empty", func(t *testing.T) {
album := NewAlbum("", TypeDefault)
album := NewAlbum("", TypeAlbum)
defaultName := time.Now().Format("January 2006")
defaultSlug := slug.Make(defaultName)
@ -29,7 +29,7 @@ func TestNewAlbum(t *testing.T) {
func TestAlbum_SetName(t *testing.T) {
t.Run("valid name", func(t *testing.T) {
album := NewAlbum("initial name", TypeDefault)
album := NewAlbum("initial name", TypeAlbum)
assert.Equal(t, "initial name", album.AlbumTitle)
assert.Equal(t, "initial-name", album.AlbumSlug)
album.SetTitle("New Album Name")
@ -37,7 +37,7 @@ func TestAlbum_SetName(t *testing.T) {
assert.Equal(t, "new-album-name", album.AlbumSlug)
})
t.Run("empty name", func(t *testing.T) {
album := NewAlbum("initial name", TypeDefault)
album := NewAlbum("initial name", TypeAlbum)
assert.Equal(t, "initial name", album.AlbumTitle)
assert.Equal(t, "initial-name", album.AlbumSlug)
@ -57,7 +57,7 @@ The discrepancy of 1 second meridian arc length between equator and pole is abou
is an oblate spheroid.`
expected := txt.Clip(longName, txt.ClipDefault)
slugExpected := txt.Clip(longName, txt.ClipSlug)
album := NewAlbum(longName, TypeDefault)
album := NewAlbum(longName, TypeAlbum)
assert.Equal(t, expected, album.AlbumTitle)
assert.Contains(t, album.AlbumSlug, slug.Make(slugExpected))
})
@ -65,7 +65,7 @@ is an oblate spheroid.`
func TestAlbum_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
album := NewAlbum("Old Name", TypeDefault)
album := NewAlbum("Old Name", TypeAlbum)
assert.Equal(t, "Old Name", album.AlbumTitle)
assert.Equal(t, "old-name", album.AlbumSlug)

View file

@ -28,6 +28,7 @@ const (
TitleUnknown = "Unknown"
TypeDefault = ""
TypeAlbum = "album"
TypeFolder = "folder"
TypeMoment = "moment"
TypeImage = "image"

View file

@ -25,7 +25,7 @@ var LocationFixtures = LocationMap{
LocUID: "85d1ea7d382c",
PlaceUID: PlaceFixtures.Get("mexico").PlaceUID,
LocName: "Adosada Platform",
LocCategory: "tourism",
LocCategory: "botanical garden",
Place: PlaceFixtures.Pointer("mexico"),
LocSource: "places",
CreatedAt: time.Now(),
@ -54,7 +54,7 @@ var LocationFixtures = LocationMap{
PlaceUID: PlaceFixtures.Get("zinkwazi").PlaceUID,
Place: PlaceFixtures.Pointer("zinkwazi"),
LocName: "Zinkwazi Beach",
LocCategory: "",
LocCategory: "beach",
LocSource: "places",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
@ -64,7 +64,7 @@ var LocationFixtures = LocationMap{
PlaceUID: PlaceFixtures.Get("holidaypark").PlaceUID,
Place: PlaceFixtures.Pointer("holidaypark"),
LocName: "Holiday Park",
LocCategory: "",
LocCategory: "park",
LocSource: "places",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
@ -74,7 +74,7 @@ var LocationFixtures = LocationMap{
PlaceUID: PlaceFixtures.Get("emptyNameLongCity").PlaceUID,
Place: PlaceFixtures.Pointer("emptyNameLongCity"),
LocName: "",
LocCategory: "",
LocCategory: "botanical garden",
LocSource: "places",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
@ -84,7 +84,7 @@ var LocationFixtures = LocationMap{
PlaceUID: PlaceFixtures.Get("emptyNameShortCity").PlaceUID,
Place: PlaceFixtures.Pointer("emptyNameShortCity"),
LocName: "",
LocCategory: "",
LocCategory: "botanical garden",
LocSource: "places",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
@ -94,7 +94,7 @@ var LocationFixtures = LocationMap{
PlaceUID: PlaceFixtures.Get("veryLongLocName").PlaceUID,
Place: PlaceFixtures.Pointer("veryLongLocName"),
LocName: "longlonglonglonglonglonglonglonglonglonglonglonglongName",
LocCategory: "",
LocCategory: "cape",
LocSource: "places",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
@ -104,7 +104,7 @@ var LocationFixtures = LocationMap{
PlaceUID: PlaceFixtures.Get("mediumLongLocName").PlaceUID,
Place: PlaceFixtures.Pointer("mediumLongLocName"),
LocName: "longlonglonglonglonglongName",
LocCategory: "",
LocCategory: "botanical garden",
LocSource: "places",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),

View file

@ -37,7 +37,7 @@ func TestLocation_Keywords(t *testing.T) {
t.Run("mexico", func(t *testing.T) {
m := LocationFixtures["mexico"]
r := m.Keywords()
assert.Equal(t, []string{"adosada", "ancient", "mexico", "platform", "pyramid", "teotihuacán", "tourism"}, r)
assert.Equal(t, []string{"adosada", "ancient", "botanical", "garden", "mexico", "platform", "pyramid", "state-of-mexico", "teotihuacán"}, r)
})
t.Run("caravan park", func(t *testing.T) {
m := LocationFixtures["caravan park"]
@ -55,6 +55,11 @@ func TestLocation_Find(t *testing.T) {
t.Run("invalid api", func(t *testing.T) {
l := NewLocation(2, 1)
err := l.Find("")
if err == nil {
t.Fatal("error expected")
}
assert.Equal(t, "maps: reverse lookup disabled", err.Error())
})
}

View file

@ -25,7 +25,7 @@ var PlaceFixtures = PlacesMap{
PlaceUID: "85d1ea7d3278",
LocLabel: "Teotihuacán, Mexico, Mexico",
LocCity: "Teotihuacán",
LocState: "Mexico",
LocState: "State of Mexico",
LocCountry: "mx",
LocKeywords: "ancient, pyramid",
LocNotes: "",

View file

@ -9,7 +9,7 @@ func TestPlaceMap_Get(t *testing.T) {
t.Run("get existing place", func(t *testing.T) {
r := PlaceFixtures.Get("mexico")
assert.Equal(t, "Teotihuacán", r.LocCity)
assert.Equal(t, "Mexico", r.LocState)
assert.Equal(t, "State of Mexico", r.LocState)
assert.IsType(t, Place{}, r)
})
t.Run("get not existing place", func(t *testing.T) {
@ -24,7 +24,7 @@ func TestPlaceMap_Pointer(t *testing.T) {
t.Run("get existing place pointer", func(t *testing.T) {
r := PlaceFixtures.Pointer("mexico")
assert.Equal(t, "Teotihuacán", r.LocCity)
assert.Equal(t, "Mexico", r.LocState)
assert.Equal(t, "State of Mexico", r.LocState)
assert.IsType(t, &Place{}, r)
})
t.Run("get not existing place pointer", func(t *testing.T) {

View file

@ -4,6 +4,8 @@ package form
type AlbumSearch struct {
Query string `form:"q"`
ID string `form:"id"`
Type string `form:"type"`
Category string `form:"category"`
Slug string `form:"slug"`
Title string `form:"title"`
Country string `json:"country"`

18
internal/maps/country.go Normal file
View file

@ -0,0 +1,18 @@
package maps
import "strings"
// CountryName tries to find a matching country name for a code.
func CountryName(code string) string {
if code == "" {
code = "zz"
} else {
code = strings.ToLower(code)
}
if name, ok := CountryNames[code]; ok {
return name
}
return CountryNames["zz"]
}

View file

@ -0,0 +1,19 @@
package maps
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCountryName(t *testing.T) {
t.Run("gb", func(t *testing.T) {
result := CountryName("gb")
assert.Equal(t, "United Kingdom", result)
})
t.Run("us", func(t *testing.T) {
result := CountryName("us")
assert.Equal(t, "USA", result)
})
}

View file

@ -82,7 +82,7 @@ func AlbumSearch(f form.AlbumSearch) (results []AlbumResult, err error) {
Group("albums.id")
if f.ID != "" {
s = s.Where("albums.album_uid = ?", f.ID)
s = s.Where("albums.album_uid IN (?)", strings.Split(f.ID, ","))
if result := s.Scan(&results); result.Error != nil {
return results, result.Error
@ -96,6 +96,14 @@ func AlbumSearch(f form.AlbumSearch) (results []AlbumResult, err error) {
s = s.Where("LOWER(albums.album_title) LIKE ?", likeString)
}
if f.Type != "" {
s = s.Where("albums.album_type IN (?)", strings.Split(f.Type, ","))
}
if f.Category != "" {
s = s.Where("albums.album_category IN (?)", strings.Split(f.Category, ","))
}
if f.Favorite {
s = s.Where("albums.album_favorite = 1")
}

149
internal/query/moments.go Normal file
View file

@ -0,0 +1,149 @@
package query
import (
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/photoprism/photoprism/internal/maps"
)
// Moment contains photo counts per month and year
type Moment struct {
PhotoCategory string `json:"Category"`
PhotoCountry string `json:"Country"`
PhotoState string `json:"State"`
PhotoYear int `json:"Year"`
PhotoMonth int `json:"Month"`
PhotoCount int `json:"Count"`
}
var MomentCategory = map[string]string{
"botanical garden": "Botanical Gardens",
"nature reserve": "Nature Reserves",
"bay": "Bays, Capes & Beaches",
"beach": "Bays, Capes & Beaches",
"cape": "Bays, Capes & Beaches",
}
// Slug returns an identifier string for a moment.
func (m Moment) Slug() string {
return slug.Make(m.Title())
}
// Title returns an english title for the moment.
func (m Moment) Title() string {
if m.PhotoYear == 0 && m.PhotoMonth == 0 {
if m.PhotoCategory != "" {
return MomentCategory[m.PhotoCategory]
}
country := maps.CountryName(m.PhotoCountry)
if strings.Contains(m.PhotoState, country) {
return m.PhotoState
}
if m.PhotoState == "" {
return m.PhotoCountry
}
return fmt.Sprintf("%s / %s", m.PhotoState, country)
}
if m.PhotoCountry != "" && m.PhotoYear > 1900 && m.PhotoMonth == 0 {
if m.PhotoState != "" {
return fmt.Sprintf("%s / %s / %d", m.PhotoState, maps.CountryName(m.PhotoCountry), m.PhotoYear)
}
return fmt.Sprintf("%s %d", maps.CountryName(m.PhotoCountry), m.PhotoYear)
}
if m.PhotoYear > 1900 && m.PhotoMonth > 0 && m.PhotoMonth <= 12 {
date := time.Date(m.PhotoYear, time.Month(m.PhotoMonth), 1, 0, 0, 0, 0, time.UTC)
if m.PhotoCountry == "" {
return date.Format("January 2006")
}
return fmt.Sprintf("%s / %s", maps.CountryName(m.PhotoCountry), date.Format("January 2006"))
}
if m.PhotoMonth > 0 && m.PhotoMonth <= 12 {
return time.Month(m.PhotoMonth).String()
}
return maps.CountryName(m.PhotoCountry)
}
type Moments []Moment
// MomentsTime counts photos by month and year.
func MomentsTime(threshold int) (results Moments, err error) {
db := UnscopedDb().Table("photos").
Where("photos.photo_quality >= 3 AND deleted_at IS NULL AND photo_year > 0 AND photo_month > 0").
Select("photos.photo_year, photos.photo_month, COUNT(*) AS photo_count").
Group("photos.photo_year, photos.photo_month").
Order("photos.photo_year DESC, photos.photo_month DESC").
Having("photo_count >= ?", threshold)
if err := db.Scan(&results).Error; err != nil {
return results, err
}
return results, nil
}
// MomentsCountries returns the most popular countries by year.
func MomentsCountries(threshold int) (results Moments, err error) {
db := UnscopedDb().Table("photos").
Where("photos.photo_quality >= 3 AND deleted_at IS NULL AND photo_country <> 'zz' AND photo_year > 0").
Select("photo_country, photo_year, COUNT(*) AS photo_count ").
Group("photo_country, photo_year").
Having("photo_count >= ?", threshold)
if err := db.Scan(&results).Error; err != nil {
return results, err
}
return results, nil
}
// MomentsStates returns the most popular states and countries by year.
func MomentsStates(threshold int) (results Moments, err error) {
db := UnscopedDb().Table("photos").
Joins("JOIN places p ON p.place_uid = photos.place_uid").
Where("photos.photo_quality >= 3 AND photos.deleted_at IS NULL AND p.loc_state <> '' AND p.loc_country <> 'zz'").
Select("p.loc_country AS photo_country, p.loc_state AS photo_state, COUNT(*) AS photo_count").
Group("photo_country, photo_state").
Having("photo_count >= ?", threshold)
if err := db.Scan(&results).Error; err != nil {
return results, err
}
return results, nil
}
// MomentsCategories returns the most popular photo categories.
func MomentsCategories(threshold int) (results Moments, err error) {
var cats []string
for cat, _ := range MomentCategory {
cats = append(cats, cat)
}
db := UnscopedDb().Table("photos").
Joins("JOIN locations l ON l.loc_uid = photos.loc_uid AND photos.loc_uid <> 'zz'").
Where("photos.photo_quality >= 3 AND photos.deleted_at IS NULL AND l.loc_category IN (?)", cats).
Select("l.loc_category AS photo_category, COUNT(*) AS photo_count").
Group("photo_category").
Having("photo_count >= ?", threshold)
if err := db.Scan(&results).Error; err != nil {
return results, err
}
return results, nil
}

View file

@ -0,0 +1,112 @@
package query
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMomentsTime(t *testing.T) {
t.Run("result found", func(t *testing.T) {
results, err := MomentsTime(1)
if err != nil {
t.Fatal(err)
}
if len(results) < 4 {
t.Error("at least 4 results expected")
}
t.Logf("MomentsTime %+v", results)
for _, moment := range results {
assert.Len(t, moment.PhotoCountry, 0)
assert.GreaterOrEqual(t, moment.PhotoYear, 1990)
assert.LessOrEqual(t, moment.PhotoYear, 2800)
assert.GreaterOrEqual(t, moment.PhotoMonth, 1)
assert.LessOrEqual(t, moment.PhotoMonth, 12)
assert.GreaterOrEqual(t, moment.PhotoCount, 1)
t.Logf("Title: %s", moment.Title())
t.Logf("Slug: %s", moment.Slug())
}
})
}
func TestMomentsCountries(t *testing.T) {
t.Run("result found", func(t *testing.T) {
results, err := MomentsCountries(1)
if err != nil {
t.Fatal(err)
}
t.Logf("MomentsCountries %+v", results)
if len(results) < 1 {
t.Error("at least one result expected")
}
for _, moment := range results {
assert.Len(t, moment.PhotoCountry, 2)
assert.GreaterOrEqual(t, moment.PhotoYear, 1990)
assert.LessOrEqual(t, moment.PhotoYear, 2800)
assert.Equal(t, moment.PhotoMonth, 0)
assert.GreaterOrEqual(t, moment.PhotoCount, 1)
t.Logf("Title: %s", moment.Title())
t.Logf("Slug: %s", moment.Slug())
}
})
}
func TestMomentsStates(t *testing.T) {
t.Run("result found", func(t *testing.T) {
results, err := MomentsStates(1)
if err != nil {
t.Fatal(err)
}
t.Logf("MomentsStates %+v", results)
if len(results) < 1 {
t.Error("at least one result expected")
}
for _, moment := range results {
assert.Len(t, moment.PhotoCountry, 2)
assert.NotEmpty(t, moment.PhotoState)
assert.Equal(t, moment.PhotoYear, 0)
assert.Equal(t, moment.PhotoMonth, 0)
assert.GreaterOrEqual(t, moment.PhotoCount, 1)
t.Logf("Title: %s", moment.Title())
t.Logf("Slug: %s", moment.Slug())
}
})
}
func TestMomentsCategories(t *testing.T) {
t.Run("result found", func(t *testing.T) {
results, err := MomentsCategories(1)
if err != nil {
t.Fatal(err)
}
t.Logf("MomentsCategories %+v", results)
if len(results) < 1 {
t.Error("at least one result expected")
}
for _, moment := range results {
assert.NotEmpty(t, moment.PhotoCategory)
assert.Empty(t, moment.PhotoCountry)
assert.Empty(t, moment.PhotoState)
assert.Equal(t, moment.PhotoYear, 0)
assert.Equal(t, moment.PhotoMonth, 0)
assert.GreaterOrEqual(t, moment.PhotoCount, 1)
t.Logf("Title: %s", moment.Title())
t.Logf("Slug: %s", moment.Slug())
}
})
}

View file

@ -1,25 +0,0 @@
package query
// MomentsTimeResult contains photo counts per month and year
type MomentsTimeResult struct {
PhotoYear int
PhotoMonth int
Count int
}
// GetMomentsTime counts photos per month and year
func GetMomentsTime() (results []MomentsTimeResult, err error) {
s := UnscopedDb()
s = s.Table("photos").
Where("deleted_at IS NULL").
Select("photos.photo_year, photos.photo_month, COUNT(*) AS count").
Group("photos.photo_year, photos.photo_month").
Order("photos.photo_year DESC, photos.photo_month DESC")
if result := s.Scan(&results); result.Error != nil {
return results, result.Error
}
return results, nil
}

View file

@ -1,17 +0,0 @@
package query
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetMomentsTime(t *testing.T) {
t.Run("result found", func(t *testing.T) {
result, err := GetMomentsTime()
assert.Nil(t, err)
assert.Equal(t, 2790, result[0].PhotoYear)
assert.Equal(t, 2, result[0].Count)
})
}

View file

@ -142,6 +142,7 @@ MX:UMS
CO:Colombia
CO:Republic of Colombia
US:California
US:Washington
US:New York
US:NYC
US:Florida
@ -286,7 +287,6 @@ PT:Portugal
ER:Eritrea
IS:Iceland
IS:Ísland
IS:Island
AF:Afghanistan
AF:Islamic Republic of Afghanistan
IN:India
@ -419,6 +419,7 @@ CA:Toronto
CA:Ottawa
CA:Quebec
CA:Ontario
CA:Alberta
IL:Israel
IL:State of Israel
IL:Holy Land