mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 00:31:21 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
31d36085c4
19 changed files with 294 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 |
|
@ -12,6 +12,7 @@ var goalStates;
|
||||||
|
|
||||||
var allowedOrigins = [
|
var allowedOrigins = [
|
||||||
/https:\/\/codecombat\.com/,
|
/https:\/\/codecombat\.com/,
|
||||||
|
/https?:\/\/cn\.codecombat\.com/,
|
||||||
/http:\/\/localhost:3000/,
|
/http:\/\/localhost:3000/,
|
||||||
/http:\/\/direct\.codecombat\.com/,
|
/http:\/\/direct\.codecombat\.com/,
|
||||||
/http:\/\/staging\.codecombat\.com/,
|
/http:\/\/staging\.codecombat\.com/,
|
||||||
|
|
|
@ -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