-
Notifications
You must be signed in to change notification settings - Fork 479
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22304 from code-dot-org/responses-tab-redux
Text responses tab: add textResponsesRedux
- Loading branch information
Showing
2 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import {PropTypes} from 'react'; | ||
|
||
// Shape for an individual text response | ||
export const textResponsePropType = PropTypes.shape({ | ||
puzzle: PropTypes.number.isRequired, | ||
question: PropTypes.string, | ||
response: PropTypes.string.isRequired, | ||
stage: PropTypes.string.isRequired, | ||
studentId: PropTypes.number.isRequired, | ||
studentName: PropTypes.string.isRequired, | ||
url: PropTypes.string.isRequired | ||
}); | ||
|
||
// Initial state of textResponsesRedux | ||
const initialState = { | ||
sectionId: null, | ||
responseDataByScript: {}, | ||
isLoadingResponses: false | ||
}; | ||
|
||
const SET_SECTION_ID = 'textResponses/SET_SECTION_ID'; | ||
const SET_TEXT_RESPONSES = 'textResponses/SET_TEXT_RESPONSES'; | ||
const START_LOADING_TEXT_RESPONSES = 'textResponses/START_LOADING_TEXT_RESPONSES'; | ||
const FINISH_LOADING_TEXT_RESPONSES = 'textResponses/FINISH_LOADING_TEXT_RESPONSES'; | ||
|
||
// Action creators | ||
export const setSectionId = sectionId => ({ type: SET_SECTION_ID, sectionId }); | ||
export const setTextResponses = (scriptId, responseData) => ({ type: SET_TEXT_RESPONSES, scriptId, responseData}); | ||
export const startLoadingResponses = () => ({ type: START_LOADING_TEXT_RESPONSES }); | ||
export const finishLoadingResponses = () => ({ type: FINISH_LOADING_TEXT_RESPONSES }); | ||
|
||
export const asyncLoadTextResponses = (sectionId, scriptId, onComplete) => { | ||
return (dispatch, getState) => { | ||
const state = getState().textResponses; | ||
|
||
// Don't load data if it's already stored in redux. | ||
if (state.responseDataByScript[scriptId]) { | ||
onComplete(); | ||
return; | ||
} | ||
|
||
dispatch(startLoadingResponses()); | ||
loadTextResponsesFromServer(sectionId, scriptId, (error, data) => { | ||
if (error) { | ||
console.error(error); | ||
} else { | ||
dispatch(setTextResponses(scriptId, data)); | ||
onComplete(); | ||
} | ||
dispatch(finishLoadingResponses()); | ||
}); | ||
}; | ||
}; | ||
|
||
export default function textResponses(state=initialState, action) { | ||
if (action.type === SET_SECTION_ID) { | ||
// Setting the sectionId is the first action to be called when switching | ||
// sections, which requires us to reset our state. This might need to change | ||
// once switching sections is in react/redux. | ||
return { | ||
...initialState, | ||
sectionId: action.sectionId | ||
}; | ||
} | ||
if (action.type === SET_TEXT_RESPONSES) { | ||
return { | ||
...state, | ||
responseDataByScript: { | ||
...state.responseDataByScript, | ||
[action.scriptId]: action.responseData | ||
} | ||
}; | ||
} | ||
if (action.type === START_LOADING_TEXT_RESPONSES) { | ||
return { | ||
...state, | ||
isLoadingResponses: true | ||
}; | ||
} | ||
if (action.type === FINISH_LOADING_TEXT_RESPONSES) { | ||
return { | ||
...state, | ||
isLoadingResponses: false | ||
}; | ||
} | ||
|
||
return state; | ||
} | ||
|
||
// Flatten text responses returned from server to remove nested student object | ||
export const convertTextResponseServerData = (textResponses) => { | ||
let responses = []; | ||
textResponses.forEach(response => { | ||
const {id, name} = response.student; | ||
delete response.student; | ||
|
||
responses.push({ | ||
...response, | ||
studentId: id, | ||
studentName: name | ||
}); | ||
}); | ||
|
||
return responses; | ||
}; | ||
|
||
// Make a request to the server for text responses | ||
// scriptId is not required; endpoint will use the default script if no scriptId is supplied | ||
const loadTextResponsesFromServer = (sectionId, scriptId, onComplete) => { | ||
let payload = {}; | ||
if (scriptId) { | ||
payload.script_id = scriptId; | ||
} | ||
|
||
$.ajax({ | ||
url: `/dashboardapi/section_text_responses/${sectionId}`, | ||
method: 'GET', | ||
contentType: 'application/json;charset=UTF-8', | ||
data: payload | ||
}).done(responseData => { | ||
onComplete(null, convertTextResponseServerData(responseData)); | ||
}).fail((jqXhr, status) => { | ||
onComplete(status, jqXhr.responseJSON); | ||
}); | ||
}; |
109 changes: 109 additions & 0 deletions
109
apps/test/unit/templates/textResponses/textResponsesReduxTest.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import {assert} from '../../../util/configuredChai'; | ||
import textResponses, { | ||
setSectionId, | ||
setTextResponses, | ||
startLoadingResponses, | ||
finishLoadingResponses, | ||
convertTextResponseServerData | ||
} from '@cdo/apps/templates/textResponses/textResponsesRedux'; | ||
|
||
describe('textResponsesRedux', () => { | ||
const initialState = textResponses(undefined, {}); | ||
|
||
describe('setSectionId', () => { | ||
it('sets sectionId', () => { | ||
const action = setSectionId(3); | ||
const nextState = textResponses(initialState, action); | ||
assert.equal(nextState.sectionId, 3); | ||
}); | ||
|
||
it('resets all other state to initialState', () => { | ||
const currentState = { | ||
sectionId: 1, | ||
isLoadingResponses: true, | ||
responseDataByScript: { | ||
1: {question: 'Question 1', response: 'Response 1'}, | ||
2: {question: 'Question 2', response: 'Response 2'} | ||
} | ||
}; | ||
const newSectionId = 2; | ||
const expectedNextState = { | ||
...initialState, | ||
sectionId: newSectionId | ||
}; | ||
const action = setSectionId(newSectionId); | ||
const nextState = textResponses(currentState, action); | ||
assert.deepEqual(nextState, expectedNextState); | ||
}); | ||
}); | ||
|
||
describe('setTextResponses', () => { | ||
it('associates the response data to the correct script', () => { | ||
const scriptId = 2; | ||
const responseData = { | ||
question: 'Free Response', | ||
response: 'I love to code!' | ||
}; | ||
const action = setTextResponses(scriptId, responseData); | ||
const nextState = textResponses(initialState, action); | ||
const actualResponseData = nextState.responseDataByScript[scriptId]; | ||
assert.deepEqual(actualResponseData, responseData); | ||
}); | ||
}); | ||
|
||
describe('startLoadingResponses', () => { | ||
it('sets isLoadingResponses to true', () => { | ||
const action = startLoadingResponses(); | ||
const nextState = textResponses(initialState, action); | ||
assert.isTrue(nextState.isLoadingResponses); | ||
}); | ||
}); | ||
|
||
describe('finishLoadingResponses', () => { | ||
it('sets isLoadingResponses to false', () => { | ||
const action = finishLoadingResponses(); | ||
const nextState = textResponses(initialState, action); | ||
assert.isFalse(nextState.isLoadingResponses); | ||
}); | ||
}); | ||
|
||
describe('convertTextResponseServerData', () => { | ||
it('re-formats server data correctly', () => { | ||
const serverTextResponses = [ | ||
{ | ||
question: 'Question 1', | ||
response: 'Response 1', | ||
student: { | ||
id: 1, | ||
name: 'Student 1' | ||
} | ||
}, | ||
{ | ||
question: 'Question 2', | ||
response: 'Response 2', | ||
student: { | ||
id: 2, | ||
name: 'Student 2' | ||
} | ||
}, | ||
]; | ||
const expectedTextResponses = [ | ||
{ | ||
question: 'Question 1', | ||
response: 'Response 1', | ||
studentId: 1, | ||
studentName: 'Student 1' | ||
}, | ||
{ | ||
question: 'Question 2', | ||
response: 'Response 2', | ||
studentId: 2, | ||
studentName: 'Student 2' | ||
}, | ||
]; | ||
|
||
const convertedTextResponses = convertTextResponseServerData(serverTextResponses); | ||
assert.deepEqual(convertedTextResponses, expectedTextResponses); | ||
}); | ||
}); | ||
}); |