From f1c5c65ffc8c48f469d1a54e410899a08c0b1baa Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 11:01:35 +0200 Subject: [PATCH 01/11] updated function names to make room for new booking analytics --- occupi-backend/pkg/analytics/analytics.go | 24 +++++++++----- occupi-backend/pkg/database/database.go | 2 +- .../pkg/database/database_helpers.go | 4 +-- occupi-backend/pkg/handlers/api_handlers.go | 2 +- occupi-backend/pkg/models/database.go | 2 +- occupi-backend/tests/analytics_test.go | 32 +++++++++---------- occupi-backend/tests/database_test.go | 14 ++++---- 7 files changed, 44 insertions(+), 36 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index b533ec2b..3fb0f574 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -5,7 +5,7 @@ import ( "go.mongodb.org/mongo-driver/bson" ) -func CreateMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson.D { +func CreateMatchFilter(email string, filter models.AnalyticsFilterStruct) bson.D { // Create a match filter matchFilter := bson.D{} @@ -31,8 +31,12 @@ func CreateMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson return matchFilter } +func CreateBookingMatchFilter(creatorEmail string, attendeesEmail string, filter models.AnalyticsFilterStruct) bson.D { + +} + // GroupOfficeHoursByDay function with total hours calculation -func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) bson.A { +func GroupOfficeHoursByDay(email string, filter models.AnalyticsFilterStruct) bson.A { matchFilter := CreateMatchFilter(email, filter) return bson.A{ @@ -91,7 +95,7 @@ func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) } } -func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { +func AverageOfficeHoursByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function matchFilter := CreateMatchFilter(email, filter) @@ -172,7 +176,7 @@ func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterSt } } -func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { +func RatioInOutOfficeByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function matchFilter := CreateMatchFilter(email, filter) @@ -262,7 +266,7 @@ func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStru } // BusiestHoursByWeekday function to return the 3 busiest hours per weekday -func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { +func BusiestHoursByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function matchFilter := CreateMatchFilter(email, filter) @@ -373,7 +377,7 @@ func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) } // LeastMostInOfficeWorker function to calculate the least or most "in office" worker -func LeastMostInOfficeWorker(email string, filter models.OfficeHoursFilterStruct, sort bool) bson.A { +func LeastMostInOfficeWorker(email string, filter models.AnalyticsFilterStruct, sort bool) bson.A { // Create the match filter using the reusable function matchFilter := CreateMatchFilter(email, filter) @@ -506,7 +510,7 @@ func LeastMostInOfficeWorker(email string, filter models.OfficeHoursFilterStruct } // AverageArrivalAndDepartureTimesByWeekday function to calculate the average arrival and departure times for each weekday -func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { +func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function matchFilter := CreateMatchFilter(email, filter) @@ -760,7 +764,7 @@ func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.Office } // CalculateInOfficeRate function to calculate absenteeism rates -func CalculateInOfficeRate(email string, filter models.OfficeHoursFilterStruct) bson.A { +func CalculateInOfficeRate(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function matchFilter := CreateMatchFilter(email, filter) @@ -1031,3 +1035,7 @@ func CalculateInOfficeRate(email string, filter models.OfficeHoursFilterStruct) }, } } + +func GetTop3MostBookedRooms(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.A { + +} diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index cac483cf..a4701a29 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -1715,7 +1715,7 @@ func AddAttendance(ctx *gin.Context, appsession *models.AppSession, email string return nil } -func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email string, filter models.OfficeHoursFilterStruct, calculate string) ([]primitive.M, int64, error) { +func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email string, filter models.AnalyticsFilterStruct, calculate string) ([]primitive.M, int64, error) { // check if database is nil if appsession.DB == nil { logrus.Error("Database is nil") diff --git a/occupi-backend/pkg/database/database_helpers.go b/occupi-backend/pkg/database/database_helpers.go index 46b62529..b8e39487 100644 --- a/occupi-backend/pkg/database/database_helpers.go +++ b/occupi-backend/pkg/database/database_helpers.go @@ -5,8 +5,8 @@ import ( "strings" "time" - "github.com/go-webauthn/webauthn/webauthn" "github.com/gin-gonic/gin" + "github.com/go-webauthn/webauthn/webauthn" "github.com/ipinfo/go/v2/ipinfo" "github.com/sirupsen/logrus" "github.com/umahmood/haversine" @@ -264,7 +264,7 @@ func Month(date time.Time) int { return int(date.Month()) } -func MakeEmailAndTimeFilter(email string, filter models.OfficeHoursFilterStruct) bson.M { +func MakeEmailAndTimeFilter(email string, filter models.AnalyticsFilterStruct) bson.M { mongoFilter := bson.M{} if email != "" { mongoFilter["email"] = email diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 45fac927..1b2d6dbe 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -1264,7 +1264,7 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, calcul limit, page, skip := utils.ComputeLimitPageSkip(request.Limit, request.Page) - filter := models.OfficeHoursFilterStruct{ + filter := models.AnalyticsFilterStruct{ Filter: bson.M{ "timeFrom": request.TimeFrom, "timeTo": request.TimeTo, diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index e2492e37..123e839f 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -159,7 +159,7 @@ type OfficeHours struct { Exited time.Time `json:"exited" bson:"exited"` } -type OfficeHoursFilterStruct struct { +type AnalyticsFilterStruct struct { Filter primitive.M Limit int64 Skip int64 diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index 002b3313..c99428c6 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -13,49 +13,49 @@ func TestCreateMatchFilter(t *testing.T) { tests := []struct { name string email string - filter models.OfficeHoursFilterStruct + filter models.AnalyticsFilterStruct expected bson.D }{ { name: "empty filter with no email", email: "", - filter: models.OfficeHoursFilterStruct{Filter: bson.M{}}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{}}, expected: bson.D{}, }, { name: "empty filter with email", email: "test@example.com", - filter: models.OfficeHoursFilterStruct{Filter: bson.M{}}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{}}, expected: bson.D{{Key: "email", Value: bson.D{{Key: "$eq", Value: "test@example.com"}}}}, }, { name: "filter with no email", email: "", - filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "", "timeTo": ""}}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "", "timeTo": ""}}, expected: bson.D{}, }, { name: "filter with no email and timeFrom", email: "", - filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "09:00", "timeTo": ""}}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "09:00", "timeTo": ""}}, expected: bson.D{{Key: "entered", Value: bson.D{{Key: "$gte", Value: "09:00"}}}}, }, { name: "filter with no email and timeTo", email: "", - filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "", "timeTo": "17:00"}}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "", "timeTo": "17:00"}}, expected: bson.D{{Key: "entered", Value: bson.D{{Key: "$lte", Value: "17:00"}}}}, }, { name: "filter with no email and timeFrom and timeTo", email: "", - filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "09:00", "timeTo": "17:00"}}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "09:00", "timeTo": "17:00"}}, expected: bson.D{{Key: "entered", Value: bson.D{{Key: "$gte", Value: "09:00"}, {Key: "$lte", Value: "17:00"}}}}, }, { name: "filter with email and timeFrom and timeTo", email: "test@example.com", - filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "09:00", "timeTo": "17:00"}}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "09:00", "timeTo": "17:00"}}, expected: bson.D{ {Key: "email", Value: bson.D{{Key: "$eq", Value: "test@example.com"}}}, {Key: "entered", Value: bson.D{{Key: "$gte", Value: "09:00"}, {Key: "$lte", Value: "17:00"}}}, @@ -80,7 +80,7 @@ func equalBsonD(a, b bson.D) bool { func TestGroupOfficeHoursByDay(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} res := analytics.GroupOfficeHoursByDay(email, filter) @@ -92,7 +92,7 @@ func TestGroupOfficeHoursByDay(t *testing.T) { func TestAverageOfficeHoursByWeekday(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} res := analytics.AverageOfficeHoursByWeekday(email, filter) @@ -104,7 +104,7 @@ func TestAverageOfficeHoursByWeekday(t *testing.T) { func TestRatioInOutOfficeByWeekday(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} res := analytics.RatioInOutOfficeByWeekday(email, filter) @@ -116,7 +116,7 @@ func TestRatioInOutOfficeByWeekday(t *testing.T) { func TestBusiestHoursByWeekday(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} res := analytics.BusiestHoursByWeekday(email, filter) @@ -128,7 +128,7 @@ func TestBusiestHoursByWeekday(t *testing.T) { func TestLeastInOfficeWorker(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} res := analytics.LeastMostInOfficeWorker(email, filter, true) @@ -140,7 +140,7 @@ func TestLeastInOfficeWorker(t *testing.T) { func TestMostInOfficeWorker(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} res := analytics.LeastMostInOfficeWorker(email, filter, false) @@ -152,7 +152,7 @@ func TestMostInOfficeWorker(t *testing.T) { func TestAverageArrivalAndDepartureTimesByWeekday(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} res := analytics.AverageArrivalAndDepartureTimesByWeekday(email, filter) @@ -164,7 +164,7 @@ func TestAverageArrivalAndDepartureTimesByWeekday(t *testing.T) { func TestCalculateInOfficeRate(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} res := analytics.CalculateInOfficeRate(email, filter) diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index 34054cb3..22aec54a 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -8423,8 +8423,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { filterPrimitive[k] = v } - // Define example OfficeHoursFilterStruct - filter := models.OfficeHoursFilterStruct{ + // Define example AnalyticsFilterStruct + filter := models.AnalyticsFilterStruct{ Filter: filterPrimitive, Limit: 10, Skip: 0, @@ -8733,7 +8733,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { func TestMakeEmailAndTimeFilter(t *testing.T) { // Define a base filter for testing - baseFilter := models.OfficeHoursFilterStruct{ + baseFilter := models.AnalyticsFilterStruct{ Filter: bson.M{ "timeFrom": "", "timeTo": "", @@ -8763,7 +8763,7 @@ func TestMakeEmailAndTimeFilter(t *testing.T) { // Test case: Email and timeFrom provided t.Run("EmailAndTimeFrom", func(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{ + filter := models.AnalyticsFilterStruct{ Filter: bson.M{ "timeFrom": "2023-09-01T09:00:00", "timeTo": "", @@ -8781,7 +8781,7 @@ func TestMakeEmailAndTimeFilter(t *testing.T) { // Test case: Email and timeTo provided t.Run("EmailAndTimeTo", func(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{ + filter := models.AnalyticsFilterStruct{ Filter: bson.M{ "timeFrom": "", "timeTo": "2023-09-01T17:00:00", @@ -8801,7 +8801,7 @@ func TestMakeEmailAndTimeFilter(t *testing.T) { // Test case: timeFrom and timeTo provided, but no email t.Run("TimeFromAndTimeTo", func(t *testing.T) { email := "" - filter := models.OfficeHoursFilterStruct{ + filter := models.AnalyticsFilterStruct{ Filter: bson.M{ "timeFrom": "2023-09-01T09:00:00", "timeTo": "2023-09-01T17:00:00", @@ -8821,7 +8821,7 @@ func TestMakeEmailAndTimeFilter(t *testing.T) { // Test case: Email, timeFrom, and timeTo provided t.Run("EmailAndFullTimeRange", func(t *testing.T) { email := "test@example.com" - filter := models.OfficeHoursFilterStruct{ + filter := models.AnalyticsFilterStruct{ Filter: bson.M{ "timeFrom": "2023-09-01T09:00:00", "timeTo": "2023-09-01T17:00:00", From 007fd19e1743159dc4e518d8a4642f9a798d2b7f Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 11:01:58 +0200 Subject: [PATCH 02/11] finish final function updates --- occupi-backend/pkg/analytics/analytics.go | 16 ++++++++-------- occupi-backend/tests/analytics_test.go | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index 3fb0f574..466629cd 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -5,7 +5,7 @@ import ( "go.mongodb.org/mongo-driver/bson" ) -func CreateMatchFilter(email string, filter models.AnalyticsFilterStruct) bson.D { +func CreateOfficeHoursMatchFilter(email string, filter models.AnalyticsFilterStruct) bson.D { // Create a match filter matchFilter := bson.D{} @@ -37,7 +37,7 @@ func CreateBookingMatchFilter(creatorEmail string, attendeesEmail string, filter // GroupOfficeHoursByDay function with total hours calculation func GroupOfficeHoursByDay(email string, filter models.AnalyticsFilterStruct) bson.A { - matchFilter := CreateMatchFilter(email, filter) + matchFilter := CreateOfficeHoursMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -97,7 +97,7 @@ func GroupOfficeHoursByDay(email string, filter models.AnalyticsFilterStruct) bs func AverageOfficeHoursByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := CreateMatchFilter(email, filter) + matchFilter := CreateOfficeHoursMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -178,7 +178,7 @@ func AverageOfficeHoursByWeekday(email string, filter models.AnalyticsFilterStru func RatioInOutOfficeByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := CreateMatchFilter(email, filter) + matchFilter := CreateOfficeHoursMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -268,7 +268,7 @@ func RatioInOutOfficeByWeekday(email string, filter models.AnalyticsFilterStruct // BusiestHoursByWeekday function to return the 3 busiest hours per weekday func BusiestHoursByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := CreateMatchFilter(email, filter) + matchFilter := CreateOfficeHoursMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -379,7 +379,7 @@ func BusiestHoursByWeekday(email string, filter models.AnalyticsFilterStruct) bs // LeastMostInOfficeWorker function to calculate the least or most "in office" worker func LeastMostInOfficeWorker(email string, filter models.AnalyticsFilterStruct, sort bool) bson.A { // Create the match filter using the reusable function - matchFilter := CreateMatchFilter(email, filter) + matchFilter := CreateOfficeHoursMatchFilter(email, filter) var sortV int if sort { @@ -512,7 +512,7 @@ func LeastMostInOfficeWorker(email string, filter models.AnalyticsFilterStruct, // AverageArrivalAndDepartureTimesByWeekday function to calculate the average arrival and departure times for each weekday func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := CreateMatchFilter(email, filter) + matchFilter := CreateOfficeHoursMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -766,7 +766,7 @@ func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.Analyt // CalculateInOfficeRate function to calculate absenteeism rates func CalculateInOfficeRate(email string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := CreateMatchFilter(email, filter) + matchFilter := CreateOfficeHoursMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index c99428c6..31ab7483 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -65,9 +65,9 @@ func TestCreateMatchFilter(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := analytics.CreateMatchFilter(tt.email, tt.filter) + result := analytics.CreateOfficeHoursMatchFilter(tt.email, tt.filter) if !equalBsonD(result, tt.expected) { - t.Errorf("%s for CreateMatchFilter() = %v, want %v", tt.name, result, tt.expected) + t.Errorf("%s for CreateOfficeHoursMatchFilter() = %v, want %v", tt.name, result, tt.expected) } }) } From 936aa1491a2dfdfd97a5bc79be7b9ba559f7430c Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 11:23:17 +0200 Subject: [PATCH 03/11] Refactor CreateBookingMatchFilter function to include date filtering --- occupi-backend/pkg/analytics/analytics.go | 106 +++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index 466629cd..d78e2c89 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -1,6 +1,8 @@ package analytics import ( + "time" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "go.mongodb.org/mongo-driver/bson" ) @@ -31,8 +33,35 @@ func CreateOfficeHoursMatchFilter(email string, filter models.AnalyticsFilterStr return matchFilter } -func CreateBookingMatchFilter(creatorEmail string, attendeesEmail string, filter models.AnalyticsFilterStruct) bson.D { +func CreateBookingMatchFilter(creatorEmail string, attendeesEmail []string, filter models.AnalyticsFilterStruct, dateFilter string) bson.D { + // Create a match filter + matchFilter := bson.D{} + + // Conditionally add the email filter if email is not empty + if creatorEmail != "" { + matchFilter = append(matchFilter, bson.E{Key: "creator", Value: bson.D{{Key: "$eq", Value: creatorEmail}}}) + } + + // Conditionally add the attendees filter if emails is not of length 0 + if len(attendeesEmail) > 0 { + matchFilter = append(matchFilter, bson.E{Key: "emails", Value: bson.D{{Key: "$in", Value: attendeesEmail}}}) + } + + // Conditionally add the time range filter if provided + timeRangeFilter := bson.D{} + if filter.Filter["timeFrom"] != "" { + timeRangeFilter = append(timeRangeFilter, bson.E{Key: "$gte", Value: filter.Filter["timeFrom"]}) + } + if filter.Filter["timeTo"] != "" { + timeRangeFilter = append(timeRangeFilter, bson.E{Key: "$lte", Value: filter.Filter["timeTo"]}) + } + + // If there are time range filters, append them to the match filter + if len(timeRangeFilter) > 0 && len(filter.Filter) > 0 { + matchFilter = append(matchFilter, bson.E{Key: dateFilter, Value: timeRangeFilter}) + } + return matchFilter } // GroupOfficeHoursByDay function with total hours calculation @@ -1037,5 +1066,80 @@ func CalculateInOfficeRate(email string, filter models.AnalyticsFilterStruct) bs } func GetTop3MostBookedRooms(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.A { + // Create the match filter using the reusable function + matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, "date") + return bson.A{ + // Stage 1: Match filter conditions (email and time range) + bson.D{{Key: "$match", Value: matchFilter}}, + // Stage 2: Apply skip for pagination + bson.D{{Key: "$skip", Value: filter.Skip}}, + // Stage 3: Apply limit for pagination + bson.D{{Key: "$limit", Value: filter.Limit}}, + // Stage 4: Group by the room ID to calculate the total bookings + bson.D{ + {Key: "$group", + Value: bson.D{ + {Key: "_id", Value: "$roomId"}, + {Key: "roomName", Value: bson.D{{Key: "$first", Value: "$roomName"}}}, + {Key: "floorNo", Value: bson.D{{Key: "$first", Value: "$floorNo"}}}, + {Key: "creators", Value: bson.D{{Key: "$push", Value: "$creator"}}}, + {Key: "emails", Value: bson.D{{Key: "$push", Value: "$emails"}}}, + {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, + }, + }, + }, + // Stage 5: Sort by count + bson.D{{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}}, + // Stage 6: Limit to the top 3 results + bson.D{{Key: "$limit", Value: 3}}, + } +} + +func GetHistoricalBookings(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.A { + // add or overwrite "timeTo" with time.Now and delete "timeFrom" if present + filter.Filter["timeTo"] = time.Now().Format(time.RFC3339) + delete(filter.Filter, "timeFrom") + + // Create the match filter using the reusable function + matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, "end") + return bson.A{ + // Stage 1: Match filter conditions (email and time range) + bson.D{{Key: "$match", Value: matchFilter}}, + // Stage 2: Apply skip for pagination + bson.D{{Key: "$skip", Value: filter.Skip}}, + // Stage 3: Apply limit for pagination + bson.D{{Key: "$limit", Value: filter.Limit}}, + // Stage 4: Group by the date to calculate the total bookings + bson.D{{Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$date"}, + {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, + }}}, + // Stage 5: Sort by date + bson.D{{Key: "$sort", Value: bson.D{{Key: "_id", Value: 1}}}}, + } +} + +func GetUpcomingBookings(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.A { + // add or overwrite "timeTo" with time.Now and delete "timeFrom" if present + filter.Filter["timeFrom"] = time.Now().Format(time.RFC3339) + delete(filter.Filter, "timeFrom") + + // Create the match filter using the reusable function + matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, "end") + return bson.A{ + // Stage 1: Match filter conditions (email and time range) + bson.D{{Key: "$match", Value: matchFilter}}, + // Stage 2: Apply skip for pagination + bson.D{{Key: "$skip", Value: filter.Skip}}, + // Stage 3: Apply limit for pagination + bson.D{{Key: "$limit", Value: filter.Limit}}, + // Stage 4: Group by the date to calculate the total bookings + bson.D{{Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$date"}, + {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, + }}}, + // Stage 5: Sort by date + bson.D{{Key: "$sort", Value: bson.D{{Key: "_id", Value: 1}}}}, + } } From f73ac2307c659ffa4c7c0317c49b188c6ce76d03 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 11:32:01 +0200 Subject: [PATCH 04/11] added missing projection fields --- occupi-backend/pkg/analytics/analytics.go | 39 +++++++++++++---------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index d78e2c89..992ff0c1 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -1077,18 +1077,14 @@ func GetTop3MostBookedRooms(creatorEmail string, attendeeEmails []string, filter // Stage 3: Apply limit for pagination bson.D{{Key: "$limit", Value: filter.Limit}}, // Stage 4: Group by the room ID to calculate the total bookings - bson.D{ - {Key: "$group", - Value: bson.D{ - {Key: "_id", Value: "$roomId"}, - {Key: "roomName", Value: bson.D{{Key: "$first", Value: "$roomName"}}}, - {Key: "floorNo", Value: bson.D{{Key: "$first", Value: "$floorNo"}}}, - {Key: "creators", Value: bson.D{{Key: "$push", Value: "$creator"}}}, - {Key: "emails", Value: bson.D{{Key: "$push", Value: "$emails"}}}, - {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, - }, - }, - }, + bson.D{{Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$roomId"}, + {Key: "roomName", Value: bson.D{{Key: "$first", Value: "$roomName"}}}, + {Key: "floorNo", Value: bson.D{{Key: "$first", Value: "$floorNo"}}}, + {Key: "creators", Value: bson.D{{Key: "$push", Value: "$creator"}}}, + {Key: "emails", Value: bson.D{{Key: "$push", Value: "$emails"}}}, + {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, + }}}, // Stage 5: Sort by count bson.D{{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}}, // Stage 6: Limit to the top 3 results @@ -1110,13 +1106,22 @@ func GetHistoricalBookings(creatorEmail string, attendeeEmails []string, filter bson.D{{Key: "$skip", Value: filter.Skip}}, // Stage 3: Apply limit for pagination bson.D{{Key: "$limit", Value: filter.Limit}}, - // Stage 4: Group by the date to calculate the total bookings - bson.D{{Key: "$group", Value: bson.D{ - {Key: "_id", Value: "$date"}, - {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, + // Stage 4: Get all bookings without grouping + bson.D{{Key: "$project", Value: bson.D{ + {Key: "_id", Value: 0}, + {Key: "occupiID", Value: "$occupiId"}, + {Key: "roomName", Value: "$roomName"}, + {Key: "roomId", Value: "$roomId"}, + {Key: "emails", Value: "$emails"}, + {Key: "checkedIn", Value: "$checkedIn"}, + {Key: "creators", Value: "$creator"}, + {Key: "floorNo", Value: "$floorNo"}, + {Key: "date", Value: "$date"}, + {Key: "start", Value: "$start"}, + {Key: "end", Value: "$end"}, }}}, // Stage 5: Sort by date - bson.D{{Key: "$sort", Value: bson.D{{Key: "_id", Value: 1}}}}, + bson.D{{Key: "$sort", Value: bson.D{{Key: "date", Value: 1}}}}, } } From 7436b9a516c78b664525f9e02e8b7ef4531f993e Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 11:41:43 +0200 Subject: [PATCH 05/11] Refactor analytics package to improve code organization and remove unused functions --- occupi-backend/pkg/analytics/analytics.go | 32 +----------- occupi-backend/pkg/database/database.go | 49 +++++++++++++++++++ .../pkg/database/database_helpers.go | 23 +++++++++ 3 files changed, 73 insertions(+), 31 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index 992ff0c1..ba4adb46 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -1,8 +1,6 @@ package analytics import ( - "time" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "go.mongodb.org/mongo-driver/bson" ) @@ -1092,11 +1090,7 @@ func GetTop3MostBookedRooms(creatorEmail string, attendeeEmails []string, filter } } -func GetHistoricalBookings(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.A { - // add or overwrite "timeTo" with time.Now and delete "timeFrom" if present - filter.Filter["timeTo"] = time.Now().Format(time.RFC3339) - delete(filter.Filter, "timeFrom") - +func AggregateBookings(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.A { // Create the match filter using the reusable function matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, "end") return bson.A{ @@ -1124,27 +1118,3 @@ func GetHistoricalBookings(creatorEmail string, attendeeEmails []string, filter bson.D{{Key: "$sort", Value: bson.D{{Key: "date", Value: 1}}}}, } } - -func GetUpcomingBookings(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.A { - // add or overwrite "timeTo" with time.Now and delete "timeFrom" if present - filter.Filter["timeFrom"] = time.Now().Format(time.RFC3339) - delete(filter.Filter, "timeFrom") - - // Create the match filter using the reusable function - matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, "end") - return bson.A{ - // Stage 1: Match filter conditions (email and time range) - bson.D{{Key: "$match", Value: matchFilter}}, - // Stage 2: Apply skip for pagination - bson.D{{Key: "$skip", Value: filter.Skip}}, - // Stage 3: Apply limit for pagination - bson.D{{Key: "$limit", Value: filter.Limit}}, - // Stage 4: Group by the date to calculate the total bookings - bson.D{{Key: "$group", Value: bson.D{ - {Key: "_id", Value: "$date"}, - {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, - }}}, - // Stage 5: Sort by date - bson.D{{Key: "$sort", Value: bson.D{{Key: "_id", Value: 1}}}}, - } -} diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index a4701a29..d8fb3dce 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -2049,3 +2049,52 @@ func ToggleAllowAnonymousIP(ctx *gin.Context, appsession *models.AppSession, req return nil } + +func GetAnalyticsOnBookings(ctx *gin.Context, appsession *models.AppSession, creatorEmail string, + attendeeEmails []string, filter models.AnalyticsFilterStruct, calculate string) ([]primitive.M, int64, error) { + + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return nil, 0, errors.New("database is nil") + } + + // Prepare the aggregate + var pipeline bson.A + switch calculate { + case "top3": + pipeline = analytics.GetTop3MostBookedRooms(creatorEmail, attendeeEmails, filter) + case "historical": + // add or overwrite "timeTo" with time.Now and delete "timeFrom" if present + filter.Filter["timeTo"] = time.Now().Format(time.RFC3339) + delete(filter.Filter, "timeFrom") + pipeline = analytics.AggregateBookings(creatorEmail, attendeeEmails, filter) + case "upcoming": + // add or overwrite "timeFrom" with time.Now and delete "timeTo" if present + filter.Filter["timeFrom"] = time.Now().Format(time.RFC3339) + delete(filter.Filter, "timeTo") + pipeline = analytics.AggregateBookings(creatorEmail, attendeeEmails, filter) + default: + return nil, 0, errors.New("invalid calculate value") + } + + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("OfficeHoursArchive") + + cursor, err := collection.Aggregate(ctx, pipeline) + if err != nil { + logrus.Error(err) + return nil, 0, err + } + + mongoFilter := MakeEmailAndEmailsAndTimeFilter(creatorEmail, attendeeEmails, filter) + + results, totalResults, errv := GetResultsAndCount(ctx, collection, cursor, mongoFilter) + + if errv != nil { + logrus.Error(errv) + return nil, 0, errv + } + + return results, totalResults, nil + +} diff --git a/occupi-backend/pkg/database/database_helpers.go b/occupi-backend/pkg/database/database_helpers.go index b8e39487..aa3dae50 100644 --- a/occupi-backend/pkg/database/database_helpers.go +++ b/occupi-backend/pkg/database/database_helpers.go @@ -282,6 +282,29 @@ func MakeEmailAndTimeFilter(email string, filter models.AnalyticsFilterStruct) b return mongoFilter } +func MakeEmailAndEmailsAndTimeFilter(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.M { + mongoFilter := bson.M{} + if creatorEmail != "" { + mongoFilter["creator"] = creatorEmail + } + + // filter attendeeEmails in emails array + if len(attendeeEmails) > 0 { + mongoFilter["emails"] = bson.M{"$in": attendeeEmails} + } + + switch { + case filter.Filter["timeFrom"] != "" && filter.Filter["timeTo"] != "": + mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"], "$lte": filter.Filter["timeTo"]} + case filter.Filter["timeTo"] != "": + mongoFilter["entered"] = bson.M{"$lte": filter.Filter["timeTo"]} + case filter.Filter["timeFrom"] != "": + mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"]} + } + + return mongoFilter +} + func GetResultsAndCount(ctx *gin.Context, collection *mongo.Collection, cursor *mongo.Cursor, mongoFilter primitive.M) ([]bson.M, int64, error) { var results []bson.M if err := cursor.All(ctx, &results); err != nil { From 75b6318c3501e2992cffc6ad586c2212ab840a59 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 11:52:19 +0200 Subject: [PATCH 06/11] Refactor database collection name for booking analytics --- occupi-backend/pkg/database/database.go | 2 +- occupi-backend/pkg/handlers/api_handlers.go | 87 ++++++++++++++++++++- occupi-backend/pkg/models/request.go | 9 +++ 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index d8fb3dce..fcda3d31 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -2078,7 +2078,7 @@ func GetAnalyticsOnBookings(ctx *gin.Context, appsession *models.AppSession, cre return nil, 0, errors.New("invalid calculate value") } - collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("OfficeHoursArchive") + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") cursor, err := collection.Aggregate(ctx, pipeline) if err != nil { diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 1b2d6dbe..5f3987fd 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -1294,7 +1294,6 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, calcul ctx.JSON(http.StatusOK, utils.SuccessResponseWithMeta(http.StatusOK, "Successfully fetched user analytics! Note that all analytics are measured in hours.", userHours, gin.H{"totalResults": len(userHours), "totalPages": (totalResults + limit - 1) / limit, "currentPage": page})) - } func CreateUser(ctx *gin.Context, appsession *models.AppSession) { @@ -1481,3 +1480,89 @@ func ToggleAllowAnonymousIP(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully toggled allow anonymous IP status!", nil)) } + +func GetAnalyticsOnBookings(ctx *gin.Context, appsession *models.AppSession, calculate string) { + var request models.RequestBooking + if err := ctx.ShouldBindJSON(&request); err != nil { + request.Creator = ctx.DefaultQuery("creator", "") + attendees := ctx.DefaultQuery("attendeeEmails", "") + request.Attendees = strings.Split(attendees, ",") + + // default time is since 1970 + timeFromStr := ctx.DefaultQuery("timeFrom", "1970-01-01T00:00:00Z") + // default time is now + timeToStr := ctx.DefaultQuery("timeTo", time.Now().Format(time.RFC3339)) + + timeFrom, err1 := time.Parse(time.RFC3339, timeFromStr) + timeTo, err2 := time.Parse(time.RFC3339, timeToStr) + + if err1 != nil || err2 != nil { + captureError(ctx, err1) + ctx.JSON(http.StatusBadRequest, utils.InternalServerError()) + return + } + + request.TimeFrom = timeFrom + request.TimeTo = timeTo + + limitStr := ctx.DefaultQuery("limit", "50") + limit, err := strconv.ParseInt(limitStr, 10, 64) + if err != nil { + captureError(ctx, err) + ctx.JSON(http.StatusBadRequest, utils.InternalServerError()) + return + } + + request.Limit = limit + + pageStr := ctx.DefaultQuery("page", "1") + page, err := strconv.ParseInt(pageStr, 10, 64) + if err != nil { + captureError(ctx, err) + ctx.JSON(http.StatusBadRequest, utils.InternalServerError()) + return + } + + request.Page = page + } else { + // ensure that the time from and time to are set else set them to default + if request.TimeFrom.IsZero() || request.TimeTo.IsZero() { + request.TimeFrom = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) + request.TimeTo = time.Now() + } + + // ensure that the limit is set else set it to default + if request.Limit == 0 { + request.Limit = 50 + } + + } + + limit, page, skip := utils.ComputeLimitPageSkip(request.Limit, request.Page) + + filter := models.AnalyticsFilterStruct{ + Filter: bson.M{ + "timeFrom": request.TimeFrom, + "timeTo": request.TimeTo, + }, + Limit: limit, // or whatever limit you want to apply + Skip: skip, // or whatever skip you want to apply + } + + // Get the analytics + result, totalResults, err := database.GetAnalyticsOnBookings(ctx, appsession, request.Creator, request.Attendees, filter, calculate) + if err != nil { + captureError(ctx, err) + logrus.Error("Failed to get analytics because: ", err) + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse( + http.StatusInternalServerError, + "Failed to get analytics", + constants.InternalServerErrorCode, + "Failed to get analytics", + nil)) + return + } + + ctx.JSON(http.StatusOK, utils.SuccessResponseWithMeta(http.StatusOK, "Successfully fetched analytics!", result, + gin.H{"totalResults": len(result), "totalPages": (totalResults + limit - 1) / limit, "currentPage": page})) +} diff --git a/occupi-backend/pkg/models/request.go b/occupi-backend/pkg/models/request.go index 7e0628c1..2c9809cf 100644 --- a/occupi-backend/pkg/models/request.go +++ b/occupi-backend/pkg/models/request.go @@ -142,6 +142,15 @@ type RequestHours struct { Page int64 `json:"page"` } +type RequestBooking struct { + Creator string `json:"creator" binding:"omitempty,email"` + Attendees []string `json:"attendees" binding:"omitempty,email"` + TimeFrom time.Time `json:"timeFrom" binding:"omitempty" time_format:"2006-01-02T15:04:05Z07:00"` + TimeTo time.Time `json:"timeTo" binding:"omitempty" time_format:"2006-01-02T15:04:05Z07:00"` + Limit int64 `json:"limit"` + Page int64 `json:"page"` +} + type RequestSpecialEvent struct { Date time.Time `json:"date" binding:"required"` IsSpecialEvent string `json:"isSpecialEvent" binding:"required"` From 26f447074638c4589bee5799727b88b46734c5c7 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 12:10:05 +0200 Subject: [PATCH 07/11] Refactor CreateBookingMatchFilter function to include date filtering --- occupi-backend/pkg/analytics/analytics.go | 8 +- occupi-backend/pkg/database/database.go | 12 ++- .../pkg/database/database_helpers.go | 8 +- occupi-backend/pkg/router/router.go | 5 +- occupi-backend/tests/database_test.go | 82 +++++++++++++++++++ 5 files changed, 102 insertions(+), 13 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index ba4adb46..07318445 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -1063,9 +1063,9 @@ func CalculateInOfficeRate(email string, filter models.AnalyticsFilterStruct) bs } } -func GetTop3MostBookedRooms(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.A { +func GetTop3MostBookedRooms(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct, dateFilter string) bson.A { // Create the match filter using the reusable function - matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, "date") + matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, dateFilter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -1090,9 +1090,9 @@ func GetTop3MostBookedRooms(creatorEmail string, attendeeEmails []string, filter } } -func AggregateBookings(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.A { +func AggregateBookings(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct, dateFilter string) bson.A { // Create the match filter using the reusable function - matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, "end") + matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, dateFilter) return bson.A{ // Stage 1: Match filter conditions (email and time range) bson.D{{Key: "$match", Value: matchFilter}}, diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index fcda3d31..dda53ab4 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -2061,19 +2061,23 @@ func GetAnalyticsOnBookings(ctx *gin.Context, appsession *models.AppSession, cre // Prepare the aggregate var pipeline bson.A + var dateFilter string switch calculate { case "top3": - pipeline = analytics.GetTop3MostBookedRooms(creatorEmail, attendeeEmails, filter) + dateFilter = "date" + pipeline = analytics.GetTop3MostBookedRooms(creatorEmail, attendeeEmails, filter, dateFilter) case "historical": // add or overwrite "timeTo" with time.Now and delete "timeFrom" if present filter.Filter["timeTo"] = time.Now().Format(time.RFC3339) delete(filter.Filter, "timeFrom") - pipeline = analytics.AggregateBookings(creatorEmail, attendeeEmails, filter) + dateFilter = "end" + pipeline = analytics.AggregateBookings(creatorEmail, attendeeEmails, filter, dateFilter) case "upcoming": // add or overwrite "timeFrom" with time.Now and delete "timeTo" if present filter.Filter["timeFrom"] = time.Now().Format(time.RFC3339) delete(filter.Filter, "timeTo") - pipeline = analytics.AggregateBookings(creatorEmail, attendeeEmails, filter) + dateFilter = "end" + pipeline = analytics.AggregateBookings(creatorEmail, attendeeEmails, filter, dateFilter) default: return nil, 0, errors.New("invalid calculate value") } @@ -2086,7 +2090,7 @@ func GetAnalyticsOnBookings(ctx *gin.Context, appsession *models.AppSession, cre return nil, 0, err } - mongoFilter := MakeEmailAndEmailsAndTimeFilter(creatorEmail, attendeeEmails, filter) + mongoFilter := MakeEmailAndEmailsAndTimeFilter(creatorEmail, attendeeEmails, filter, dateFilter) results, totalResults, errv := GetResultsAndCount(ctx, collection, cursor, mongoFilter) diff --git a/occupi-backend/pkg/database/database_helpers.go b/occupi-backend/pkg/database/database_helpers.go index aa3dae50..2790ce13 100644 --- a/occupi-backend/pkg/database/database_helpers.go +++ b/occupi-backend/pkg/database/database_helpers.go @@ -282,7 +282,7 @@ func MakeEmailAndTimeFilter(email string, filter models.AnalyticsFilterStruct) b return mongoFilter } -func MakeEmailAndEmailsAndTimeFilter(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct) bson.M { +func MakeEmailAndEmailsAndTimeFilter(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct, dateFilter string) bson.M { mongoFilter := bson.M{} if creatorEmail != "" { mongoFilter["creator"] = creatorEmail @@ -295,11 +295,11 @@ func MakeEmailAndEmailsAndTimeFilter(creatorEmail string, attendeeEmails []strin switch { case filter.Filter["timeFrom"] != "" && filter.Filter["timeTo"] != "": - mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"], "$lte": filter.Filter["timeTo"]} + mongoFilter[dateFilter] = bson.M{"$gte": filter.Filter["timeFrom"], "$lte": filter.Filter["timeTo"]} case filter.Filter["timeTo"] != "": - mongoFilter["entered"] = bson.M{"$lte": filter.Filter["timeTo"]} + mongoFilter[dateFilter] = bson.M{"$lte": filter.Filter["timeTo"]} case filter.Filter["timeFrom"] != "": - mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"]} + mongoFilter[dateFilter] = bson.M{"$gte": filter.Filter["timeFrom"]} } return mongoFilter diff --git a/occupi-backend/pkg/router/router.go b/occupi-backend/pkg/router/router.go index cd4986af..985dadcc 100644 --- a/occupi-backend/pkg/router/router.go +++ b/occupi-backend/pkg/router/router.go @@ -83,7 +83,10 @@ func OccupiRouter(router *gin.Engine, appsession *models.AppSession) { analytics.GET("/peak-office-hours", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnHours(ctx, appsession, "peakhours", true) }) analytics.GET("/arrival-departure-average", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnHours(ctx, appsession, "arrivaldeparture", true) }) analytics.GET("/in-office", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnHours(ctx, appsession, "inofficehours", true) }) - // analytics.GET("/booking-hours", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetBookingHours(ctx, appsession) }) + + analytics.GET("/top-bookings", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "top3") }) + analytics.GET("/bookings-historical", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "historical") }) + analytics.GET("/bookings-current", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "upcoming") }) } auth := router.Group("/auth") { diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index 22aec54a..a74a6afa 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -10140,3 +10140,85 @@ func TestToggleAllowAnonymousIP(t *testing.T) { assert.NoError(t, mock.ExpectationsWereMet()) }) } + +func TestMakeEmailAndEmailsAndTimeFilter(t *testing.T) { + tests := []struct { + name string + creatorEmail string + attendeeEmails []string + filter models.AnalyticsFilterStruct + expectedFilter bson.M + }{ + { + name: "Empty filter", + creatorEmail: "", + attendeeEmails: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{ + "timeFrom": "", + "timeTo": "", + }}, + expectedFilter: bson.M{}, + }, + { + name: "Filter by creatorEmail", + creatorEmail: "test@example.com", + attendeeEmails: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{ + "timeFrom": "", + "timeTo": "", + }}, + expectedFilter: bson.M{"creator": "test@example.com"}, + }, + { + name: "Filter by attendeeEmails", + creatorEmail: "", + attendeeEmails: []string{"attendee1@example.com", "attendee2@example.com"}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{ + "timeFrom": "", + "timeTo": "", + }}, + expectedFilter: bson.M{"emails": bson.M{"$in": []string{"attendee1@example.com", "attendee2@example.com"}}}, + }, + { + name: "Filter by timeFrom and timeTo", + creatorEmail: "", + attendeeEmails: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "2023-01-01", "timeTo": "2023-01-31"}}, + expectedFilter: bson.M{"date": bson.M{"$gte": "2023-01-01", "$lte": "2023-01-31"}}, + }, + { + name: "Filter by timeTo only", + creatorEmail: "", + attendeeEmails: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeTo": "2023-01-31", "timeFrom": ""}}, + expectedFilter: bson.M{"date": bson.M{"$lte": "2023-01-31"}}, + }, + { + name: "Filter by timeFrom only", + creatorEmail: "", + attendeeEmails: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "2023-01-01", "timeTo": ""}}, + expectedFilter: bson.M{"date": bson.M{"$gte": "2023-01-01"}}, + }, + { + name: "Filter by creatorEmail, attendeeEmails, and time range", + creatorEmail: "test@example.com", + attendeeEmails: []string{"attendee1@example.com"}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "2023-01-01", "timeTo": "2023-01-31"}}, + expectedFilter: bson.M{ + "creator": "test@example.com", + "emails": bson.M{"$in": []string{"attendee1@example.com"}}, + "date": bson.M{"$gte": "2023-01-01", "$lte": "2023-01-31"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := database.MakeEmailAndEmailsAndTimeFilter(tt.creatorEmail, tt.attendeeEmails, tt.filter, "date") + if !reflect.DeepEqual(result, tt.expectedFilter) { + t.Errorf("expected %v, got %v", tt.expectedFilter, result) + } + }) + } +} From 4dca434879e773283806e67c011bc552f01d7ed1 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 12:16:42 +0200 Subject: [PATCH 08/11] Refactor CreateBookingMatchFilter function to include date filtering in tests --- occupi-backend/tests/analytics_test.go | 127 +++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index 31ab7483..eb02e015 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -173,3 +173,130 @@ func TestCalculateInOfficeRate(t *testing.T) { t.Errorf("CalculateInOfficeRate() = %v, want greater than 0", res) } } + +func TestCreateBookingMatchFilter(t *testing.T) { + tests := []struct { + name string + creatorEmail string + attendeesEmail []string + filter models.AnalyticsFilterStruct + dateFilter string + expectedFilter bson.D + }{ + { + name: "Empty filter", + creatorEmail: "", + attendeesEmail: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{}}, + dateFilter: "date", + expectedFilter: bson.D{}, + }, + { + name: "Filter by creatorEmail", + creatorEmail: "creator@example.com", + attendeesEmail: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{}}, + dateFilter: "date", + expectedFilter: bson.D{ + {Key: "creator", Value: bson.D{{Key: "$eq", Value: "creator@example.com"}}}, + }, + }, + { + name: "Filter by attendeesEmail", + creatorEmail: "", + attendeesEmail: []string{"attendee1@example.com", "attendee2@example.com"}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{}}, + dateFilter: "date", + expectedFilter: bson.D{ + {Key: "emails", Value: bson.D{{Key: "$in", Value: []string{"attendee1@example.com", "attendee2@example.com"}}}}, + }, + }, + { + name: "Filter by time range (timeFrom and timeTo)", + creatorEmail: "", + attendeesEmail: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "2023-01-01", "timeTo": "2023-01-31"}}, + dateFilter: "date", + expectedFilter: bson.D{ + {Key: "date", Value: bson.D{ + {Key: "$gte", Value: "2023-01-01"}, + {Key: "$lte", Value: "2023-01-31"}, + }}, + }, + }, + { + name: "Filter by creatorEmail, attendeesEmail, and time range", + creatorEmail: "creator@example.com", + attendeesEmail: []string{"attendee1@example.com"}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "2023-01-01", "timeTo": "2023-01-31"}}, + dateFilter: "date", + expectedFilter: bson.D{ + {Key: "creator", Value: bson.D{{Key: "$eq", Value: "creator@example.com"}}}, + {Key: "emails", Value: bson.D{{Key: "$in", Value: []string{"attendee1@example.com"}}}}, + {Key: "date", Value: bson.D{ + {Key: "$gte", Value: "2023-01-01"}, + {Key: "$lte", Value: "2023-01-31"}, + }}, + }, + }, + { + name: "Filter by timeFrom only", + creatorEmail: "", + attendeesEmail: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeFrom": "2023-01-01", "timeTo": ""}}, + dateFilter: "date", + expectedFilter: bson.D{ + {Key: "date", Value: bson.D{ + {Key: "$gte", Value: "2023-01-01"}, + }}, + }, + }, + { + name: "Filter by timeTo only", + creatorEmail: "", + attendeesEmail: []string{}, + filter: models.AnalyticsFilterStruct{Filter: bson.M{"timeTo": "2023-01-31", "timeFrom": ""}}, + dateFilter: "date", + expectedFilter: bson.D{ + {Key: "date", Value: bson.D{ + {Key: "$lte", Value: "2023-01-31"}, + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := analytics.CreateBookingMatchFilter(tt.creatorEmail, tt.attendeesEmail, tt.filter, tt.dateFilter) + if !reflect.DeepEqual(result, tt.expectedFilter) { + t.Errorf("expected %v, got %v", tt.expectedFilter, result) + } + }) + } +} + +func TestGetTop3MostBookedRooms(t *testing.T) { + creatorEmail := "test@example.com" + attendeeEmails := []string{"test@example.com"} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} + + res := analytics.GetTop3MostBookedRooms(creatorEmail, attendeeEmails, filter, "date") + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("CalculateInOfficeRate() = %v, want greater than 0", res) + } +} + +func TestAggregateBookings(t *testing.T) { + creatorEmail := "test@example.com" + attendeeEmails := []string{"test@example.com"} + filter := models.AnalyticsFilterStruct{Filter: bson.M{}} + + res := analytics.AggregateBookings(creatorEmail, attendeeEmails, filter, "date") + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("CalculateInOfficeRate() = %v, want greater than 0", res) + } +} From 351311943d4cc7ae047df4d14f9b9a37bb61a961 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 12:29:24 +0200 Subject: [PATCH 09/11] Refactor CreateBookingMatchFilter function to include date filtering in tests --- occupi-backend/tests/database_test.go | 262 ++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index a74a6afa..0c7008d5 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -10222,3 +10222,265 @@ func TestMakeEmailAndEmailsAndTimeFilter(t *testing.T) { }) } } + +func TestGetAnalyticsOnBookings(t *testing.T) { + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + // Set gin run mode + gin.SetMode(configs.GetGinRunMode()) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + + booking := models.Booking{ + OccupiID: "123", + RoomID: "456", + RoomName: "Room 1", + Emails: []string{""}, + CheckedIn: true, + Creator: "test@example.com", + FloorNo: "1", + Date: time.Now(), + Start: time.Now(), + End: time.Now(), + } + + filterMap := map[string]string{ + // 1970-01-01T00:00:00Z + "timeFrom": time.Unix(0, 0).Format(time.RFC3339), + // time now + "timeTo": time.Now().Format(time.RFC3339), + } + + // Convert filterMap to primitive.M + filterPrimitive := make(primitive.M) + for k, v := range filterMap { + filterPrimitive[k] = v + } + + // Define example AnalyticsFilterStruct + filter := models.AnalyticsFilterStruct{ + Filter: filterPrimitive, + Limit: 10, + Skip: 0, + } + + mt.Run("Nil database", func(mt *mtest.T) { + // Create a mock AppSession with a nil database + appsession := &models.AppSession{DB: nil} + + results, total, err := database.GetAnalyticsOnBookings(ctx, appsession, "test@example.com", []string{}, filter, "count") + assert.Nil(t, results) + assert.Equal(t, int64(0), total) + assert.EqualError(t, err, "database is nil", "Expected error for nil database") + }) + + mt.Run("Invalid calculation type", func(mt *mtest.T) { + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnBookings(ctx, appsession, "test@example.com", []string{}, filter, "invalid") + assert.Nil(t, results) + assert.Equal(t, int64(0), total) + assert.EqualError(t, err, "invalid calculate value", "Expected error for invalid calculation type") + }) + + mt.Run("Successful query and top3 calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "occupiId", Value: booking.OccupiID}, + {Key: "roomId", Value: booking.RoomID}, + {Key: "roomName", Value: booking.RoomName}, + {Key: "emails", Value: booking.Emails}, + {Key: "checkedIn", Value: booking.CheckedIn}, + {Key: "creator", Value: booking.Creator}, + {Key: "floorNo", Value: booking.FloorNo}, + {Key: "date", Value: booking.Date}, + {Key: "start", Value: booking.Start}, + {Key: "end", Value: booking.End}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, + {Key: "occupiId", Value: booking.OccupiID}, + {Key: "roomId", Value: booking.RoomID}, + {Key: "roomName", Value: booking.RoomName}, + {Key: "emails", Value: booking.Emails}, + {Key: "checkedIn", Value: booking.CheckedIn}, + {Key: "creator", Value: booking.Creator}, + {Key: "floorNo", Value: booking.FloorNo}, + {Key: "date", Value: booking.Date}, + {Key: "start", Value: booking.Start}, + {Key: "end", Value: booking.End}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnBookings(ctx, appsession, "", []string{}, filter, "top3") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") + }) + + mt.Run("Successful query and historical calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "occupiId", Value: booking.OccupiID}, + {Key: "roomId", Value: booking.RoomID}, + {Key: "roomName", Value: booking.RoomName}, + {Key: "emails", Value: booking.Emails}, + {Key: "checkedIn", Value: booking.CheckedIn}, + {Key: "creator", Value: booking.Creator}, + {Key: "floorNo", Value: booking.FloorNo}, + {Key: "date", Value: booking.Date}, + {Key: "start", Value: booking.Start}, + {Key: "end", Value: booking.End.Add(-time.Hour)}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, + {Key: "occupiId", Value: booking.OccupiID}, + {Key: "roomId", Value: booking.RoomID}, + {Key: "roomName", Value: booking.RoomName}, + {Key: "emails", Value: booking.Emails}, + {Key: "checkedIn", Value: booking.CheckedIn}, + {Key: "creator", Value: booking.Creator}, + {Key: "floorNo", Value: booking.FloorNo}, + {Key: "date", Value: booking.Date}, + {Key: "start", Value: booking.Start}, + {Key: "end", Value: booking.End.Add(-time.Hour)}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnBookings(ctx, appsession, "", []string{}, filter, "historical") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") + }) + + mt.Run("Successful query and upcoming calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "occupiId", Value: booking.OccupiID}, + {Key: "roomId", Value: booking.RoomID}, + {Key: "roomName", Value: booking.RoomName}, + {Key: "emails", Value: booking.Emails}, + {Key: "checkedIn", Value: booking.CheckedIn}, + {Key: "creator", Value: booking.Creator}, + {Key: "floorNo", Value: booking.FloorNo}, + {Key: "date", Value: booking.Date}, + {Key: "start", Value: booking.Start}, + {Key: "end", Value: booking.End.Add(time.Hour)}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, + {Key: "occupiId", Value: booking.OccupiID}, + {Key: "roomId", Value: booking.RoomID}, + {Key: "roomName", Value: booking.RoomName}, + {Key: "emails", Value: booking.Emails}, + {Key: "checkedIn", Value: booking.CheckedIn}, + {Key: "creator", Value: booking.Creator}, + {Key: "floorNo", Value: booking.FloorNo}, + {Key: "date", Value: booking.Date}, + {Key: "start", Value: booking.Start}, + {Key: "end", Value: booking.End.Add(time.Hour)}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnBookings(ctx, appsession, "", []string{}, filter, "upcoming") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") + }) + + mt.Run("Failed aggregate query", func(mt *mtest.T) { + // Mock Find to return an error + mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ + Code: 1, + Message: "query failed", + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnBookings(ctx, appsession, "", []string{}, filter, "top3") + assert.Nil(t, results) + assert.Equal(t, int64(0), total) + assert.EqualError(t, err, "query failed", "Expected error for failed query") + }) + + mt.Run("Failed cursor", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "occupiId", Value: booking.OccupiID}, + {Key: "roomId", Value: booking.RoomID}, + {Key: "roomName", Value: booking.RoomName}, + {Key: "emails", Value: booking.Emails}, + {Key: "checkedIn", Value: booking.CheckedIn}, + {Key: "creator", Value: booking.Creator}, + {Key: "floorNo", Value: booking.FloorNo}, + {Key: "date", Value: booking.Date}, + {Key: "start", Value: booking.Start}, + {Key: "end", Value: booking.End}, + })) + + // Mock CountDocuments to return an error + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnBookings(ctx, appsession, "", []string{}, filter, "top3") + assert.Nil(t, results) + assert.Equal(t, int64(0), total) + assert.EqualError(t, err, "cursor.id should be an int64 but is a BSON invalid", "Expected error for failed count documents") + }) + + mt.Run("Failed count", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "occupiId", Value: booking.OccupiID}, + {Key: "roomId", Value: booking.RoomID}, + {Key: "roomName", Value: booking.RoomName}, + {Key: "emails", Value: booking.Emails}, + {Key: "checkedIn", Value: booking.CheckedIn}, + {Key: "creator", Value: booking.Creator}, + {Key: "floorNo", Value: booking.FloorNo}, + {Key: "date", Value: booking.Date}, + {Key: "start", Value: booking.Start}, + {Key: "end", Value: booking.End}, + })) + + // Mock CountDocuments to return an error + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnBookings(ctx, appsession, "", []string{}, filter, "top3") + assert.Nil(t, results) + assert.Equal(t, int64(0), total) + assert.EqualError(t, err, "database response does not contain a cursor", "Expected error for failed count documents") + }) +} From dfaa7f5ee64fc4e59a6a245c5241fde946a14fd3 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 13:36:30 +0200 Subject: [PATCH 10/11] Refactor CreateBookingMatchFilter function to include date filtering in tests --- occupi-backend/pkg/analytics/analytics.go | 5 +++++ occupi-backend/pkg/database/database.go | 9 +++++---- occupi-backend/pkg/database/database_helpers.go | 3 +++ occupi-backend/pkg/handlers/api_handlers.go | 6 +++++- occupi-backend/pkg/router/router.go | 6 +++--- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index 07318445..324178be 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -1,6 +1,8 @@ package analytics import ( + "fmt" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "go.mongodb.org/mongo-driver/bson" ) @@ -42,6 +44,9 @@ func CreateBookingMatchFilter(creatorEmail string, attendeesEmail []string, filt // Conditionally add the attendees filter if emails is not of length 0 if len(attendeesEmail) > 0 { + fmt.Println(attendeesEmail) + // print len of attendeesEmail + fmt.Println(len(attendeesEmail)) matchFilter = append(matchFilter, bson.E{Key: "emails", Value: bson.D{{Key: "$in", Value: attendeesEmail}}}) } diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index dda53ab4..e3225e0d 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -2065,17 +2065,18 @@ func GetAnalyticsOnBookings(ctx *gin.Context, appsession *models.AppSession, cre switch calculate { case "top3": dateFilter = "date" + filter.Filter["timeTo"] = "" pipeline = analytics.GetTop3MostBookedRooms(creatorEmail, attendeeEmails, filter, dateFilter) case "historical": // add or overwrite "timeTo" with time.Now and delete "timeFrom" if present - filter.Filter["timeTo"] = time.Now().Format(time.RFC3339) - delete(filter.Filter, "timeFrom") + filter.Filter["timeTo"] = time.Now() + filter.Filter["timeFrom"] = "" dateFilter = "end" pipeline = analytics.AggregateBookings(creatorEmail, attendeeEmails, filter, dateFilter) case "upcoming": // add or overwrite "timeFrom" with time.Now and delete "timeTo" if present - filter.Filter["timeFrom"] = time.Now().Format(time.RFC3339) - delete(filter.Filter, "timeTo") + filter.Filter["timeFrom"] = time.Now() + filter.Filter["timeTo"] = "" dateFilter = "end" pipeline = analytics.AggregateBookings(creatorEmail, attendeeEmails, filter, dateFilter) default: diff --git a/occupi-backend/pkg/database/database_helpers.go b/occupi-backend/pkg/database/database_helpers.go index 2790ce13..30df15a3 100644 --- a/occupi-backend/pkg/database/database_helpers.go +++ b/occupi-backend/pkg/database/database_helpers.go @@ -1,6 +1,7 @@ package database import ( + "fmt" "strconv" "strings" "time" @@ -290,6 +291,8 @@ func MakeEmailAndEmailsAndTimeFilter(creatorEmail string, attendeeEmails []strin // filter attendeeEmails in emails array if len(attendeeEmails) > 0 { + fmt.Println(attendeeEmails) + fmt.Println(len(attendeeEmails)) mongoFilter["emails"] = bson.M{"$in": attendeeEmails} } diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 5f3987fd..697fb3eb 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -1486,7 +1486,11 @@ func GetAnalyticsOnBookings(ctx *gin.Context, appsession *models.AppSession, cal if err := ctx.ShouldBindJSON(&request); err != nil { request.Creator = ctx.DefaultQuery("creator", "") attendees := ctx.DefaultQuery("attendeeEmails", "") - request.Attendees = strings.Split(attendees, ",") + if attendees == "" { + request.Attendees = []string{} + } else { + request.Attendees = strings.Split(attendees, ",") + } // default time is since 1970 timeFromStr := ctx.DefaultQuery("timeFrom", "1970-01-01T00:00:00Z") diff --git a/occupi-backend/pkg/router/router.go b/occupi-backend/pkg/router/router.go index 985dadcc..b5f7c3ab 100644 --- a/occupi-backend/pkg/router/router.go +++ b/occupi-backend/pkg/router/router.go @@ -84,9 +84,9 @@ func OccupiRouter(router *gin.Engine, appsession *models.AppSession) { analytics.GET("/arrival-departure-average", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnHours(ctx, appsession, "arrivaldeparture", true) }) analytics.GET("/in-office", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnHours(ctx, appsession, "inofficehours", true) }) - analytics.GET("/top-bookings", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "top3") }) - analytics.GET("/bookings-historical", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "historical") }) - analytics.GET("/bookings-current", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "upcoming") }) + analytics.GET("/top-bookings", func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "top3") }) + analytics.GET("/bookings-historical", func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "historical") }) + analytics.GET("/bookings-current", func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "upcoming") }) } auth := router.Group("/auth") { From 47cfe8da140391c7783c17f991b1bd399e44a668 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Tue, 17 Sep 2024 13:45:02 +0200 Subject: [PATCH 11/11] Refactor analytics API endpoints for booking data in docs --- .../api-documentation/analytics-usage.mdx | 176 +++++++++++++++++- occupi-backend/pkg/router/router.go | 6 +- 2 files changed, 178 insertions(+), 4 deletions(-) diff --git a/documentation/occupi-docs/pages/api-documentation/analytics-usage.mdx b/documentation/occupi-docs/pages/api-documentation/analytics-usage.mdx index 5b854ea2..3301a4b5 100644 --- a/documentation/occupi-docs/pages/api-documentation/analytics-usage.mdx +++ b/documentation/occupi-docs/pages/api-documentation/analytics-usage.mdx @@ -24,6 +24,9 @@ all the users in the office space. - [Workers peak office hours](#workers-peak-office-hours) - [Workers arrival departure average](#workers-arival-departure-average) - [Workers in office rate](#workers-in-office-rate) + - [Top Bookings](#top-bookings) + - [Bookings historical](#bookings-historical) + - [Bookings current](#bookings-current) ## Base URL @@ -808,4 +811,175 @@ The workers in office rate endpoint is used to get the in office rate of all the "error": "Failed to fetch user analytics!", "status": 500, } - ``` \ No newline at end of file + ``` + +### Top Bookings + +The top bookings endpoint is used to get the top 3 bookings in the office space. + +- **URL** + + `/analytics/top-bookings` + +- **Method** + + `GET` + +- **Request Body** + + ```json + { + "creator": "abcd@gmail", // this is optional + "attendees": ["abcd@gmail", "efgh@gmail.com"], // this is optional + "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z + "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date + "limit": 50, // this is optional and will default to 50 bookings to select + "page": 1 // this is optional and will default to 1 + } + ``` + +- **URL Params** + +``` +/analytics/top-bookings?creator=abcd@gmail&attendees=abcd@gmail,egfh@gmail.com&timeFrom=2021-01-01T00:00:00.000Z&timeTo=2021-01-01T00:00:00.000Z&limit=50&page=1 +``` + +- **Success Response** + + - **Code:** 200 + - **Content:** + ```json + { + "response": "Successfully fetched top bookings!", + "data": [data], + "totalResults": 1, + "totalPages": 1, + "currentPage": 1, + "status": 200, + } + ``` + +- **Error Response** + + - **Code:** 500 + - **Content:** + ```json + { + "error": "Failed to fetch top bookings!", + "status": 500, + } + ``` + +### Bookings historical + +The bookings historical endpoint is used to get the historical bookings in the office space. + +- **URL** + + `/analytics/bookings-historical` + +- **Method** + + `GET` + +- **Request Body** + + ```json + { + "creator": "abcd@gmail", // this is optional + "attendees": ["abcd@gmail", "efgh@gmail.com"], // this is optional + "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z + "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date + "limit": 50, // this is optional and will default to 50 bookings to select + "page": 1 // this is optional and will default to 1 + } + ``` + +- **URL Params** + +``` +/analytics/bookings-historical?creator=abcd@gmail&attendees=abcd@gmail,egfh@gmail.com&timeFrom=2021-01-01T00:00:00.000Z&timeTo=2021-01-01T00:00:00.000Z&limit=50&page=1 +``` + +- **Success Response** + + - **Code:** 200 + - **Content:** + ```json + { + "response": "Successfully fetched historical bookings!", + "data": [data], + "totalResults": 1, + "totalPages": 1, + "currentPage": 1, + "status": 200, + } + ``` + +- **Error Response** + + - **Code:** 500 + - **Content:** + ```json + { + "error": "Failed to fetch historical bookings!", + "status": 500, + } + ``` + +### Bookings current + +The bookings current endpoint is used to get the current bookings in the office space. + +- **URL** + + `/analytics/bookings-current` + +- **Method** + + `GET` + +- **Request Body** + + ```json + { + "creator": "abcd@gmail", // this is optional + "attendees": ["abcd@gmail", "efgh@gmail.com"], // this is optional + "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z + "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date + "limit": 50, // this is optional and will default to 50 bookings to select + "page": 1 // this is optional and will default to 1 + } + ``` + +- **URL Params** + +``` +/analytics/bookings-current?creator=abcd@gmail&attendees=abcd@gmail,egfh@gmail.com&timeFrom=2021-01-01T00:00:00.000Z&timeTo=2021-01-01T00:00:00.000Z&limit=50&page=1 +``` + +- **Success Response** + + - **Code:** 200 + - **Content:** + ```json + { + "response": "Successfully fetched current bookings!", + "data": [data], + "totalResults": 1, + "totalPages": 1, + "currentPage": 1, + "status": 200, + } + ``` + +- **Error Response** + + - **Code:** 500 + - **Content:** + ```json + { + "error": "Failed to fetch current bookings!", + "status": 500, + } + ``` \ No newline at end of file diff --git a/occupi-backend/pkg/router/router.go b/occupi-backend/pkg/router/router.go index b5f7c3ab..985dadcc 100644 --- a/occupi-backend/pkg/router/router.go +++ b/occupi-backend/pkg/router/router.go @@ -84,9 +84,9 @@ func OccupiRouter(router *gin.Engine, appsession *models.AppSession) { analytics.GET("/arrival-departure-average", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnHours(ctx, appsession, "arrivaldeparture", true) }) analytics.GET("/in-office", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnHours(ctx, appsession, "inofficehours", true) }) - analytics.GET("/top-bookings", func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "top3") }) - analytics.GET("/bookings-historical", func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "historical") }) - analytics.GET("/bookings-current", func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "upcoming") }) + analytics.GET("/top-bookings", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "top3") }) + analytics.GET("/bookings-historical", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "historical") }) + analytics.GET("/bookings-current", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.GetAnalyticsOnBookings(ctx, appsession, "upcoming") }) } auth := router.Group("/auth") {