Simplify applying licenses

In TeacherClassView, when a teacher assigns a paid course to any unenrolled
student, the view automatically enrolls those students, rather than requiring
the teacher to enroll those students manually first. Update copy throughout.

Also add back (smaller) padding to progress dots in TeacherClassView.
This commit is contained in:
Scott Erickson 2016-08-23 10:43:31 -07:00
parent c2ae4d3748
commit ef0547f72a
18 changed files with 293 additions and 110 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -34,3 +34,5 @@ module.exports = class CocoCollection extends Backbone.Collection
setProjection: (@project) -> setProjection: (@project) ->
stringify: -> return JSON.stringify(@toJSON()) stringify: -> return JSON.stringify(@toJSON())
wait: (event) -> new Promise((resolve) => @once(event, resolve))

View file

@ -84,3 +84,5 @@ module.exports = class CocoClass
playSound: (trigger, volume=1) -> playSound: (trigger, volume=1) ->
Backbone.Mediator.publish 'audio-player:play-sound', trigger: trigger, volume: volume Backbone.Mediator.publish 'audio-player:play-sound', trigger: trigger, volume: volume
wait: (event) -> new Promise((resolve) => @once(event, resolve))

View file

@ -1434,12 +1434,13 @@
earliest_incomplete: "Earliest incomplete level" earliest_incomplete: "Earliest incomplete level"
latest_complete: "Latest completed level" latest_complete: "Latest completed level"
enroll_student: "Enroll student" enroll_student: "Enroll student"
apply_license: "Apply License"
course_progress: "Course Progress" course_progress: "Course Progress"
not_applicable: "N/A" not_applicable: "N/A"
edit: "edit" edit: "edit"
edit_2: "Edit" edit_2: "Edit"
remove: "remove" remove: "remove"
latest_completed: "Latest completed" latest_completed: "Latest completed:" # {change}
sort_by: "Sort by" sort_by: "Sort by"
progress: "Progress" progress: "Progress"
completed: "Completed" completed: "Completed"
@ -1447,6 +1448,7 @@
click_to_view_progress: "click to view progress" click_to_view_progress: "click to view progress"
no_progress: "No progress" no_progress: "No progress"
select_course: "Select course to view" select_course: "Select course to view"
students_not_assigned: "Students who have not been assigned {{courseName}}"
course_overview: "Course Overview" course_overview: "Course Overview"
copy_class_code: "Copy Class Code" copy_class_code: "Copy Class Code"
class_code_blurb: "Students can join your class using this Class Code. No email address is required when creating a Student account with this Class Code." class_code_blurb: "Students can join your class using this Class Code. No email address is required when creating a Student account with this Class Code."
@ -1454,16 +1456,24 @@
class_join_url_blurb: "You can also post this unique class URL to a shared webpage." class_join_url_blurb: "You can also post this unique class URL to a shared webpage."
add_students_manually: "Invite Students by Email" add_students_manually: "Invite Students by Email"
bulk_assign: "Bulk-assign" bulk_assign: "Bulk-assign"
assigned_msg_1: "{{numberAssigned}} students were assigned {{courseName}}."
assigned_msg_2: "{{numberEnrolled}} licenses were applied."
assigned_msg_3: "You now have {{remainingSpots}} available licenses remaining."
assign_course: "Assign Course"
not_assigned_modal_title: "Courses were not assigned"
not_assigned_modal_body_1: "You do not have enough licenses available to assign additional Courses to all {{selected}} selected students."
not_assigned_modal_body_2: "You only have {{totalSpotsAvailable}} licenses available ({{unenrolledStudents}} students did not have an active license)."
not_assigned_modal_body_3: "Please select fewer students, or reach out to {{email}} for assistance."
assign_to_selected_students: "Assign to Selected Students" assign_to_selected_students: "Assign to Selected Students"
assigned: "Assigned" assigned: "Assigned"
enroll_selected_students: "Enroll Selected Students" enroll_selected_students: "Enroll Selected Students"
cant_assign_to_unenrolled: "Course cannot be assigned to students who are not enrolled."
no_students_selected: "No students were selected." no_students_selected: "No students were selected."
guides_coming_soon: "Guides coming soon!" # Courses guides_coming_soon: "Guides coming soon!" # Courses
show_students_from: "Show students from" # Enroll students modal show_students_from: "Show students from" # Enroll students modal
enroll_the_following_students: "Enroll the following students" apply_licenses_to_the_following_students: "Apply Licenses to the Following Students"
students_have_licenses: "The following students already have licenses applied:"
all_students: "All Students" all_students: "All Students"
enroll_students: "Enroll Students" apply_licenses: "Apply Licenses"
not_enough_enrollments: "Not enough licenses available." not_enough_enrollments: "Not enough licenses available."
enrollments_blurb_1: "Students taking Computer Science" enrollments_blurb_1: "Students taking Computer Science"
enrollments_blurb_2: "require a license to access the courses." enrollments_blurb_2: "require a license to access the courses."
@ -1476,9 +1486,7 @@
purchased: "Purchased!" purchased: "Purchased!"
purchase_now: "Purchase Now" purchase_now: "Purchase Now"
how_to_enroll: "How to Enroll Students" how_to_enroll: "How to Enroll Students"
how_to_enroll_blurb_1: "If a student is not enrolled yet, there will be an \"Enroll\" button next to their course progress in your class." how_to_apply_licenses: "How to Apply Licenses"
how_to_enroll_blurb_2: "To bulk-enroll multiple students, select them using the checkboxes on the left side of the classroom page and click the \"Enroll Selected Students\" button."
how_to_enroll_blurb_3: "Once a student is enrolled, they will have access to all of the course content."
bulk_pricing_blurb: "Purchasing for more than 25 students? Contact us to discuss next steps." bulk_pricing_blurb: "Purchasing for more than 25 students? Contact us to discuss next steps."
total_unenrolled: "Total unenrolled" total_unenrolled: "Total unenrolled"
export_student_progress: "Export Student Progress (CSV)" export_student_progress: "Export Student Progress (CSV)"
@ -1495,11 +1503,12 @@
end_date: "end date:" end_date: "end date:"
num_enrollments_needed: "Number of licenses needed:" num_enrollments_needed: "Number of licenses needed:"
get_enrollments_blurb: " We'll help you build a solution that meets the needs of your class, school or district." get_enrollments_blurb: " We'll help you build a solution that meets the needs of your class, school or district."
enroll_request_sent_blurb1: "Thanks! Your request has been sent." how_to_apply_licenses_blurb_1: "When a teacher assigns a course to a student for the first time, well automatically apply a license. Use the bulk-assign dropdown in your classroom to assign a course to selected students:"
enroll_request_sent_blurb2: "Our classroom success team will be in touch shortly to help you find the best solution for your students' needs!" how_to_apply_licenses_blurb_2: "Can I still apply a license without assigning a course?"
enroll_request_sent_blurb3: "Please reach out to <a href='mailto:schools@codecombat.com'>schools@codecombat.com</a> if you have additional questions at this time." how_to_apply_licenses_blurb_3: "Yes — go to the License Status tab in your classroom and click \"Apply License\" to any student who does not have an active license."
request_sent: "Request Sent!" request_sent: "Request Sent!"
enrollment_status: "Enrollment Status" enrollment_status: "Enrollment Status"
license_status: "License Status"
status_expired: "Expired on {{date}}" status_expired: "Expired on {{date}}"
status_not_enrolled: "Not Enrolled" status_not_enrolled: "Not Enrolled"
status_enrolled: "Expires on {{date}}" status_enrolled: "Expires on {{date}}"

