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: add experiment, storybook, implement feedback #22394

Merged
merged 4 commits into from
May 14, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion apps/src/sites/code.org/pages/public/teacher-dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
renderLoginTypeControls,
unmountLoginTypeControls,
renderSectionTable,
renderStatsTable
renderStatsTable,
renderTextResponsesTable
} from '@cdo/apps/templates/teacherDashboard/sections';
import logToCloud from '@cdo/apps/logToCloud';
import sectionProgress, {setSection, setValidScripts} from '@cdo/apps/templates/sectionProgress/sectionProgressRedux';
Expand Down Expand Up @@ -900,6 +901,14 @@ function main() {
$scope.sections = sectionsService.query();
$scope.tab = 'responses';

if (experiments.isEnabled(experiments.TEXT_RESPONSES_TAB)) {
$scope.react_text_responses = true;
$scope.$on('text-responses-table-rendered', () => {
$scope.section.$promise.then(section => renderTextResponsesTable(section, valid_scripts));
});
return;
}

$scope.responses = sectionsService.responses({id: $routeParams.id});
// error handling
$scope.genericError = function (result) {
Expand Down
42 changes: 42 additions & 0 deletions apps/src/templates/teacherDashboard/sections.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import manageStudents, {
convertStudentServerData,
toggleSharingColumn,
} from '@cdo/apps/templates/manageStudents/manageStudentsRedux';
import sectionProgress, {setSection, setValidScripts} from '@cdo/apps/templates/sectionProgress/sectionProgressRedux';
import textResponses, {asyncLoadTextResponses, setSectionId as textResponsesSetSectionId} from '@cdo/apps/templates/textResponses/textResponsesRedux';
import SyncOmniAuthSectionControl from '@cdo/apps/lib/ui/SyncOmniAuthSectionControl';
import LoginTypeParagraph from '@cdo/apps/templates/teacherDashboard/LoginTypeParagraph';
import ManageStudentsTable from '@cdo/apps/templates/manageStudents/ManageStudentsTable';
import isRtl from '@cdo/apps/code-studio/isRtlRedux';
import StatsTable from '@cdo/apps/templates/teacherDashboard/StatsTable';
import TextResponses from '@cdo/apps/templates/textResponses/TextResponses';

/**
* On the manage students tab of an oauth section, use React to render a button
Expand Down Expand Up @@ -70,6 +73,45 @@ export function renderLoginTypeControls(sectionId) {
);
}

export function renderTextResponsesTable(section, validScripts) {
const element = document.getElementById('text-responses-table-react');

registerReducers({sectionProgress, textResponses});
const store = getStore();
// data from setSection and setValidScripts (line 100) required on multiple tabs
// TODO (madelynkasula): refactor multi-tab data into common reducer
store.dispatch(setSection(section));
store.dispatch(textResponsesSetSectionId(section.id));

const promises = [
$.ajax({
method: 'GET',
url: `/dashboardapi/sections/${section.id}/student_script_ids`,
dataType: 'json'
}),
$.ajax({
method: 'GET',
url: `/dashboardapi/courses?allVersions=1`,
dataType: 'json'
})
];

Promise.all(promises).then(data => {
let [studentScriptsData, validCourses] = data;
store.dispatch(setValidScripts(validScripts, studentScriptsData.studentScriptIds, validCourses));
const scriptId = store.getState().sectionProgress.scriptId;

store.dispatch(asyncLoadTextResponses(section.id, scriptId, () => {
ReactDOM.render(
<Provider store={store}>
<TextResponses sectionId={section.id}/>
</Provider>,
element
);
}));
});
}

export function renderStatsTable(section) {
const dataUrl = `/dashboardapi/sections/${section.id}/students/completed_levels_count`;
const element = document.getElementById('stats-table-react');
Expand Down
59 changes: 59 additions & 0 deletions apps/src/templates/textResponses/TestResponsesTable.story.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import TextResponsesTable from "./TextResponsesTable";

const sectionId = 1;
const responses = [
{
puzzle: 2,
question: "Check Your Understanding",
response: "Lorem ipsum dolor sit amet, postea pericula",
stage: "Lesson 1",
studentId: 1,
studentName: "Student A",
url: "http://fake.url"
},
{
puzzle: 3,
question: "Free Response",
response: "Lorem ipsum dolor sit amet, postea pericula",
stage: "Lesson 2",
studentId: 3,
studentName: "Student C",
url: "http://fake.url"
},
{
puzzle: 1,
question: "Free Response",
response: "Lorem ipsum dolor sit amet, postea pericula. Lorem ipsum dolor sit amet, postea pericula. Lorem ipsum dolor sit amet, postea pericula",
stage: "Lesson 1",
studentId: 2,
studentName: "Student B",
url: "http://fake.url"
},
];

export default storybook => storybook
.storiesOf('TextResponsesTable', module)
.addStoryTable([
{
name: 'Text responses table',
story: () => (
<TextResponsesTable
responses={responses}
sectionId={sectionId}
isLoading={false}
/>
)
},
{
name: 'Empty text responses table',
description: 'Displays an empty state message',
story: () => (
<TextResponsesTable
responses={[]}
sectionId={sectionId}
isLoading={false}
/>
)
}
]);
1 change: 1 addition & 0 deletions apps/src/templates/textResponses/TextResponses.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ class TextResponses extends Component {
data={filteredResponses}
headers={CSV_HEADERS}
>
{/* onClick functionality for Button handled by CSVLink */}
<Button
text={i18n.downloadCSV()}
onClick={() => {}}
Expand Down
1 change: 1 addition & 0 deletions apps/src/templates/textResponses/textResponsesRedux.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const textResponsePropType = PropTypes.shape({
});

// Initial state of textResponsesRedux
// responseDataByScript - object - key is scriptId, value is array of textResponsePropType
const initialState = {
sectionId: null,
responseDataByScript: {},
Expand Down
1 change: 1 addition & 0 deletions apps/src/util/experiments.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const EXPERIMENT_LIFESPAN_HOURS = 12;
// specific experiment names
experiments.COURSE_VERSIONS = 'courseVersions';
experiments.PROGRESS_TAB = 'sectionProgressRedesign';
experiments.TEXT_RESPONSES_TAB = 'textResponsesRedesign';

// This is a per user experiment and is defined in experiments.rb
// On the front end we are treating it as an experiment group that contains
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,67 @@ theme: none
content-type: text/ng-template
---

%br/
#text-responses-table-react{'ng-init' => '$emit("text-responses-table-rendered");'}

.loading{'ng-hide' => 'responsesLoaded'}
= I18n.t('dashboard_landing_loading')
#text-responses-table-angular{'ng-hide' => 'react_text_responses'}
%br/

%div{'ng-show' => 'responsesLoaded'}
= I18n.t('dashboard_responses_view')
%select#uitest-course-dropdown{"ng-model" => "script_id", "ng-options"=>"script.id as script.name group by script.category for script in script_list | orderBy:['category_priority', 'category', 'position', 'name']", 'ng-change' => 'changeScript(script_id)'}
.loading{'ng-hide' => 'responsesLoaded'}
= I18n.t('dashboard_landing_loading')

%br/
%div{'ng-show' => 'responsesLoaded'}
= I18n.t('dashboard_responses_view')
%select#uitest-course-dropdown{"ng-model" => "script_id", "ng-options"=>"script.id as script.name group by script.category for script in script_list | orderBy:['category_priority', 'category', 'position', 'name']", 'ng-change' => 'changeScript(script_id)'}

:ruby
responses_filtered = "responses | orderBy:order | filter:(stageFilter && {stage: stageFilter}):true"
%br/

%div{style: "padding-left:20px", 'ng-show' => '!responses.length'}
= I18n.t('dashboard_responses_none')
#uitest-responses-tab{style: "padding-left:20px", 'ng-show' => 'responses.length'}
%table{style: "width:950px; table-layout:fixed;", 'ng-show' => 'responses.length', "ng-form" => "allForm"}
%colgroup
%col{width: "200"}/
%col{width: "175"}/
%col{width: "75"}/
%col{width: "175"}/
%col{width: "300"}/
%tr
%th.manage-th{colspan: 5}
%div{'ng-show' => 'stages && stages.length > 1', style: 'float: left; line-height: 35px'}
= I18n.t('dashboard_filter_by_stage')
%select{'ng-model' => 'stageFilter', "ng-options" => "stage for stage in stages", "ng-show" => 'stages'}
%option{value: ""}= I18n.t('dashboard_filter_all')
%button.btn.btn-white{'ng-csv' => responses_filtered,
filename: "responses.csv",
'csv-header' => "['Name', 'Stage', 'Puzzle', 'Question', 'Response']",
'csv-column-order' => "['student.name', 'stage', 'puzzle', 'question', 'response']",
style: 'float: right'}
%i.fa.fa-download
= I18n.t('dashboard_download_csv')
:ruby
responses_filtered = "responses | orderBy:order | filter:(stageFilter && {stage: stageFilter}):true"

%tr
%th.manage-th
%a{href: "", "ng-click" => "order = 'name'"}= I18n.t('dashboard_students_name')
%th.manage-th
%a{href: "", "ng-click" => "order = 'stage'"}= I18n.t('dashboard_stage')
%th.manage-th
%a{href: "", "ng-click" => "order = 'puzzle'"}= I18n.t('dashboard_puzzle')
%th.manage-th
%a{href: "", "ng-click" => "order = 'question'"}= I18n.t('dashboard_question')
%th.manage-th
%a{href: "", "ng-click" => "order = 'response'"}= I18n.t('dashboard_response')
%tr{"ng-repeat" => "response in #{responses_filtered}", "ng-form" => "form"}
%td
%a{"ng-href" => "#/sections/{{section.id}}/student/{{response.student.id}}"} {{response.student.name}}
%td
{{response.stage}}
%td
{{response.puzzle}}
%td
{{response.question}}
%td
{{response.response | limitTo:100}}
%a{'ng-href' => '{{response.url}}', 'ng-show' => 'response.response.length > 100'} ... see full answer
%div{style: "padding-left:20px", 'ng-show' => '!responses.length'}
= I18n.t('dashboard_responses_none')
#uitest-responses-tab{style: "padding-left:20px", 'ng-show' => 'responses.length'}
%table{style: "width:950px; table-layout:fixed;", 'ng-show' => 'responses.length', "ng-form" => "allForm"}
%colgroup
%col{width: "200"}/
%col{width: "175"}/
%col{width: "75"}/
%col{width: "175"}/
%col{width: "300"}/
%tr
%th.manage-th{colspan: 5}
%div{'ng-show' => 'stages && stages.length > 1', style: 'float: left; line-height: 35px'}
= I18n.t('dashboard_filter_by_stage')
%select{'ng-model' => 'stageFilter', "ng-options" => "stage for stage in stages", "ng-show" => 'stages'}
%option{value: ""}= I18n.t('dashboard_filter_all')
%button.btn.btn-white{'ng-csv' => responses_filtered,
filename: "responses.csv",
'csv-header' => "['Name', 'Stage', 'Puzzle', 'Question', 'Response']",
'csv-column-order' => "['student.name', 'stage', 'puzzle', 'question', 'response']",
style: 'float: right'}
%i.fa.fa-download
= I18n.t('dashboard_download_csv')

%tr
%th.manage-th
%a{href: "", "ng-click" => "order = 'name'"}= I18n.t('dashboard_students_name')
%th.manage-th
%a{href: "", "ng-click" => "order = 'stage'"}= I18n.t('dashboard_stage')
%th.manage-th
%a{href: "", "ng-click" => "order = 'puzzle'"}= I18n.t('dashboard_puzzle')
%th.manage-th
%a{href: "", "ng-click" => "order = 'question'"}= I18n.t('dashboard_question')
%th.manage-th
%a{href: "", "ng-click" => "order = 'response'"}= I18n.t('dashboard_response')
%tr{"ng-repeat" => "response in #{responses_filtered}", "ng-form" => "form"}
%td
%a{"ng-href" => "#/sections/{{section.id}}/student/{{response.student.id}}"} {{response.student.name}}
%td
{{response.stage}}
%td
{{response.puzzle}}
%td
{{response.question}}
%td
{{response.response | limitTo:100}}
%a{'ng-href' => '{{response.url}}', 'ng-show' => 'response.response.length > 100'} ... see full answer