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

Text responses tab: add textResponsesRedux #22304

Merged
merged 12 commits into from May 10, 2018
Merged
125 changes: 125 additions & 0 deletions apps/src/templates/textResponses/textResponsesRedux.js
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be useful to have a comment explaining that responseData is an object where the key is a script, and the value is etc.

In progress, we appended "byScript" to our names to make it a bit more clear: https://github.com/code-dot-org/code-dot-org/blob/staging/apps/src/templates/sectionProgress/sectionProgressRedux.js#L153

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 apps/test/unit/templates/textResponses/textResponsesReduxTest.js
@@ -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);
});
});
});