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

Fix/time logs validation #210

Merged
merged 6 commits into from
Mar 19, 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
21 changes: 21 additions & 0 deletions backend/controllers/timeLogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ const validateTimeEntry = ({ sprint, date, minutes, description }) => {
return null
}

const validateDateRange = (date, start, end) => {
const logDate = new Date(date)
logDate.setHours(0, 0, 0, 0)

const startDate = new Date(start)
startDate.setHours(0, 0, 0, 0)

const endDate = new Date(end)
endDate.setHours(0, 0, 0, 0)

if (logDate < startDate || logDate > endDate) {
return 'The log date is not within sprint start and end date.'
}
return null
}

const fetchFromDb = async (studentNumber) => {
try {
const rawLogs = await db.TimeLog.findAll({
Expand Down Expand Up @@ -122,6 +138,11 @@ timeLogsRouter.post('/', checkLogin, async (req, res) => {
return res.status(404).json({ error: 'Sprint not found.' })
}

const err = validateDateRange(date, sprintRecord.start_date, sprintRecord.end_date)
if (err) {
console.error('error:', err)
return res.status(400).json({ error: err })
}
const sprintId = sprintRecord.id

const timeLog = await db.TimeLog.create({
Expand Down
74 changes: 74 additions & 0 deletions frontend/e2e/cypress/integration/timeLogs.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
describe('Time Logs Page', () => {
beforeEach(() => {
cy.clearLocalStorage()
cy.loginAsRegisteredUser()
cy.createGroup({
name: 'Brand New Group',
topicId: 1,
configurationId: 1,
instructorId: null,
studentIds: ['012345698']
})
cy.visit('/sprints')
cy.get('.sprints-container').should('exist')
cy.get('#sprintNumber').type('1')
cy.get('#startDate').type('2022-01-01')
cy.get('#endDate').type('2022-01-31')
cy.get('.button').click()
cy.wait(200)

cy.visit('/timelogs')
})

//currently just placeholder, no such functionality immpemented
it.skip('does not show log form when there are no sprints', () => {
cy.wait(2500)
cy.get('.timelogs-container-1').should('not.exist')
})

it('creates timeLog successfully', () => {
cy.visit('/timelogs')
cy.get('.timelogs-container-1').should('exist')
cy.get('.input-container').should('exist')
cy.get('.date').type('2022-01-01')
cy.get('.time').type('01:00')
cy.get('.description').type('Test description')
cy.get('.submit-button').click()

cy.get('.notification').should('exist')
cy.get('.notification').should('have.text', 'Time log created successfully')
})

// this test will fail when frontend validation is done properly, remove at that point
it('shows error from backend when creating timeLog fails too short description', () => {
cy.visit('/timelogs')
cy.get('.timelogs-container-1').should('exist')
cy.get('.input-container').should('exist')
cy.get('.date').type('2022-01-01')
cy.get('.time').type('01:00')
cy.get('.description').type('inv')
cy.get('.submit-button').click()

cy.get('.notification').should('exist')
cy.get('.notification').should('have.text', 'Description must be over 5 characters.')
})

it('shows error from backend when creating timeLog fails with data outside sprint', () => {
cy.visit('/timelogs')
cy.get('.timelogs-container-1').should('exist')
cy.get('.input-container').should('exist')
cy.get('.date').type('2022-02-01')
cy.get('.time').type('01:00')
cy.get('.description').type('valid description')
cy.get('.submit-button').click()

cy.get('.notification').should('exist')
cy.get('.notification').should('have.text', 'The log date is not within sprint start and end date.')
})

after(() => {
cy.visit('/sprints')
cy.get('.delete-sprint-button').click()
cy.deleteAllGroups()
})
})
1 change: 1 addition & 0 deletions frontend/src/components/SprintsPage/SprintsDashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ const SprintsPage = (props) => {
<td>
<Button
onClick={() => handleDeleteSprint(sprint.id)}
className = 'delete-sprint-button'
variant="contained"
color="secondary"
>
Expand Down
33 changes: 25 additions & 8 deletions frontend/src/components/TimeLogsPage/TimeLogsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
hoursAndMinutesToMinutes,
} from '../../utils/functions'
import './TimeLogsPage.css'
import * as notificationActions from '../../reducers/actions/notificationActions'

const TimeLogsPage = (props) => {
const [allLogs, setAllLogs] = useState([])
Expand All @@ -28,23 +29,26 @@ const TimeLogsPage = (props) => {
try {
await initializeMyGroup()
} catch (error) {
console.error('Error fetching group:', error)
console.error('Error fethcing group:', error.message, ' / ' , error.response.data.error)
notificationActions.setError(error.response.data.error)
}
}
const fetchTimeLogs = async () => {
try {
const fetchedData = await timeLogsService.getTimeLogs()
setAllLogs(fetchedData)
} catch (error) {
console.error('Error fetching logs:', error)
console.error('Error fethcing timelogs:', error.message, ' / ' , error.response.data.error)
notificationActions.setError(error.response.data.error)
}
}
const fetchSprints = async () => {
try {
const fetchedData = await sprintService.getSprints()
setAllSprints(fetchedData)
} catch (error) {
console.error('Error fetching sprints:', error)
console.error('Error fethcing sprints:', error.message, ' / ' , error.response.data.error)
notificationActions.setError(error.response.data.error)
}
}

Expand Down Expand Up @@ -76,14 +80,25 @@ const TimeLogsPage = (props) => {
tags: [],
groupId: group.id,
}

const updatedLogs = await timeLogsService.createTimeLog(log)
setAllLogs(updatedLogs)
try {
const updatedLogs = await timeLogsService.createTimeLog(log)
setAllLogs(updatedLogs)
props.setSuccess('Time log created successfully')
} catch (error) {
console.error('Error creating time log:', error.message, ' / ' , error.response.data.error)
props.setError(error.response.data.error)
}
}

const handleDelete = async (logId) => {
const updatedLogs = await timeLogsService.deleteTimeLog(logId)
setAllLogs(updatedLogs)
try {
const updatedLogs = await timeLogsService.deleteTimeLog(logId)
setAllLogs(updatedLogs)
props.setSuccess('Time log deleted successfully')
} catch (error) {
console.error('Error deleting time log:', error.message, ' / ' , error.response.data.error)
props.setError(error.response.data.error)
}
}

const handleClickNextSprint = () => {
Expand Down Expand Up @@ -139,6 +154,8 @@ const mapStateToProps = (state) => ({

const mapDispatchToProps = {
initializeMyGroup: myGroupActions.initializeMyGroup,
setError: notificationActions.setError,
setSuccess: notificationActions.setSuccess,
}

export default withRouter(
Expand Down
37 changes: 25 additions & 12 deletions frontend/src/services/timeLogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,37 @@ const url = `${BACKEND_API_BASE}/timelogs`


const getTimeLogs = async () => {
const response = await axios.get(url, {
headers: { Authorization: `Bearer ${getUserToken()}` }
})
return response.data
try {
const response = await axios.get(url, {
headers: { Authorization: `Bearer ${getUserToken()}` }
})
return response.data
} catch (error) {
throw error
}
}

const createTimeLog = async (timeEntry) => {
const response = await axios.post(url, timeEntry, {
headers: { Authorization: `Bearer ${getUserToken()}` }
})
return response.data
try {
const response = await axios.post(url, timeEntry, {
headers: { Authorization: `Bearer ${getUserToken()}` }
})
return response.data
} catch (error) {
throw error
}
}

const deleteTimeLog = async (id) => {
const response = await axios.delete(`${url}/${id}`, {
headers: { Authorization: `Bearer ${getUserToken()}` }
})
return response.data
try {
const response = await axios.delete(`${url}/${id}`, {
headers: { Authorization: `Bearer ${getUserToken()}` }
})
return response.data
} catch (error) {
console.error('error in deleteTimeLog', error.response.data.error)
throw error
}
}

export default { getTimeLogs, createTimeLog, deleteTimeLog }