mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
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:
parent
c2ae4d3748
commit
ef0547f72a
18 changed files with 293 additions and 110 deletions
BIN
app/assets/images/pages/courses/how_to_apply_licenses.png
Executable file
BIN
app/assets/images/pages/courses/how_to_apply_licenses.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -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))
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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, we’ll 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}}"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
4
app/styles/teachers/how-to-enroll-modal.sass
Normal file
4
app/styles/teachers/how-to-enroll-modal.sass
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#how-to-enroll-modal
|
||||||
|
img
|
||||||
|
width: 500px
|
||||||
|
margin: 0 auto
|
|
@ -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")
|
||||||
|
|
13
app/templates/courses/courses-not-assigned-modal.jade
Normal file
13
app/templates/courses/courses-not-assigned-modal.jade
Normal 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")
|
|
@ -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')
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
9
app/views/courses/CoursesNotAssignedModal.coffee
Normal file
9
app/views/courses/CoursesNotAssignedModal.coffee
Normal 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'))
|
|
@ -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()
|
||||||
|
|
|
@ -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" })
|
||||||
|
})
|
||||||
|
|
Loading…
Reference in a new issue