Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement route for user attendance records #8

Merged
merged 1 commit into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions controllers/meeting.controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
40 changes: 40 additions & 0 deletions controllers/meeting.controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
13 changes: 13 additions & 0 deletions mocks/meeting.repository_mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
13 changes: 13 additions & 0 deletions mocks/meeting.service_mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
2 changes: 2 additions & 0 deletions models/meeting.model.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type MeetingAttendanceListResponse struct {
AttendanceMarkedAt time.Time
OnTime bool
User User
MeetingName string
TeamName string
}

type UserUpcomingMeetingsListResponse struct {
Expand Down
10 changes: 10 additions & 0 deletions repository/meeting.repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
92 changes: 92 additions & 0 deletions repository/meeting.repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
3 changes: 3 additions & 0 deletions routers/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
34 changes: 34 additions & 0 deletions services/meeting.service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
68 changes: 68 additions & 0 deletions services/meeting.service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading