Skip to content

Commit

Permalink
Merge branch 'staging' into column-dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
ajpal committed May 13, 2019
2 parents f60cf38 + 4dd53ad commit 9ba45f2
Show file tree
Hide file tree
Showing 28 changed files with 253 additions and 90 deletions.
28 changes: 26 additions & 2 deletions apps/src/applab/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {getAppOptions} from '@cdo/apps/code-studio/initApp/loadApp';
import {AllowedWebRequestHeaders} from '@cdo/apps/util/sharedConstants';
import {actions} from './redux/applab';
import {getStore} from '../redux';
import datasetLibrary from '@cdo/apps/code-studio/datasetLibrary.json';
import $ from 'jquery';

// For proxying non-https xhr requests
Expand Down Expand Up @@ -1797,8 +1798,31 @@ applabCommands.getList = function(opts) {

var handleGetListSync = function(opts, values) {
let columnList = [];
values.forEach(row => columnList.push(row[opts.columnName]));
opts.callback(columnList);

if (values.length > 0) {
values.forEach(row => columnList.push(row[opts.columnName]));
opts.callback(columnList);
return;
}

let url;
datasetLibrary.datasets.forEach(dataset => {
if (dataset.name === opts.tableName) {
url = dataset.url;
}
});
if (url) {
// Import the dataset, then try getList again.
Applab.storage.importDataset(
opts.tableName,
url,
() => applabCommands.getList(opts),
() => console.log('error')
);
} else {
// No dataset with the specified name, call back into interpreter and return the empty list.
opts.callback(columnList);
}
};

var handleGetListSyncError = function(opts, values) {
Expand Down
26 changes: 4 additions & 22 deletions apps/src/code-studio/components/DatasetPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,13 @@ export default class DatasetPicker extends React.Component {
assetChosen: PropTypes.func.isRequired
};

importCsvFromUrl = (name, url) => {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'text';
request.onload = function() {
FirebaseStorage.importCsv(
name,
request.response,
() => console.log('importCsv onSuccess'),
() => console.log('importCsv onError')
);
};
request.onerror = function() {
console.log('onerror');
};
request.send();
};

chooseAsset = (name, url) => {
FirebaseStorage.createTable(
FirebaseStorage.importDataset(
name,
() => console.log('createTable onSuccess'),
() => console.log('createTable onError')
url,
() => console.log('importDataset onSuccess'),
() => console.log('importDataset onError')
);
this.importCsvFromUrl(name, url);
this.props.assetChosen(name);
};

Expand Down
1 change: 1 addition & 0 deletions apps/src/code-studio/components/SoundLibrary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export default class SoundLibrary extends React.Component {
category: '',
search: ''
});
this.sounds.stopAllAudio();
};

animationCategoriesRendering() {
Expand Down
7 changes: 6 additions & 1 deletion apps/src/code-studio/components/SoundPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../../assetManagement/assetPrefix';
import SoundLibrary from './SoundLibrary';
import i18n from '@cdo/locale';
import Sounds from '../../Sounds';

const audioExtension = '.mp3';
const styles = {
Expand Down Expand Up @@ -55,7 +56,11 @@ export default class SoundPicker extends React.Component {

setSoundMode = () => this.setState({mode: MODE.sounds});

setFileMode = () => this.setState({mode: MODE.files});
setFileMode = () => {
let sounds = Sounds.getSingleton();
sounds.stopAllAudio();
this.setState({mode: MODE.files});
};

render() {
const isFileMode = this.state.mode === MODE.files;
Expand Down
6 changes: 6 additions & 0 deletions apps/src/code-studio/datasetLibrary.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"description": "The top 20 songs in many regions of the world, including 30 second MP3 links to most songs",
"url": "/api/v1/dataset-library/spotify.csv",
"columns": "Track Name,Artist,Streams,songURL,Album Image,previewMP3,Position,Region,Country Code"
},
{
"name": "weather",
"description": "Daily weather forecasts for the next five days for a selection of US cities.",
"url": "/api/v1/dataset-library/weather.csv",
"columns": "city,date,lowTemp,highTemp,condition,icon"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,45 @@ import React from 'react';
import _ from 'lodash';
import {Panel} from 'react-bootstrap';

export default class SingleChoiceResponses extends React.Component {
// This component renders a survey answer for answer_type of 'scale',
// 'singleSelect', or 'multiSelect'.

export default class ChoiceResponses extends React.Component {
static propTypes = {
question: PropTypes.string.isRequired,
answers: PropTypes.object.isRequired,
perFacilitator: PropTypes.bool,
numRespondents: PropTypes.number,
answerType: PropTypes.string.isRequired,
possibleAnswers: PropTypes.array.isRequired,
otherText: PropTypes.string
};

getTotalAnswers() {
getTotalRespondents() {
if (this.props.perFacilitator) {
return Object.values(this.props.answers).reduce((sum, answers) => {
return (
sum + Object.values(answers).reduce((subSum, x) => subSum + x, 0)
);
}, 0);
} else {
return Object.values(this.props.answers).reduce((sum, x) => sum + x, 0);
if (this.props.numRespondents !== undefined) {
// Multi-select questions will tell us how many respondents there were,
// so that we can correctly show the percentage of respondents who
// gave a certain answer. (The default technique of counting the number
// of answers doesn't work when a single respondent can choose multiple
// answers, though it continues to work for single-select questions.)
return this.props.numRespondents;
} else {
// There are still multiple paths through summarize_workshop_surveys on
// the server which return results without telling us how many
// respondents there were, and so we maintain this behavior for
// backwards compatibility. This technique counts the number of answers
// provided, but for non-multiSelect questions this works out the same
// as the number of respondents to a question, since there can only be
// one response given per question.
return Object.values(this.props.answers).reduce((sum, x) => sum + x, 0);
}
}
}

Expand Down Expand Up @@ -53,7 +73,7 @@ export default class SingleChoiceResponses extends React.Component {

return (
<tr key={i}>
<td>{this.formatPercentage(count / this.getTotalAnswers())}</td>
<td>{this.formatPercentage(count / this.getTotalRespondents())}</td>
<td style={{paddingLeft: '20px'}}>{count}</td>
<td style={{paddingLeft: '20px'}}>{possibleAnswer}</td>
</tr>
Expand Down Expand Up @@ -117,7 +137,7 @@ export default class SingleChoiceResponses extends React.Component {
{showTotalCount && (
<td style={{paddingLeft: '4px'}}>
{`(${this.formatPercentage(
totalCount / this.getTotalAnswers()
totalCount / this.getTotalRespondents()
)})`}
</td>
)}
Expand Down Expand Up @@ -163,7 +183,7 @@ export default class SingleChoiceResponses extends React.Component {
<tr>
<td>
{this.formatPercentage(
otherAnswers.length / this.getTotalAnswers()
otherAnswers.length / this.getTotalRespondents()
)}
</td>
<td style={{paddingLeft: '20px'}}>{otherAnswers.length}</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from 'react';
import SingleChoiceResponses from './single_choice_responses';
import ChoiceResponses from './choice_responses';
import reactBootstrapStoryDecorator from '../../../reactBootstrapStoryDecorator';

export default storybook => {
storybook
.storiesOf('Single choice responses', module)
.storiesOf('Choice responses', module)
.addDecorator(reactBootstrapStoryDecorator)
.addStoryTable([
{
name: 'Single choice responses without other',
name: 'Choice responses without other',
story: () => (
<SingleChoiceResponses
<ChoiceResponses
question="What is your favorite pizza topping?"
answers={{
Peppers: 4,
Expand All @@ -32,9 +32,9 @@ export default storybook => {
)
},
{
name: 'Single choice responses with others',
name: 'Choice responses with others',
story: () => (
<SingleChoiceResponses
<ChoiceResponses
question={
'What is your favorite pizza topping? Please provide the topping if it is not listed here'
}
Expand Down Expand Up @@ -62,9 +62,9 @@ export default storybook => {
)
},
{
name: 'Single choice selectValue response',
name: 'Choice selectValue response',
story: () => (
<SingleChoiceResponses
<ChoiceResponses
question={'What do you think about pineapples on pizza?'}
answers={{
1: 10,
Expand All @@ -87,7 +87,7 @@ export default storybook => {
{
name: 'Scale ratings',
story: () => (
<SingleChoiceResponses
<ChoiceResponses
question={'How do you feel about deep dish?'}
answers={{
1: 1,
Expand All @@ -102,13 +102,13 @@ export default storybook => {
]);

storybook
.storiesOf('Single choice per-facilitator responses', module)
.storiesOf('Choice per-facilitator responses', module)
.addDecorator(reactBootstrapStoryDecorator)
.addStoryTable([
{
name: 'Single choice responses for only one facilitator',
name: 'Choice responses for only one facilitator',
story: () => (
<SingleChoiceResponses
<ChoiceResponses
question="What is your favorite pizza topping?"
perFacilitator={true}
answers={{
Expand All @@ -132,9 +132,9 @@ export default storybook => {
)
},
{
name: 'Single choice responses without other',
name: 'Choice responses without other',
story: () => (
<SingleChoiceResponses
<ChoiceResponses
question="What is your favorite pizza topping?"
perFacilitator={true}
answers={{
Expand Down Expand Up @@ -167,9 +167,9 @@ export default storybook => {
)
},
{
name: 'Single choice responses with others',
name: 'Choice responses with others',
story: () => (
<SingleChoiceResponses
<ChoiceResponses
question={
'What is your favorite pizza topping? Please provide the topping if it is not listed here'
}
Expand Down Expand Up @@ -211,9 +211,9 @@ export default storybook => {
)
},
{
name: 'Single choice selectValue response',
name: 'Choice selectValue response',
story: () => (
<SingleChoiceResponses
<ChoiceResponses
question={'What do you think about pineapples on pizza?'}
perFacilitator={true}
answers={{
Expand Down Expand Up @@ -249,7 +249,7 @@ export default storybook => {
{
name: 'Scale ratings',
story: () => (
<SingleChoiceResponses
<ChoiceResponses
question={'How do you feel about deep dish?'}
perFacilitator={true}
answers={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import {Tab, Tabs} from 'react-bootstrap';
import SingleChoiceResponses from '../../components/survey_results/single_choice_responses';
import ChoiceResponses from '../../components/survey_results/choice_responses';
import FacilitatorAveragesTable from '../../components/survey_results/facilitator_averages_table';
import TextResponses from '../../components/survey_results/text_responses';
import _ from 'lodash';
Expand Down Expand Up @@ -33,12 +33,26 @@ export default class Results extends React.Component {
return null;
}

if (['scale', 'singleSelect'].includes(question['answer_type'])) {
if (
['scale', 'singleSelect', 'multiSelect'].includes(
question['answer_type']
)
) {
// numRespondents will get either a value (for multiSelect) or undefined.
const numRespondents = answers[questionId].num_respondents;

// Make a copy of the answers without the num_respondents field.
const filteredAnswers = _.omit(
answers[questionId],
'num_respondents'
);

return (
<SingleChoiceResponses
<ChoiceResponses
perFacilitator={section === 'facilitator'}
numRespondents={numRespondents}
question={question['text']}
answers={answers[questionId]}
answers={filteredAnswers}
possibleAnswers={question['options']}
key={i}
answerType={question['answer_type']}
Expand Down
28 changes: 28 additions & 0 deletions apps/src/storage/firebaseStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,34 @@ FirebaseStorage.importCsv = function(
.then(onSuccess, onError);
};

/**
* Imports a dataset specified by a URL. Used by the dataset picker modal to allow users to import
* Code.org provided datasets.
* @param {string} tableName
* @param {string} url
* @param onSuccess
* @param onError
*/
FirebaseStorage.importDataset = function(tableName, url, onSuccess, onError) {
let request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'text';
request.onload = function() {
FirebaseStorage.createTable(
tableName,
() =>
FirebaseStorage.importCsv(
tableName,
request.response,
onSuccess,
onError
),
onError
);
};
request.send();
};

export default FirebaseStorage;

export function initFirebaseStorage(config) {
Expand Down

0 comments on commit 9ba45f2

Please sign in to comment.