From 45ef541e1c6d1d574eee892905d927ae4b9451b1 Mon Sep 17 00:00:00 2001 From: anirudh Date: Sun, 4 Feb 2024 02:05:19 +0530 Subject: [PATCH] feat: implement route for user attendance records --- controllers/meeting.controller.go | 15 +++++ controllers/meeting.controller_test.go | 40 +++++++++++ mocks/meeting.repository_mocks.go | 13 ++++ mocks/meeting.service_mocks.go | 13 ++++ models/meeting.model.go | 2 + repository/meeting.repository.go | 10 +++ repository/meeting.repository_test.go | 92 ++++++++++++++++++++++++++ routers/index.go | 3 + services/meeting.service.go | 34 ++++++++++ services/meeting.service_test.go | 68 +++++++++++++++++++ 10 files changed, 290 insertions(+) diff --git a/controllers/meeting.controller.go b/controllers/meeting.controller.go index bf826ed..6830279 100644 --- a/controllers/meeting.controller.go +++ b/controllers/meeting.controller.go @@ -283,3 +283,18 @@ func (mc *MeetingController) UpcomingUserMeetings(c *gin.Context) { c.JSON(http.StatusOK, meetings) } + +// GetUserAttendanceRecords retrieves attendance records for a user. +func (mc *MeetingController) GetUserAttendanceRecords(c *gin.Context) { + currentUser, _ := c.Get("user") + userID := currentUser.(*models.User).ID + + // Call the meeting service to get attendance records for the user + attendance, err := mc.meetingService.GetFullUserAttendanceRecord(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to get attendance records for user", "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, attendance) +} diff --git a/controllers/meeting.controller_test.go b/controllers/meeting.controller_test.go index eb424d6..fe339cf 100644 --- a/controllers/meeting.controller_test.go +++ b/controllers/meeting.controller_test.go @@ -435,3 +435,43 @@ func TestMeetingController_GetAttendanceForMeeting(t *testing.T) { w, _ = sendRequest("GET", "/team/1/meetings/1/attendance") assert.Equal(t, http.StatusInternalServerError, w.Code) } + +// test GetUserAttendanceRecords +func TestMeetingController_GetUserAttendanceRecords(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockService := mocks.NewMockMeetingService(ctrl) + + r := gin.Default() + meetingController := NewMeetingController(mockService) + + r.GET("/user/me/attendance", func(c *gin.Context) { + // Set up a user in the context + user := &models.User{} + user.ID = 1 + c.Set("user", user) + + // Call the controller function + meetingController.GetUserAttendanceRecords(c) + }) + + // parseMeetingAttendanceResponse parses the response body of GetUserAttendanceRecords + parseMeetingAttendanceResponse := func(w *httptest.ResponseRecorder) []models.MeetingAttendanceListResponse { + var response []models.MeetingAttendanceListResponse + _ = json.NewDecoder(w.Body).Decode(&response) + return response + } + + // Helper function to send a request and check the response + sendRequest := func(method, path string) (*httptest.ResponseRecorder, []models.MeetingAttendanceListResponse) { + req, _ := http.NewRequest(method, path, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + return w, parseMeetingAttendanceResponse(w) + } + + mockService.EXPECT().GetFullUserAttendanceRecord(uint(1)).Return([]models.MeetingAttendanceListResponse{}, nil) + w, _ := sendRequest("GET", "/user/me/attendance") + assert.Equal(t, http.StatusOK, w.Code) +} diff --git a/mocks/meeting.repository_mocks.go b/mocks/meeting.repository_mocks.go index 0827a5a..34e1609 100644 --- a/mocks/meeting.repository_mocks.go +++ b/mocks/meeting.repository_mocks.go @@ -103,6 +103,14 @@ func (m *MockMeetingRepository) GetMeetingAttendanceByUserIDAndMeetingID(userID return ret0, ret1 } +// GetMeetingAttendancesByUserID mocks the GetMeetingAttendancesByUserID method. +func (m *MockMeetingRepository) GetMeetingAttendancesByUserID(userID uint) ([]models.MeetingAttendance, error) { + ret := m.ctrl.Call(m, "GetMeetingAttendancesByUserID", userID) + ret0, _ := ret[0].([]models.MeetingAttendance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + // MockMeetingRepositoryMockRecorder is a mock recorder for MockMeetingRepository. type MockMeetingRepositoryMockRecorder struct { mock *MockMeetingRepository @@ -157,3 +165,8 @@ func (mr *MockMeetingRepositoryMockRecorder) GetMeetingAttendanceByMeetingIDAndO func (mr *MockMeetingRepositoryMockRecorder) GetMeetingAttendanceByUserIDAndMeetingID(userID uint, meetingID uint) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMeetingAttendanceByUserIDAndMeetingID", reflect.TypeOf((*MockMeetingRepository)(nil).GetMeetingAttendanceByUserIDAndMeetingID), userID, meetingID) } + +// GetMeetingAttendancesByUserID mocks the GetMeetingAttendancesByUserID method. +func (mr *MockMeetingRepositoryMockRecorder) GetMeetingAttendancesByUserID(userID uint) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMeetingAttendancesByUserID", reflect.TypeOf((*MockMeetingRepository)(nil).GetMeetingAttendancesByUserID), userID) +} diff --git a/mocks/meeting.service_mocks.go b/mocks/meeting.service_mocks.go index 51a6890..c9f9936 100644 --- a/mocks/meeting.service_mocks.go +++ b/mocks/meeting.service_mocks.go @@ -113,6 +113,14 @@ func (m *MockMeetingService) UpcomingUserMeetings(userID uint) ([]models.UserUpc return ret0, ret1 } +// GetFullUserAttendanceRecord mocks the GetFullUserAttendanceRecord method. +func (m *MockMeetingService) GetFullUserAttendanceRecord(userID uint) ([]models.MeetingAttendanceListResponse, error) { + ret := m.ctrl.Call(m, "GetFullUserAttendanceRecord", userID) + ret0, _ := ret[0].([]models.MeetingAttendanceListResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + // MockMeetingServiceMockRecorder is a mock recorder for MockMeetingService. type MockMeetingServiceMockRecorder struct { mock *MockMeetingService @@ -171,3 +179,8 @@ func (mr *MockMeetingServiceMockRecorder) GetAttendanceForMeeting(meetingID, tea func (mr *MockMeetingServiceMockRecorder) UpcomingUserMeetings(userID uint) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpcomingUserMeetings", reflect.TypeOf((*MockMeetingService)(nil).UpcomingUserMeetings), userID) } + +// GetFullUserAttendanceRecord mocks the GetFullUserAttendanceRecord method. +func (mr *MockMeetingServiceMockRecorder) GetFullUserAttendanceRecord(userID uint) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFullUserAttendanceRecord", reflect.TypeOf((*MockMeetingService)(nil).GetFullUserAttendanceRecord), userID) +} diff --git a/models/meeting.model.go b/models/meeting.model.go index 04c27a0..05aa99f 100644 --- a/models/meeting.model.go +++ b/models/meeting.model.go @@ -76,6 +76,8 @@ type MeetingAttendanceListResponse struct { AttendanceMarkedAt time.Time OnTime bool User User + MeetingName string + TeamName string } type UserUpcomingMeetingsListResponse struct { diff --git a/repository/meeting.repository.go b/repository/meeting.repository.go index 28ea7a5..f8f32f9 100644 --- a/repository/meeting.repository.go +++ b/repository/meeting.repository.go @@ -25,6 +25,7 @@ type MeetingRepositoryInterface interface { GetMeetingAttendanceByMeetingID(meetingID uint) ([]models.MeetingAttendance, error) GetMeetingAttendanceByMeetingIDAndOnTime(meetingID uint, onTime bool) ([]models.MeetingAttendance, error) GetMeetingAttendanceByUserIDAndMeetingID(userID, meetingID uint) (models.MeetingAttendance, error) + GetMeetingAttendancesByUserID(userID uint) ([]models.MeetingAttendance, error) } // CreateMeeting creates a new meeting in the database. @@ -110,3 +111,12 @@ func (mr *MeetingRepository) GetMeetingAttendanceByUserIDAndMeetingID(userID, me } return meetingAttendance, nil } + +// GetMeetingAttendanceByUserID fetches all attendance records for a user across meetings and teams +func (mr *MeetingRepository) GetMeetingAttendancesByUserID(userID uint) ([]models.MeetingAttendance, error) { + var userAttendance []models.MeetingAttendance + if err := mr.db.Where("user_id = ?", userID).Find(&userAttendance).Error; err != nil { + return nil, err + } + return userAttendance, nil +} diff --git a/repository/meeting.repository_test.go b/repository/meeting.repository_test.go index 0fdde76..de9cec5 100644 --- a/repository/meeting.repository_test.go +++ b/repository/meeting.repository_test.go @@ -450,3 +450,95 @@ func TestMeetingRepository_GetMeetingAttendanceByUserIDAndMeetingID(t *testing.T t.Errorf("GetMeetingAttendanceByUserIDAndMeetingID should have returned an error") } } + +// test GetMeetingAttendancesByUserID +func TestMeetingRepository_GetMeetingAttendancesByUserID(t *testing.T) { + db, err := test_utils.SetupTestDB() + if err != nil { + t.Fatalf("Failed to set up test database: %v", err) + } + defer db.Migrator().DropTable(&models.Meeting{}, &models.MeetingAttendance{}) + + // Create the Meeting Repository with the test database + mr := NewMeetingRepository() + mr.db = db + + // Create a test meeting + meeting := models.Meeting{ + Title: "Test Meeting", + Description: "Test Meeting Description", + TeamID: 1, + StartTime: time.Now().Add(time.Hour * 24), + Venue: "Test Venue", + Location: models.Location{ + Latitude: 12.345678, + Longitude: 98.765432, + Altitude: 0, + }, + } + + // Test CreateMeeting function + createdMeeting, err := mr.CreateMeeting(meeting) + if err != nil { + t.Errorf("CreateMeeting returned an error: %v", err) + } + + // Create a test meeting attendance + meetingAttendance := models.MeetingAttendance{ + MeetingID: createdMeeting.ID, + UserID: 1, + AttendanceMarkedAt: time.Now(), + OnTime: true, + } + + // Another meeting and attendance + anotherMeeting := models.Meeting{ + Title: "Another Test Meeting", + Description: "Another Test Meeting Description", + TeamID: 2, + StartTime: time.Now().Add(time.Hour * 24), + Venue: "Another Test Venue", + Location: models.Location{ + Latitude: 12.345678, + Longitude: 98.765432, + Altitude: 0, + }, + } + createdAnotherMeeting, err := mr.CreateMeeting(anotherMeeting) + if err != nil { + t.Errorf("CreateMeeting returned an error: %v", err) + } + anotherMeetingAttendance := models.MeetingAttendance{ + MeetingID: createdAnotherMeeting.ID, + UserID: 1, + AttendanceMarkedAt: time.Now(), + OnTime: true, + } + err = mr.AddMeetingAttendance(anotherMeetingAttendance) + if err != nil { + t.Errorf("AddMeetingAttendance returned an error: %v", err) + } + + // Test AddMeetingAttendance function + err = mr.AddMeetingAttendance(meetingAttendance) + if err != nil { + t.Errorf("AddMeetingAttendance returned an error: %v", err) + } + + // Test GetMeetingAttendancesByUserID with a valid user ID + _, err = mr.GetMeetingAttendancesByUserID(1) + if err != nil { + t.Errorf("GetMeetingAttendancesByUserID returned an error: %v", err) + } + // Test GetMeetingAttendancesByUserID returned correct number of attendances + attendances, _ := mr.GetMeetingAttendancesByUserID(1) + if len(attendances) != 2 { + t.Errorf("Expected 2 attendances, got: %d", len(attendances)) + } + + // Test GetMeetingAttendancesByUserID with an invalid user ID + attendances, _ = mr.GetMeetingAttendancesByUserID(999) // Non-existent user ID + if len(attendances) != 0 { + t.Errorf("GetMeetingAttendancesByUserID should have returned an empty slice") + } +} diff --git a/routers/index.go b/routers/index.go index 47d4b09..3f7e714 100644 --- a/routers/index.go +++ b/routers/index.go @@ -86,6 +86,9 @@ func RegisterRoutes(route *gin.Engine) { // Get my team requests, query ?status=accepted/rejected/pending user.GET("/me/requests", middleware.BaseAuthMiddleware(), userController.GetMyRequests) + + // Get past user attendance, TODO: filterable by team + user.GET("/me/attendance", middleware.BaseAuthMiddleware(), meetingController.GetUserAttendanceRecords) } team := v1.Group("/team") diff --git a/services/meeting.service.go b/services/meeting.service.go index 7989818..79ab8bd 100644 --- a/services/meeting.service.go +++ b/services/meeting.service.go @@ -42,6 +42,7 @@ type MeetingServiceInterface interface { MarkAttendanceForUserInMeeting(userID, meetingID uint, attendanceTime time.Time, teamid uint) (bool, error) GetAttendanceForMeeting(meetingID, teamID uint) ([]models.MeetingAttendanceListResponse, error) UpcomingUserMeetings(userID uint) ([]models.UserUpcomingMeetingsListResponse, error) + GetFullUserAttendanceRecord(userID uint) ([]models.MeetingAttendanceListResponse, error) } // CreateMeeting creates a new meeting in the database. @@ -348,4 +349,37 @@ func (ms *MeetingService) UpcomingUserMeetings(userID uint) ([]models.UserUpcomi return meetings, nil } +// GetFullUserAttendanceRecord retrieves all attendance records for a user across meetings and teams. +func (ms *MeetingService) GetFullUserAttendanceRecord(userID uint) ([]models.MeetingAttendanceListResponse, error) { + // Get all attendance records for the user + attendance, err := ms.meetingRepo.GetMeetingAttendancesByUserID(userID) + if err != nil { + return nil, err + } + + // Get meeting details for each attendance record, and make array of MeetingAttendanceResponse + var attendanceResponse []models.MeetingAttendanceListResponse + for _, attendanceRecord := range attendance { + meeting, err := ms.meetingRepo.GetMeetingByID(attendanceRecord.MeetingID) + if err != nil { + return nil, err + } + team, err := ms.teamRepo.GetTeamByID(meeting.TeamID) + if err != nil { + return nil, err + } + attendanceResponse = append(attendanceResponse, models.MeetingAttendanceListResponse{ + ID: attendanceRecord.ID, + MeetingID: attendanceRecord.MeetingID, + AttendanceMarkedAt: attendanceRecord.AttendanceMarkedAt, + OnTime: attendanceRecord.OnTime, + User: models.User{}, + TeamName: team.Name, + MeetingName: meeting.Title, + }) + } + + return attendanceResponse, nil +} + // GetMeetingStatsByMeetingID retrieves meeting stats for a given meeting ID. Stats: total attendance, on time attendance, late attendance. diff --git a/services/meeting.service_test.go b/services/meeting.service_test.go index 7a73bc9..3bef2a9 100644 --- a/services/meeting.service_test.go +++ b/services/meeting.service_test.go @@ -777,3 +777,71 @@ func TestMeetingService_UpcomingUserMeetings(t *testing.T) { assert.NotNil(t, meetings) assert.Equal(t, meetingData[0].Title, meetings[0].Meeting.Title) } + +func TestMeetingService_GetFullUserAttendanceRecord(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockRepo := mocks.NewMockMeetingRepository(ctrl) + mockEmailService := mocks.NewMockEmailService(ctrl) + mockUserRepo := mocks.NewMockUserRepository(ctrl) + mockTeamRepo := mocks.NewMockTeamRepository(ctrl) + mockTeamMemberRepo := mocks.NewMockTeamMemberRepository(ctrl) + + meetingService := NewMeetingService(mockRepo, mockEmailService, mockUserRepo, mockTeamRepo, mockTeamMemberRepo) + + // Define test data + userID := uint(1) + meetingID := uint(2) + meeting2ID := uint(3) + teamID := uint(3) + + // Mock GetMeetingAttendancesByUserID + mockRepo.EXPECT().GetMeetingAttendancesByUserID(userID).Return([]models.MeetingAttendance{ + { + UserID: userID, + MeetingID: meetingID, + AttendanceMarkedAt: time.Now(), + OnTime: true, + }, + { + UserID: userID, + MeetingID: meeting2ID, + AttendanceMarkedAt: time.Now(), + OnTime: false, + }, + }, nil) + + // Mock GetMeetingByID + mockRepo.EXPECT().GetMeetingByID(meetingID).Return(models.Meeting{ + TeamID: teamID, + Title: "Sample Meeting", + }, nil) + + mockRepo.EXPECT().GetMeetingByID(meeting2ID).Return(models.Meeting{ + TeamID: teamID, + Title: "Sample Meeting 2", + }, nil) + + // Mock GetTeamByID for both invocations + for i := 0; i < 2; i++ { + mockTeamRepo.EXPECT().GetTeamByID(teamID).Return(models.Team{ + Name: "Sample Team", + // Add other team details as needed + }, nil) + } + + // Call the method under test + attendanceRecords, err := meetingService.GetFullUserAttendanceRecord(userID) + + // Check the results + assert.NoError(t, err) + assert.NotNil(t, attendanceRecords) + assert.Equal(t, 2, len(attendanceRecords)) + assert.Equal(t, meetingID, attendanceRecords[0].MeetingID) + assert.Equal(t, "Sample Team", attendanceRecords[0].TeamName) + assert.Equal(t, "Sample Meeting", attendanceRecords[0].MeetingName) + assert.Equal(t, meeting2ID, attendanceRecords[1].MeetingID) + assert.Equal(t, "Sample Team", attendanceRecords[1].TeamName) + assert.Equal(t, "Sample Meeting 2", attendanceRecords[1].MeetingName) +}