Skip to content

Commit

Permalink
Merge pull request #24684 from code-dot-org/overview-unhide-assigned-…
Browse files Browse the repository at this point in the history
…unit

unhide script and warn when assigning it from the script overview page
  • Loading branch information
davidsbailey committed Sep 8, 2018
2 parents 5b8d882 + 3ad0f2b commit f1fd719
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 16 deletions.
3 changes: 3 additions & 0 deletions apps/i18n/common/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"assignACourse": "Assign a course to your classroom or start your own course.",
"assignCourse": "Assign Course",
"assignConfirm": "Are you sure you want to assign \"{assignmentName}\" to \"{sectionName}\"?",
"assignHiddenUnitConfirm": "It looks like you previously hid \"{assignmentName}\" from section \"{sectionName}\". Do you still want to assign this unit and make it visible?",
"assignedTo": "Assigned to",
"assignUnit": "Assign Unit",
"assignmentSelectorCourse": "Select course",
Expand Down Expand Up @@ -1285,6 +1286,8 @@
"unattachedBlockTipBody": "Blocks that are not attached will not do anything. If you want these blocks in your program, try connecting them to other blocks.",
"unexpectedError": "An unexpected error occurred, please try again. If this keeps happening, try reloading the page.",
"unfeatured": "Unfeatured",
"unhideAndAssignHeader": "This unit is currently hidden from the section",
"unhideUnitAndAssign": "Unhide unit and assign",
"unnamedFunction": "You have a variable or function that does not have a name. Don't forget to give everything a descriptive name.",
"unplugged": "Unplugged",
"unpluggedActivity": "Unplugged Activity",
Expand Down
4 changes: 2 additions & 2 deletions apps/src/code-studio/hiddenStageRedux.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default function reducer(state = new HiddenState(), action) {

if (action.type === UPDATE_HIDDEN_SCRIPT) {
const { sectionId, scriptId, hidden } = action;
const nextState = state.setIn(['scriptsBySection', sectionId, scriptId.toString()], hidden);
const nextState = state.setIn(['scriptsBySection', sectionId.toString(), scriptId.toString()], hidden);
validateSectionIds(nextState);
return nextState;
}
Expand Down Expand Up @@ -262,5 +262,5 @@ function isHiddenForSection(state, sectionId, itemId, bySectionKey) {
sectionId = STUDENT_SECTION_ID;
}
const bySection = state.get(bySectionKey);
return !!bySection.getIn([sectionId, itemId.toString()]);
return !!bySection.getIn([sectionId.toString(), itemId.toString()]);
}
4 changes: 3 additions & 1 deletion apps/src/code-studio/progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import DisabledBubblesAlert from './DisabledBubblesAlert';
import { getStore } from './redux';
import { authorizeLockable } from './stageLockRedux';
import { setViewType, ViewType } from './viewAsRedux';
import { getHiddenStages } from './hiddenStageRedux';
import { getHiddenStages, initializeHiddenScripts } from './hiddenStageRedux';
import { TestResults } from '@cdo/apps/constants';
import {
initProgress,
Expand Down Expand Up @@ -135,6 +135,8 @@ progress.renderCourseProgress = function (scriptData) {
const teacherResources = (scriptData.teacher_resources || []).map(
([type, link]) => ({type, link}));

store.dispatch(initializeHiddenScripts(scriptData.section_hidden_unit_info));

const mountPoint = document.createElement('div');
$('.user-stats-block').prepend(mountPoint);
ReactDOM.render(
Expand Down
21 changes: 18 additions & 3 deletions apps/src/templates/courseOverview/AssignToSection.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { Component, PropTypes } from 'react';
import Radium from 'radium';
import { connect } from 'react-redux';
import $ from 'jquery';
import queryString from 'query-string';
import i18n from '@cdo/locale';
import Button from '@cdo/apps/templates/Button';
import DropdownButton from '@cdo/apps/templates/DropdownButton';
import BaseDialog from '@cdo/apps/templates/BaseDialog';
import ConfirmAssignment from './ConfirmAssignment';
import { isScriptHiddenForSection, updateHiddenScript } from '@cdo/apps/code-studio/hiddenStageRedux';

const styles = {
main: {
Expand All @@ -28,6 +30,8 @@ class AssignToSection extends Component {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
hiddenStageState: PropTypes.object,
updateHiddenScript: PropTypes.func.isRequired,
};

state = {
Expand All @@ -47,7 +51,8 @@ class AssignToSection extends Component {
};

updateCourse = () => {
const section = this.props.sectionsInfo[this.state.sectionIndexToAssign];
const { sectionsInfo, updateHiddenScript, scriptId } = this.props;
const section = sectionsInfo[this.state.sectionIndexToAssign];
$.ajax({
url: `/dashboardapi/sections/${section.id}`,
method: 'PATCH',
Expand All @@ -57,6 +62,7 @@ class AssignToSection extends Component {
script_id: this.props.scriptId,
}),
}).done(result => {
updateHiddenScript(section.id, scriptId, false);
this.setState({
sectionIndexToAssign: null
});
Expand All @@ -74,10 +80,12 @@ class AssignToSection extends Component {
};

render() {
const { courseId, scriptId, assignmentName, sectionsInfo } = this.props;
const { courseId, scriptId, assignmentName, sectionsInfo, hiddenStageState } = this.props;
const { sectionIndexToAssign, errorString } = this.state;
const section = sectionsInfo[sectionIndexToAssign];
const queryParams = queryString.stringify({courseId, scriptId});
const isHiddenFromSection = section && hiddenStageState &&
isScriptHiddenForSection(hiddenStageState, section.id, scriptId);

return (
<div style={styles.main}>
Expand Down Expand Up @@ -108,6 +116,7 @@ class AssignToSection extends Component {
assignmentName={assignmentName}
onClose={this.onCloseDialog}
onConfirm={this.updateCourse}
isHiddenFromSection={isHiddenFromSection}
/>
)}
{errorString && (
Expand All @@ -123,4 +132,10 @@ class AssignToSection extends Component {
}
}

export default Radium(AssignToSection);
export const UnconnectedAssignToSection = Radium(AssignToSection);

export default connect(state => ({
hiddenStageState: state.hiddenStage,
}), ({
updateHiddenScript,
}))(UnconnectedAssignToSection);
5 changes: 3 additions & 2 deletions apps/src/templates/courseOverview/AssignToSection.story.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import AssignToSection from './AssignToSection';
import {UnconnectedAssignToSection as AssignToSection} from './AssignToSection';

const defaultProps = {
courseId: 30,
Expand All @@ -21,7 +21,8 @@ const defaultProps = {
id: 338,
name: "section_with_course"
}
]
],
updateHiddenScript: () => {},
};

export default storybook => {
Expand Down
14 changes: 10 additions & 4 deletions apps/src/templates/courseOverview/ConfirmAssignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,24 @@ export default class ConfirmAssignment extends Component {
assignmentName: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
isHiddenFromSection: PropTypes.bool,
};

render() {
const { sectionName, assignmentName, onClose, onConfirm } = this.props;
const { sectionName, assignmentName, onClose, onConfirm, isHiddenFromSection } = this.props;
const header = isHiddenFromSection ? i18n.unhideAndAssignHeader() : i18n.assignCourse();
const content = isHiddenFromSection ?
i18n.assignHiddenUnitConfirm({assignmentName, sectionName}) :
i18n.assignConfirm({assignmentName, sectionName});
const buttonText = isHiddenFromSection ? i18n.unhideUnitAndAssign() : i18n.assign();

return (
<BaseDialog isOpen={true} handleClose={onClose}>
<div style={styles.header}>
{i18n.assignCourse()}
{header}
</div>
<div style={styles.content}>
{i18n.assignConfirm({assignmentName, sectionName})}
{content}
</div>
<div style={{textAlign: 'right'}}>
<Button
Expand All @@ -54,7 +60,7 @@ export default class ConfirmAssignment extends Component {
color={Button.ButtonColor.gray}
/>
<Button
text={i18n.assign()}
text={buttonText}
style={{marginLeft: 5}}
onClick={onConfirm}
color={Button.ButtonColor.orange}
Expand Down
39 changes: 37 additions & 2 deletions apps/test/unit/templates/courseOverview/AssignToSectionTest.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { assert } from '../../../util/configuredChai';
import React from 'react';
import { shallow, mount } from 'enzyme';
import AssignToSection from '@cdo/apps/templates/courseOverview/AssignToSection';
import {UnconnectedAssignToSection as AssignToSection} from '@cdo/apps/templates/courseOverview/AssignToSection';
import Immutable from 'immutable';

const defaultProps = {
courseId: 30,
Expand All @@ -23,7 +24,8 @@ const defaultProps = {
id: 338,
name: "section_with_course"
}
]
],
updateHiddenScript: () => {},
};

describe('AssignToSection', () => {
Expand Down Expand Up @@ -81,6 +83,39 @@ describe('AssignToSection', () => {
const confirm = wrapper.find('ConfirmAssignment');
assert.equal(confirm.props().assignmentName, 'Computer Science Principles');
assert.equal(confirm.props().sectionName, 'brent_section');
assert.equal(confirm.find('Button').at(1).text(), 'Assign');
});

it('shows a warning when clicking a hidden section', () => {
const scriptId = 99;
const sectionId = defaultProps.sectionsInfo[0].id;
const hiddenStageState = Immutable.fromJS({
scriptsBySection: { [sectionId]: {[scriptId]: true} }
});

const props = {
...defaultProps,
courseId: undefined,
scriptId,
hiddenStageState,
};
const wrapper = mount(
<AssignToSection {...props}/>
);
wrapper.find('Button').simulate('click');
const firstSection = wrapper.find('a').at(1);
assert.equal(firstSection.props()['data-section-index'], 0);
// Enzyme simulate doesn't set target for us automatically, so we fake one.
const target = {
getAttribute: () => 0
};
firstSection.simulate('click', {target});

assert.strictEqual(wrapper.state().sectionIndexToAssign, 0);
const confirm = wrapper.find('ConfirmAssignment');
assert.equal(confirm.props().assignmentName, 'Computer Science Principles');
assert.equal(confirm.props().sectionName, 'brent_section');
assert.equal(confirm.find('Button').at(1).text(), 'Unhide unit and assign');
});

it('shows an error dialog when we have an error', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('CourseOverviewTopRow', () => {
{...defaultProps}
/>
);
assert.equal(wrapper.find('AssignToSection').length, 1);
assert.equal(wrapper.find('Connect(AssignToSection)').length, 1);
});

it('has a button for each resource', () => {
Expand Down
3 changes: 3 additions & 0 deletions dashboard/app/controllers/api/v1/sections_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,13 @@ def update

if script_id
script = Script.get_from_cache(script_id)
return head :bad_request if script.nil?
# If given a course and script, make sure the script is in that course
return head :bad_request if course_id && course_id != script.course.try(:id)
# If script has a course and no course_id was provided, use default course
course_id ||= script.course.try(:id)
# Unhide script for this section before assigning
section.toggle_hidden_script script, false
end

# TODO: (madelynkasula) refactor to use strong params
Expand Down
15 changes: 14 additions & 1 deletion dashboard/app/models/script.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,8 @@ def summarize(include_stages = true, user = nil)
show_course_unit_version_warning: !course&.has_dismissed_version_warning?(user) && has_older_course_progress,
show_script_version_warning: !user_script&.version_warning_dismissed && !has_older_course_progress && has_older_script_progress,
versions: summarize_versions,
supported_locales: supported_locales
supported_locales: supported_locales,
section_hidden_unit_info: section_hidden_unit_info(user),
}

summary[:stages] = stages.map(&:summarize) if include_stages
Expand All @@ -1111,6 +1112,18 @@ def summarize(include_stages = true, user = nil)
summary
end

# @return {Hash<string,number[]>}
# For teachers, this will be a hash mapping from section id to a list of hidden
# script ids for that section, filtered so that the only script id which appears
# is the current script id. This mirrors the output format of
# User#get_hidden_script_ids, and satisfies the input format of
# initializeHiddenScripts in hiddenStageRedux.js.
def section_hidden_unit_info(user)
return {} unless user&.teacher?
hidden_section_ids = SectionHiddenScript.where(script_id: id, section: user.sections).pluck(:section_id)
hidden_section_ids.map {|section_id| [section_id, [id]]}.to_h
end

# Similar to summarize, but returns an even more narrow set of fields, restricted
# to those needed in header.html.haml
def summarize_header
Expand Down
11 changes: 11 additions & 0 deletions dashboard/test/controllers/api/v1/sections_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,17 @@ class Api::V1::SectionsControllerTest < ActionController::TestCase
assert_nil section.script_id
end

test "update: hidden script is unhidden when assigned" do
sign_in @teacher
@section.toggle_hidden_script @csp_script, true
refute_nil SectionHiddenScript.find_by(script: @csp_script, section: @section)
post :update, params: {
id: @section.id,
script_id: @csp_script.id,
}
assert_nil SectionHiddenScript.find_by(script: @csp_script, section: @section)
end

test "update: cannot update section you dont own" do
other_teacher = create(:teacher)
sign_in other_teacher
Expand Down
32 changes: 32 additions & 0 deletions dashboard/test/models/script_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,38 @@ def populate_cache_and_disconnect_db
assert_equal ['English', 'fr-fr'], script.supported_locale_names
end

test 'section_hidden_unit_info' do
teacher = create :teacher
section1 = create :section, user: teacher
assert_equal({}, @script_in_course.section_hidden_unit_info(teacher))

create :section_hidden_script, section: section1, script: @script_in_course
assert_equal({section1.id => [@script_in_course.id]}, @script_in_course.section_hidden_unit_info(teacher))

# other script has no effect
other_script = create :script
create :section_hidden_script, section: section1, script: other_script
assert_equal({section1.id => [@script_in_course.id]}, @script_in_course.section_hidden_unit_info(teacher))

# other teacher's sections have no effect
other_teacher = create :teacher
other_teacher_section = create :section, user: other_teacher
create :section_hidden_script, section: other_teacher_section, script: @script_in_course
assert_equal({section1.id => [@script_in_course.id]}, @script_in_course.section_hidden_unit_info(teacher))

# other section for same teacher hidden for same script appears in list
section2 = create :section, user: teacher
assert_equal({section1.id => [@script_in_course.id]}, @script_in_course.section_hidden_unit_info(teacher))
create :section_hidden_script, section: section2, script: @script_in_course
assert_equal(
{
section1.id => [@script_in_course.id],
section2.id => [@script_in_course.id]
},
@script_in_course.section_hidden_unit_info(teacher)
)
end

private

def has_hidden_script?(scripts)
Expand Down

0 comments on commit f1fd719

Please sign in to comment.