View file

@ -460,4 +460,6 @@ class CocoModel extends Backbone.Model
stringify: -> return JSON.stringify(@toJSON()) stringify: -> return JSON.stringify(@toJSON())
wait: (event) -> new Promise((resolve) => @once(event, resolve))
module.exports = CocoModel module.exports = CocoModel

View file

@ -36,11 +36,12 @@ module.exports = class CourseInstance extends CocoModel
@trigger 'add-members', { userIDs } @trigger 'add-members', { userIDs }
} }
_.extend options, opts _.extend options, opts
@fetch(options) jqxhr = @fetch(options)
if me.id in userIDs if me.id in userIDs
unless me.get('courseInstances') unless me.get('courseInstances')
me.set('courseInstances', []) me.set('courseInstances', [])
me.get('courseInstances').push(@id) me.get('courseInstances').push(@id)
return jqxhr
removeMember: (userID, opts) -> removeMember: (userID, opts) ->
options = { options = {

View file

@ -19,6 +19,7 @@ module.exports = class Prepaid extends CocoModel
maxRedeemers = @get('maxRedeemers') maxRedeemers = @get('maxRedeemers')
if _.isString(maxRedeemers) if _.isString(maxRedeemers)
@set 'maxRedeemers', parseInt(maxRedeemers) @set 'maxRedeemers', parseInt(maxRedeemers)
super(arguments...)
status: -> status: ->
endDate = @get('endDate') endDate = @get('endDate')

View file

@ -260,6 +260,7 @@
min-width: 34px min-width: 34px
height: 34px height: 34px
border-radius: 16px border-radius: 16px
padding: 0 5px
// margin-top: 23px // margin-top: 23px
// margin-bottom: 23px // margin-bottom: 23px
background: $gray-light background: $gray-light
@ -337,7 +338,7 @@
top: 8px top: 8px
right: 5px right: 5px
#enrollment-status-table #license-status-table
// These column widths are just to keep the cells from resizing on search // These column widths are just to keep the cells from resizing on search
.checkbox-col .checkbox-col
width: 75px width: 75px

View file

@ -0,0 +1,4 @@
#how-to-enroll-modal
img
width: 500px
margin: 0 auto

View file

@ -3,7 +3,7 @@ extends /templates/core/modal-base-flat
block modal-header-content block modal-header-content
.clearfix .clearfix
.text-center .text-center
h1(data-i18n="teacher.enroll_students") h1(data-i18n="teacher.apply_licenses")
h2(data-i18n="courses.grants_lifetime_access") h2(data-i18n="courses.grants_lifetime_access")
block modal-body-content block modal-body-content
@ -26,7 +26,7 @@ block modal-body-content
option(selected=(!view.classroom), value='' data-i18n='teacher.all_students') option(selected=(!view.classroom), value='' data-i18n='teacher.all_students')
form.form.m-t-3 form.form.m-t-3
span(data-i18n="teacher.enroll_the_following_students") span(data-i18n="teacher.apply_licenses_to_the_following_students")
span : span :
.well.form-group .well.form-group
- var enrolledUsers = view.users.filter(function(user){ return user.isEnrolled() }) - var enrolledUsers = view.users.filter(function(user){ return user.isEnrolled() })
@ -39,8 +39,7 @@ block modal-body-content
span.spr= user.broadName() span.spr= user.broadName()
if enrolledUsers.length > 0 if enrolledUsers.length > 0
.small-details.m-t-3 .small-details.m-t-3
span(data-i18n='TODO') span(data-i18n='teacher.students_have_licenses')
| The following students are already enrolled:
for user in enrolledUsers for user in enrolledUsers
- var selected = Boolean(paid || state.get('selectedUsers').get(user.id)) - var selected = Boolean(paid || state.get('selectedUsers').get(user.id))
.checkbox .checkbox
@ -62,12 +61,11 @@ block modal-body-content
p p
button#activate-licenses-btn.btn.btn-lg.btn-primary(type="submit" class=(tooManySelected || noneSelected ? 'disabled' : '')) button#activate-licenses-btn.btn.btn-lg.btn-primary(type="submit" class=(tooManySelected || noneSelected ? 'disabled' : ''))
span.spr(data-i18n="courses.enroll") span(data-i18n="teacher.apply_licenses")
| ( | (
span#total-selected-span span#total-selected-span
= numToEnroll = numToEnroll
| ) | )
span.spl(data-i18n="courses.students1")
p p
a#get-more-licenses-btn.btn.btn-lg.btn-primary-alt(href="/teachers/licenses", data-i18n="courses.get_enrollments") a#get-more-licenses-btn.btn.btn-lg.btn-primary-alt(href="/teachers/licenses", data-i18n="courses.get_enrollments")

View file

@ -0,0 +1,13 @@
extends /templates/core/modal-base-flat
block modal-header-content
h1(data-i18n="teacher.not_assigned_modal_title")
block modal-body-content
p= translate("teacher.not_assigned_modal_body_1").replace('{{selected}}', view.selected)
p= translate("teacher.not_assigned_modal_body_2").replace('{{totalSpotsAvailable}}', view.totalSpotsAvailable).replace('{{unenrolledStudents}}', view.unenrolledStudents)
p!= translate("teacher.not_assigned_modal_body_3").replace('{{email}}', "<a href='mailto:support@codecombat.com'>support@codecombat.com</a>")
block modal-footer-content
button.btn.btn-primary(data-dismiss="modal", data-i18n="modal.okay")

View file

@ -36,7 +36,7 @@ block content
.pull-right .pull-right
span.glyphicon.glyphicon-question-sign span.glyphicon.glyphicon-question-sign
=' ' =' '
a#how-to-enroll-link(data-i18n="teacher.how_to_enroll") a#how-to-enroll-link(data-i18n="teacher.how_to_apply_licenses")
h3(data-i18n='teacher.enrollments') h3(data-i18n='teacher.enrollments')
h4#enrollments-blurb h4#enrollments-blurb
span(data-i18n='teacher.enrollments_blurb_1') span(data-i18n='teacher.enrollments_blurb_1')
@ -131,4 +131,4 @@ mixin enrollmentStats
.text-center .text-center
button#enroll-students-btn.btn.btn-lg.btn-navy button#enroll-students-btn.btn.btn-lg.btn-navy
span(data-i18n='teacher.enroll_students') span(data-i18n='teacher.apply_licenses')

View file

@ -119,9 +119,9 @@ block content
a.course-progress-tab-btn(href='#course-progress-tab') a.course-progress-tab-btn(href='#course-progress-tab')
.small-details.text-center(data-i18n='teacher.course_progress') .small-details.text-center(data-i18n='teacher.course_progress')
.tab-spacer .tab-spacer
li(class=(activeTab === "#enrollment-status-tab" ? 'active' : '')) li(class=(activeTab === "#license-status-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#enrollment-status-tab') a.course-progress-tab-btn(href='#license-status-tab')
.small-details.text-center(data-i18n='teacher.enrollment_status') .small-details.text-center(data-i18n='teacher.license_status')
.tab-filler .tab-filler
.tab-content .tab-content
@ -129,7 +129,7 @@ block content
+studentsTab +studentsTab
else if activeTab === '#course-progress-tab' else if activeTab === '#course-progress-tab'
+courseProgressTab +courseProgressTab
else if activeTab === '#enrollment-status-tab' else if activeTab === '#license-status-tab'
+enrollmentStatusTab +enrollmentStatusTab
else else
@ -215,7 +215,6 @@ mixin studentRow(student)
div div
i i
span(data-i18n='teacher.latest_completed') span(data-i18n='teacher.latest_completed')
span :
div div
+longLevelName(student.latestCompleteLevel) +longLevelName(student.latestCompleteLevel)
td td
@ -231,8 +230,6 @@ mixin studentRow(student)
//- - var level = ??? //- - var level = ???
- var label = courseLabelsArray[index]; - var label = courseLabelsArray[index];
+studentCourseProgressDot(progress, levelsTotal, level, label) +studentCourseProgressDot(progress, levelsTotal, level, label)
unless student.isEnrolled()
+enrollStudentButton(student)
//- td //- td
//- span.view-class-arrow.glyphicon.glyphicon-chevron-right //- span.view-class-arrow.glyphicon.glyphicon-chevron-right
td td
@ -244,10 +241,6 @@ mixin studentRow(student)
div.glyphicon.glyphicon-remove div.glyphicon.glyphicon-remove
div(data-i18n='teacher.remove') div(data-i18n='teacher.remove')
mixin enrollStudentButton(student)
a.enroll-student-button.btn.btn-lg.btn-primary(data-classroom-id=view.classroom.id data-user-id=student.id data-event-action="Teachers Class Students Enroll Student")
span(data-i18n='teacher.enroll_student')
mixin courseProgressTab mixin courseProgressTab
#course-progress-tab.m-t-3 #course-progress-tab.m-t-3
if view.courses if view.courses
@ -267,14 +260,11 @@ mixin courseProgressTab
each student in state.get('students').models each student in state.get('students').models
if _.contains(state.get('selectedCourse').members, student.id) if _.contains(state.get('selectedCourse').members, student.id)
+studentLevelsRow(student) +studentLevelsRow(student)
//- TODO: If any students aren't assigned the course
.unassigned-students.render-on-course-sync .unassigned-students.render-on-course-sync
if state.get('selectedCourse') && state.get('selectedCourse').members.length < state.get('students').length if state.get('selectedCourse') && state.get('selectedCourse').members.length < state.get('students').length
h2 h2
span(data-i18n='TODO') - var courseName = i18n(state.get('selectedCourse').attributes, 'name');
| Students who have not been assigned span= translate('teacher.students_not_assigned').replace('{{courseName}}', courseName)
|
span= state.get('selectedCourse').get('name')
for student in state.get('students').models for student in state.get('students').models
unless _.contains(state.get('selectedCourse').members, student.id) unless _.contains(state.get('selectedCourse').members, student.id)
.row.unassigned-student-row.alternating-background .row.unassigned-student-row.alternating-background
@ -285,19 +275,11 @@ mixin courseProgressTab
.col-sm-4 .col-sm-4
.latest-completed.truncate.small .latest-completed.truncate.small
i.m-r-1 i.m-r-1
span(data-i18n='TODO') span(data-i18n='teacher.latest_completed')
| Latest completed
| :
+longLevelName(student.latestCompleteLevel) +longLevelName(student.latestCompleteLevel)
.col-sm-2 .col-sm-2
if student.isEnrolled()
.assign-student-button.btn.btn-md.btn-navy.pull-right(data-user-id=student.id data-course-id=state.get('selectedCourse').id) .assign-student-button.btn.btn-md.btn-navy.pull-right(data-user-id=student.id data-course-id=state.get('selectedCourse').id)
span(data-i18n='TODO') span(data-i18n='teacher.assign_course')
| Assign Course
else
.enroll-student-button.btn.btn-md.btn-navy.pull-right(data-user-id=student.id data-event-action="Teachers Class Course Enroll Student")
span(data-i18n='TODO')
| Enroll Student
mixin courseOverview mixin courseOverview
- var course = state.get('selectedCourse') - var course = state.get('selectedCourse')
@ -417,8 +399,6 @@ mixin bulkAssignControls
.bulk-assign-controls.form-inline .bulk-assign-controls.form-inline
.no-students-selected.small-details(class=state.get('errors').assigningToNobody ? 'visible' : '') .no-students-selected.small-details(class=state.get('errors').assigningToNobody ? 'visible' : '')
span(data-i18n='teacher.no_students_selected') span(data-i18n='teacher.no_students_selected')
.cant-assign-to-unenrolled.small-details(class=state.get('errors').assigningToUnenrolled ? 'visible' : '')
span(data-i18n='teacher.cant_assign_to_unenrolled')
span.small span.small
span(data-i18n='teacher.bulk_assign') span(data-i18n='teacher.bulk_assign')
span : span :
@ -429,8 +409,6 @@ mixin bulkAssignControls
= i18n(course.attributes, 'name') = i18n(course.attributes, 'name')
button.btn.btn-primary-alt.assign-to-selected-students button.btn.btn-primary-alt.assign-to-selected-students
span(data-i18n='teacher.assign_to_selected_students') span(data-i18n='teacher.assign_to_selected_students')
button.btn.btn-primary-alt.enroll-selected-students
span(data-i18n='teacher.enroll_selected_students')
mixin enrollmentStatusTab mixin enrollmentStatusTab
// TODO: Have search input in all tabs // TODO: Have search input in all tabs
@ -441,7 +419,7 @@ mixin enrollmentStatusTab
// input#student-search.form-control.m-l-1(type="search") // input#student-search.form-control.m-l-1(type="search")
// span.glyphicon.glyphicon-search.form-control-feedback // span.glyphicon.glyphicon-search.form-control-feedback
table.table#enrollment-status-table.table-condensed.m-t-3 table.table#license-status-table.table-condensed.m-t-3
thead thead
// Checkbox code works, but don't need it yet. // Checkbox code works, but don't need it yet.
//th.checkbox-col.select-all //th.checkbox-col.select-all
@ -473,4 +451,4 @@ mixin enrollmentStatusTab
strong(class= status === 'expired' ? 'text-danger' : '')= view.studentStatusString(student) strong(class= status === 'expired' ? 'text-danger' : '')= view.studentStatusString(student)
td.enroll-col td.enroll-col
if status !== 'enrolled' if status !== 'enrolled'
button.enroll-student-button.btn.btn-navy(data-i18n="teacher.enroll_student", data-user-id=student.id, data-event-action="Teachers Class Enrollment Enroll Student") button.enroll-student-button.btn.btn-navy(data-i18n="teacher.apply_license", data-user-id=student.id, data-event-action="Teachers Class Enrollment Enroll Student")

View file

@ -2,11 +2,11 @@ extends /templates/core/modal-base-flat
block modal-header-content block modal-header-content
.text-center .text-center
h3(data-i18n='teacher.how_to_enroll') h3(data-i18n='teacher.how_to_apply_licenses')
block modal-body block modal-body-content
ol p(data-i18n='teacher.how_to_apply_licenses_blurb_1')
li.m-t-1(data-i18n='teacher.how_to_enroll_blurb_1') img.m-y-3(src="/images/pages/courses/how_to_apply_licenses.png")
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_2') h5(data-i18n='teacher.how_to_apply_licenses_blurb_2')
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_3') p(data-i18n='teacher.how_to_apply_licenses_blurb_3')

View file

@ -504,6 +504,8 @@ module.exports = class CocoView extends Backbone.View
message = 'Oops, unable to copy' message = 'Oops, unable to copy'
noty text: message, layout: 'topCenter', type: 'error', killer: false noty text: message, layout: 'topCenter', type: 'error', killer: false
wait: (event) -> new Promise((resolve) => @once(event, resolve))
mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i
mobileREShort = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i mobileREShort = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i

View file

@ -0,0 +1,9 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/courses/courses-not-assigned-modal'
module.exports = class CoursesNotAssignedModal extends ModalView
id: 'courses-not-assigned-modal'
template: template
initialize: (options) ->
_.assign(@, _.pick(options, 'selected', 'totalSpotsAvailable', 'unenrolledStudents'))

View file

@ -7,6 +7,7 @@ InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal' ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
EditStudentModal = require 'views/teachers/EditStudentModal' EditStudentModal = require 'views/teachers/EditStudentModal'
RemoveStudentModal = require 'views/courses/RemoveStudentModal' RemoveStudentModal = require 'views/courses/RemoveStudentModal'
CoursesNotAssignedModal = require './CoursesNotAssignedModal'
Campaigns = require 'collections/Campaigns' Campaigns = require 'collections/Campaigns'
Classroom = require 'models/Classroom' Classroom = require 'models/Classroom'
@ -19,6 +20,7 @@ Course = require 'models/Course'
Courses = require 'collections/Courses' Courses = require 'collections/Courses'
CourseInstance = require 'models/CourseInstance' CourseInstance = require 'models/CourseInstance'
CourseInstances = require 'collections/CourseInstances' CourseInstances = require 'collections/CourseInstances'
Prepaids = require 'collections/Prepaids'
module.exports = class TeacherClassView extends RootView module.exports = class TeacherClassView extends RootView
id: 'teacher-class-view' id: 'teacher-class-view'
@ -38,7 +40,6 @@ module.exports = class TeacherClassView extends RootView
'click .assign-student-button': 'onClickAssignStudentButton' 'click .assign-student-button': 'onClickAssignStudentButton'
'click .enroll-student-button': 'onClickEnrollStudentButton' 'click .enroll-student-button': 'onClickEnrollStudentButton'
'click .assign-to-selected-students': 'onClickBulkAssign' 'click .assign-to-selected-students': 'onClickBulkAssign'
'click .enroll-selected-students': 'onClickBulkEnroll'
'click .export-student-progress-btn': 'onClickExportStudentProgress' 'click .export-student-progress-btn': 'onClickExportStudentProgress'
'click .select-all': 'onClickSelectAll' 'click .select-all': 'onClickSelectAll'
'click .student-checkbox': 'onClickStudentCheckbox' 'click .student-checkbox': 'onClickStudentCheckbox'
@ -55,7 +56,6 @@ module.exports = class TeacherClassView extends RootView
joinURL: "" joinURL: ""
errors: errors:
assigningToNobody: false assigningToNobody: false
assigningToUnenrolled: false
selectedCourse: undefined selectedCourse: undefined
checkboxStates: {} checkboxStates: {}
classStats: classStats:
@ -85,6 +85,10 @@ module.exports = class TeacherClassView extends RootView
@onKeyPressStudentSearch = _.debounce(@onKeyPressStudentSearch, 200) @onKeyPressStudentSearch = _.debounce(@onKeyPressStudentSearch, 200)
@sortedCourses = [] @sortedCourses = []
@prepaids = new Prepaids()
@prepaids.comparator = 'endDate' # use prepaids in order of expiration
@supermodel.trackRequest @prepaids.fetchByCreator(me.id)
@students = new Users() @students = new Users()
@listenTo @classroom, 'sync', -> @listenTo @classroom, 'sync', ->
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true) jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
@ -140,8 +144,6 @@ module.exports = class TeacherClassView extends RootView
@state.set selectedCourse: @courses.first() unless @state.get('selectedCourse') @state.set selectedCourse: @courses.first() unless @state.get('selectedCourse')
@listenTo @courseInstances, 'sync change update', -> @listenTo @courseInstances, 'sync change update', ->
@setCourseMembers() @setCourseMembers()
@listenTo @courseInstances, 'add-members', ->
noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000
@listenTo @students, 'sync change update add remove reset', -> @listenTo @students, 'sync change update add remove reset', ->
# Set state/props of things that depend on students? # Set state/props of things that depend on students?
# Set specific parts of state based on the models, rather than just dumping the collection there? # Set specific parts of state based on the models, rather than just dumping the collection there?
@ -173,7 +175,7 @@ module.exports = class TeacherClassView extends RootView
@listenTo @courseInstances, 'sync change update', @debouncedRender @listenTo @courseInstances, 'sync change update', @debouncedRender
@listenTo @state, 'sync change', -> @listenTo @state, 'sync change', ->
if _.isEmpty(_.omit(@state.changed, 'searchTerm')) if _.isEmpty(_.omit(@state.changed, 'searchTerm'))
@renderSelectors('#enrollment-status-table') @renderSelectors('#license-status-table')
else else
@debouncedRender() @debouncedRender()
@listenTo @students, 'sort', @debouncedRender @listenTo @students, 'sort', @debouncedRender
@ -304,12 +306,6 @@ module.exports = class TeacherClassView extends RootView
@enrollStudents(selectedUsers) @enrollStudents(selectedUsers)
window.tracker?.trackEvent $(e.currentTarget).data('event-action'), category: 'Teachers', classroomID: @classroom.id, userID: userID, ['Mixpanel'] window.tracker?.trackEvent $(e.currentTarget).data('event-action'), category: 'Teachers', classroomID: @classroom.id, userID: userID, ['Mixpanel']
onClickBulkEnroll: ->
userIDs = @getSelectedStudentIDs()
selectedUsers = new Users(@students.get(userID) for userID in userIDs)
@enrollStudents(selectedUsers)
window.tracker?.trackEvent 'Teachers Class Students Enroll Selected', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
enrollStudents: (selectedUsers) -> enrollStudents: (selectedUsers) ->
modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students } modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students }
@openModalView(modal) @openModalView(modal)
@ -390,35 +386,95 @@ module.exports = class TeacherClassView extends RootView
onClickBulkAssign: -> onClickBulkAssign: ->
courseID = @$('.bulk-course-select').val() courseID = @$('.bulk-course-select').val()
selectedIDs = @getSelectedStudentIDs() selectedIDs = @getSelectedStudentIDs()
members = selectedIDs.filter (userID) =>
user = @students.get(userID)
user.isEnrolled()
assigningToUnenrolled = _.any selectedIDs, (userID) =>
not @students.get(userID).isEnrolled()
assigningToNobody = selectedIDs.length is 0 assigningToNobody = selectedIDs.length is 0
@state.set errors: { assigningToNobody, assigningToUnenrolled } @state.set errors: { assigningToNobody }
return if assigningToNobody return if assigningToNobody
@assignCourse courseID, members @assignCourse courseID, selectedIDs
window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, ['Mixpanel'] window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, ['Mixpanel']
# TODO: Move this to the model. Use promises/callbacks?
assignCourse: (courseID, members) -> assignCourse: (courseID, members) ->
courseInstance = null
numberEnrolled = 0
remainingSpots = 0
return Promise.resolve()
.then =>
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id }) courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
if courseInstance if not courseInstance
courseInstance.addMembers members
else
courseInstance = new CourseInstance { courseInstance = new CourseInstance {
courseID, courseID,
classroomID: @classroom.id classroomID: @classroom.id
ownerID: @classroom.get('ownerID') ownerID: @classroom.get('ownerID')
aceConfig: {} aceConfig: {}
} }
courseInstance.notyErrors = false # handling manually
@courseInstances.add(courseInstance) @courseInstances.add(courseInstance)
courseInstance.save {}, { return courseInstance.save()
success: ->
courseInstance.addMembers members .then =>
} availablePrepaids = @prepaids.filter((prepaid) -> prepaid.status() is 'available')
null unenrolledStudents = _(members)
.map((userID) => @students.get(userID))
.filter((user) => user.prepaidStatus() isnt 'enrolled')
.value()
totalSpotsAvailable = _.reduce(prepaid.openSpots() for prepaid in availablePrepaids, (val, total) -> val + total) or 0
if totalSpotsAvailable < _.size(unenrolledStudents)
modal = new CoursesNotAssignedModal({
selected: members.length
totalSpotsAvailable
unenrolledStudents: _.size(unenrolledStudents)
})
@openModalView(modal)
error = new Error('Not enough licenses available')
error.handled = true
throw error
numberEnrolled = _.size(unenrolledStudents)
remainingSpots = totalSpotsAvailable - numberEnrolled
requests = []
for prepaid in availablePrepaids
for i in _.range(prepaid.openSpots())
break unless _.size(unenrolledStudents) > 0
user = unenrolledStudents.shift()
requests.push(prepaid.redeem(user))
@trigger 'begin-redeem-for-assign-course'
return $.when(requests...)
.then =>
# refresh prepaids, since the racing multiple parallel redeem requests in the previous `then` probably did not
# end up returning the final result of all those requests together.
@prepaids.fetchByCreator(me.id)
@trigger 'begin-assign-course'
if members.length
return courseInstance.addMembers(members)
.then =>
course = @courses.get(courseID)
lines = [
$.i18n.t('teacher.assigned_msg_1')
.replace('{{numberAssigned}}', members.length)
.replace('{{courseName}}', course.get('name'))
]
if numberEnrolled > 0
lines.push(
$.i18n.t('teacher.assigned_msg_2')
.replace('{{numberEnrolled}}', numberEnrolled)
)
lines.push(
$.i18n.t('teacher.assigned_msg_3')
.replace('{{remainingSpots}}', remainingSpots)
)
noty text: lines.join('<br />'), layout: 'center', type: 'information', killer: true, timeout: 5000
.catch (e) =>
# TODO: Use this handling for errors site-wide?
return if e.handled
throw e if e instanceof Error and application.testing
text = if e instanceof Error then 'Runtime error' else e.responseJSON?.message or e.message or $.i18n.t('loading_error.unknown')
noty { text, layout: 'center', type: 'error', killer: true, timeout: 5000 }
onClickSelectAll: (e) -> onClickSelectAll: (e) ->
e.preventDefault() e.preventDefault()

View file

@ -7,6 +7,7 @@ Courses = require 'collections/Courses'
Levels = require 'collections/Levels' Levels = require 'collections/Levels'
LevelSessions = require 'collections/LevelSessions' LevelSessions = require 'collections/LevelSessions'
CourseInstances = require 'collections/CourseInstances' CourseInstances = require 'collections/CourseInstances'
Prepaids = require 'collections/Prepaids'
describe '/teachers/classes/:handle', -> describe '/teachers/classes/:handle', ->
@ -30,13 +31,15 @@ describe 'TeacherClassView', ->
factories.makeCourse({name: 'Beta Course', releasePhase: 'beta'}), factories.makeCourse({name: 'Beta Course', releasePhase: 'beta'}),
]) ])
@releasedCourses = new Courses(@courses.where({ releasePhase: 'released' })) @releasedCourses = new Courses(@courses.where({ releasePhase: 'released' }))
available = factories.makePrepaid() @available1 = factories.makePrepaid({maxRedeemers: 1})
@available2 = factories.makePrepaid({maxRedeemers: 1})
expired = factories.makePrepaid({endDate: moment().subtract(1, 'day').toISOString()}) expired = factories.makePrepaid({endDate: moment().subtract(1, 'day').toISOString()})
@prepaids = new Prepaids([@available1, @available2, expired])
@students = new Users([ @students = new Users([
factories.makeUser({name: 'Abner'}) factories.makeUser({name: 'Abner'})
factories.makeUser({name: 'Abigail'}) factories.makeUser({name: 'Abigail'})
factories.makeUser({name: 'Abby'}, {prepaid: available}) factories.makeUser({name: 'Abby'}, {prepaid: @available1})
factories.makeUser({name: 'Ben'}, {prepaid: available}) factories.makeUser({name: 'Ben'}, {prepaid: @available1})
factories.makeUser({name: 'Ned'}, {prepaid: expired}) factories.makeUser({name: 'Ned'}, {prepaid: expired})
factories.makeUser({name: 'Ebner'}, {prepaid: expired}) factories.makeUser({name: 'Ebner'}, {prepaid: expired})
]) ])
@ -74,6 +77,7 @@ describe 'TeacherClassView', ->
@view.students.fakeRequests[0].respondWith({ status: 200, responseText: @students.stringify() }) @view.students.fakeRequests[0].respondWith({ status: 200, responseText: @students.stringify() })
@view.classroom.sessions.fakeRequests[0].respondWith({ status: 200, responseText: @levelSessions.stringify() }) @view.classroom.sessions.fakeRequests[0].respondWith({ status: 200, responseText: @levelSessions.stringify() })
@view.levels.fakeRequests[0].respondWith({ status: 200, responseText: @levels.stringify() }) @view.levels.fakeRequests[0].respondWith({ status: 200, responseText: @levels.stringify() })
@view.prepaids.fakeRequests[0].respondWith({ status: 200, responseText: @prepaids.stringify() })
jasmine.demoEl(@view.$el) jasmine.demoEl(@view.$el)
_.defer done _.defer done
@ -94,14 +98,6 @@ describe 'TeacherClassView', ->
# it 'sorts correctly by Progress' # it 'sorts correctly by Progress'
describe 'bulk-assign controls', -> describe 'bulk-assign controls', ->
it 'shows alert when assigning course 2 to unenrolled students', (done) ->
expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(false)
@view.$('.student-row .checkbox-flat').click()
@view.$('.assign-to-selected-students').click()
_.defer =>
expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(true)
done()
it 'shows alert when assigning but no students are selected', (done) -> it 'shows alert when assigning but no students are selected', (done) ->
expect(@view.$('.no-students-selected').hasClass('visible')).toBe(false) expect(@view.$('.no-students-selected').hasClass('visible')).toBe(false)
@view.$('.assign-to-selected-students').click() @view.$('.assign-to-selected-students').click()
@ -116,9 +112,10 @@ describe 'TeacherClassView', ->
# it 'still shows the correct Course Overview progress' # it 'still shows the correct Course Overview progress'
# #
describe 'the Enrollment Status tab', -> describe 'the License Status tab', ->
beforeEach -> beforeEach (done) ->
@view.state.set('activeTab', '#enrollment-status-tab') @view.state.set('activeTab', '#license-status-tab')
_.defer(done)
describe 'Enroll button', -> describe 'Enroll button', ->
it 'calls enrollStudents with that user when clicked', -> it 'calls enrollStudents with that user when clicked', ->
@ -182,6 +179,7 @@ describe 'TeacherClassView', ->
@view.students.fakeRequests[0].respondWith({ status: 200, responseText: @students.stringify() }) @view.students.fakeRequests[0].respondWith({ status: 200, responseText: @students.stringify() })
@view.classroom.sessions.fakeRequests[0].respondWith({ status: 200, responseText: @levelSessions.stringify() }) @view.classroom.sessions.fakeRequests[0].respondWith({ status: 200, responseText: @levelSessions.stringify() })
@view.levels.fakeRequests[0].respondWith({ status: 200, responseText: @levels.stringify() }) @view.levels.fakeRequests[0].respondWith({ status: 200, responseText: @levels.stringify() })
@view.prepaids.fakeRequests[0].respondWith({ status: 200, responseText: @prepaids.stringify() })
jasmine.demoEl(@view.$el) jasmine.demoEl(@view.$el)
_.defer done _.defer done
@ -207,3 +205,110 @@ describe 'TeacherClassView', ->
return true return true
@view.$('.export-student-progress-btn').click() @view.$('.export-student-progress-btn').click()
expect(window.open).toHaveBeenCalled() expect(window.open).toHaveBeenCalled()
describe '.assignCourse(courseID, members)', ->
beforeEach (done) ->
@classroom = factories.makeClassroom({ aceConfig: { language: 'javascript' }}, { courses: @releasedCourses, members: @students, levels: [@levels, new Levels()]})
@courseInstances = new CourseInstances([
factories.makeCourseInstance({}, { course: @releasedCourses.first(), @classroom, members: @students })
factories.makeCourseInstance({}, { course: @releasedCourses.last(), @classroom, members: @students })
])
sessions = []
@finishedStudent = @students.first()
@unfinishedStudent = @students.last()
classLanguage = @classroom.get('aceConfig')?.language
for level in @levels.models
continue if classLanguage and classLanguage is level.get('primerLanguage')
sessions.push(factories.makeLevelSession(
{state: {complete: true}, playtime: 60},
{level, creator: @finishedStudent})
)
sessions.push(factories.makeLevelSession(
{state: {complete: true}, playtime: 60},
{level: @levels.first(), creator: @unfinishedStudent})
)
@levelSessions = new LevelSessions(sessions)
@view = new TeacherClassView({}, @courseInstances.first().id)
@view.classroom.fakeRequests[0].respondWith({ status: 200, responseText: @classroom.stringify() })
@view.courses.fakeRequests[0].respondWith({ status: 200, responseText: @courses.stringify() })
@view.courseInstances.fakeRequests[0].respondWith({ status: 200, responseText: @courseInstances.stringify() })
@view.students.fakeRequests[0].respondWith({ status: 200, responseText: @students.stringify() })
@view.classroom.sessions.fakeRequests[0].respondWith({ status: 200, responseText: @levelSessions.stringify() })
@view.levels.fakeRequests[0].respondWith({ status: 200, responseText: @levels.stringify() })
@view.prepaids.fakeRequests[0].respondWith({ status: 200, responseText: @prepaids.stringify() })
jasmine.demoEl(@view.$el)
_.defer done
describe 'when no course instance exists for the given course', ->
beforeEach (done) ->
@view.courseInstances.reset()
@view.assignCourse(@courses.first().id, @students.pluck('_id').slice(0, 1))
@view.courseInstances.wait('add').then(done)
it 'creates the missing course instance', ->
request = jasmine.Ajax.requests.mostRecent()
expect(request.method).toBe('POST')
expect(request.url).toBe('/db/course_instance')
it 'shows a noty if the course instance request fails', (done) ->
spyOn(window, 'noty').and.callFake(done)
request = jasmine.Ajax.requests.mostRecent()
request.respondWith({
status: 500,
responseText: JSON.stringify({ message: "Internal Server Error" })
})
describe 'when the course is not free and some students are not enrolled', ->
beforeEach (done) ->
# first two students are unenrolled
@view.assignCourse(@courses.first().id, @students.pluck('_id').slice(0, 2))
@view.wait('begin-redeem-for-assign-course').then(done)
it 'enrolls all unenrolled students', (done) ->
numberOfRequests = _(@view.prepaids.models)
.map((prepaid) -> prepaid.fakeRequests.length)
.reduce((num, value) -> num + value)
expect(numberOfRequests).toBe(2)
done()
it 'shows a noty if a redeem request fails', (done) ->
spyOn(window, 'noty').and.callFake(done)
request = jasmine.Ajax.requests.mostRecent()
request.respondWith({
status: 500,
responseText: JSON.stringify({ message: "Internal Server Error" })
})
describe 'when there are not enough licenses available', ->
beforeEach (done) ->
# first four students are unenrolled, but only two licenses are available
@view.assignCourse(@courses.first().id, @students.pluck('_id'))
spyOn(@view, 'openModalView').and.callFake(done)
it 'shows CoursesNotAssignedModal', ->
expect(@view.openModalView).toHaveBeenCalled()
describe 'when there is nothing else to do first', ->
beforeEach (done) ->
@courseInstance = @view.courseInstances.first()
@courseInstance.set('members', [])
@view.assignCourse(@courseInstance.get('courseID'), @students.pluck('_id').slice(2, 4))
@view.wait('begin-assign-course').then(done)
it 'adds students to the course instances', ->
request = jasmine.Ajax.requests.mostRecent()
expect(request.url).toBe("/db/course_instance/#{@courseInstance.id}/members")
expect(request.method).toBe('POST')
it 'shows a noty if POSTing students fails', (done) ->
spyOn(window, 'noty').and.callFake(done)
request = jasmine.Ajax.requests.mostRecent()
request.respondWith({
status: 500,
responseText: JSON.stringify({ message: "Internal Server Error" })
})