mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 15:48:11 -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 |
|
@ -33,4 +33,6 @@ module.exports = class CocoCollection extends Backbone.Collection
|
|||
|
||||
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) ->
|
||||
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"
|
||||
latest_complete: "Latest completed level"
|
||||
enroll_student: "Enroll student"
|
||||
apply_license: "Apply License"
|
||||
course_progress: "Course Progress"
|
||||
not_applicable: "N/A"
|
||||
edit: "edit"
|
||||
edit_2: "Edit"
|
||||
remove: "remove"
|
||||
latest_completed: "Latest completed"
|
||||
latest_completed: "Latest completed:" # {change}
|
||||
sort_by: "Sort by"
|
||||
progress: "Progress"
|
||||
completed: "Completed"
|
||||
|
@ -1447,6 +1448,7 @@
|
|||
click_to_view_progress: "click to view progress"
|
||||
no_progress: "No progress"
|
||||
select_course: "Select course to view"
|
||||
students_not_assigned: "Students who have not been assigned {{courseName}}"
|
||||
course_overview: "Course Overview"
|
||||
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."
|
||||
|
@ -1454,16 +1456,24 @@
|
|||
class_join_url_blurb: "You can also post this unique class URL to a shared webpage."
|
||||
add_students_manually: "Invite Students by Email"
|
||||
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"
|
||||
assigned: "Assigned"
|
||||
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."
|
||||
guides_coming_soon: "Guides coming soon!" # Courses
|
||||
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"
|
||||
enroll_students: "Enroll Students"
|
||||
apply_licenses: "Apply Licenses"
|
||||
not_enough_enrollments: "Not enough licenses available."
|
||||
enrollments_blurb_1: "Students taking Computer Science"
|
||||
enrollments_blurb_2: "require a license to access the courses."
|
||||
|
@ -1476,9 +1486,7 @@
|
|||
purchased: "Purchased!"
|
||||
purchase_now: "Purchase Now"
|
||||
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_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."
|
||||
how_to_apply_licenses: "How to Apply Licenses"
|
||||
bulk_pricing_blurb: "Purchasing for more than 25 students? Contact us to discuss next steps."
|
||||
total_unenrolled: "Total unenrolled"
|
||||
export_student_progress: "Export Student Progress (CSV)"
|
||||
|
@ -1495,11 +1503,12 @@
|
|||
end_date: "end date:"
|
||||
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."
|
||||
enroll_request_sent_blurb1: "Thanks! Your request has been sent."
|
||||
enroll_request_sent_blurb2: "Our classroom success team will be in touch shortly to help you find the best solution for your students' needs!"
|
||||
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_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:"
|
||||
how_to_apply_licenses_blurb_2: "Can I still apply a license without assigning a course?"
|
||||
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!"
|
||||
enrollment_status: "Enrollment Status"
|
||||
license_status: "License Status"
|
||||
status_expired: "Expired on {{date}}"
|
||||
status_not_enrolled: "Not Enrolled"
|
||||
status_enrolled: "Expires on {{date}}"
|
||||
|
|
|
@ -460,4 +460,6 @@ class CocoModel extends Backbone.Model
|
|||
|
||||
stringify: -> return JSON.stringify(@toJSON())
|
||||
|
||||
wait: (event) -> new Promise((resolve) => @once(event, resolve))
|
||||
|
||||
module.exports = CocoModel
|
||||
|
|
|
@ -36,11 +36,12 @@ module.exports = class CourseInstance extends CocoModel
|
|||
@trigger 'add-members', { userIDs }
|
||||
}
|
||||
_.extend options, opts
|
||||
@fetch(options)
|
||||
jqxhr = @fetch(options)
|
||||
if me.id in userIDs
|
||||
unless me.get('courseInstances')
|
||||
me.set('courseInstances', [])
|
||||
me.get('courseInstances').push(@id)
|
||||
return jqxhr
|
||||
|
||||
removeMember: (userID, opts) ->
|
||||
options = {
|
||||
|
|
|
@ -19,6 +19,7 @@ module.exports = class Prepaid extends CocoModel
|
|||
maxRedeemers = @get('maxRedeemers')
|
||||
if _.isString(maxRedeemers)
|
||||
@set 'maxRedeemers', parseInt(maxRedeemers)
|
||||
super(arguments...)
|
||||
|
||||
status: ->
|
||||
endDate = @get('endDate')
|
||||
|
|
|
@ -260,6 +260,7 @@
|
|||
min-width: 34px
|
||||
height: 34px
|
||||
border-radius: 16px
|
||||
padding: 0 5px
|
||||
// margin-top: 23px
|
||||
// margin-bottom: 23px
|
||||
background: $gray-light
|
||||
|
@ -337,7 +338,7 @@
|
|||
top: 8px
|
||||
right: 5px
|
||||
|
||||
#enrollment-status-table
|
||||
#license-status-table
|
||||
// These column widths are just to keep the cells from resizing on search
|
||||
.checkbox-col
|
||||
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
|
||||
.clearfix
|
||||
.text-center
|
||||
h1(data-i18n="teacher.enroll_students")
|
||||
h1(data-i18n="teacher.apply_licenses")
|
||||
h2(data-i18n="courses.grants_lifetime_access")
|
||||
|
||||
block modal-body-content
|
||||
|
@ -26,7 +26,7 @@ block modal-body-content
|
|||
option(selected=(!view.classroom), value='' data-i18n='teacher.all_students')
|
||||
|
||||
form.form.m-t-3
|
||||
span(data-i18n="teacher.enroll_the_following_students")
|
||||
span(data-i18n="teacher.apply_licenses_to_the_following_students")
|
||||
span :
|
||||
.well.form-group
|
||||
- var enrolledUsers = view.users.filter(function(user){ return user.isEnrolled() })
|
||||
|
@ -39,8 +39,7 @@ block modal-body-content
|
|||
span.spr= user.broadName()
|
||||
if enrolledUsers.length > 0
|
||||
.small-details.m-t-3
|
||||
span(data-i18n='TODO')
|
||||
| The following students are already enrolled:
|
||||
span(data-i18n='teacher.students_have_licenses')
|
||||
for user in enrolledUsers
|
||||
- var selected = Boolean(paid || state.get('selectedUsers').get(user.id))
|
||||
.checkbox
|
||||
|
@ -62,12 +61,11 @@ block modal-body-content
|
|||
|
||||
p
|
||||
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
|
||||
= numToEnroll
|
||||
| )
|
||||
span.spl(data-i18n="courses.students1")
|
||||
|
||||
p
|
||||
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
|
||||
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')
|
||||
h4#enrollments-blurb
|
||||
span(data-i18n='teacher.enrollments_blurb_1')
|
||||
|
@ -131,4 +131,4 @@ mixin enrollmentStats
|
|||
|
||||
.text-center
|
||||
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')
|
||||
.small-details.text-center(data-i18n='teacher.course_progress')
|
||||
.tab-spacer
|
||||
li(class=(activeTab === "#enrollment-status-tab" ? 'active' : ''))
|
||||
a.course-progress-tab-btn(href='#enrollment-status-tab')
|
||||
.small-details.text-center(data-i18n='teacher.enrollment_status')
|
||||
li(class=(activeTab === "#license-status-tab" ? 'active' : ''))
|
||||
a.course-progress-tab-btn(href='#license-status-tab')
|
||||
.small-details.text-center(data-i18n='teacher.license_status')
|
||||
.tab-filler
|
||||
|
||||
.tab-content
|
||||
|
@ -129,7 +129,7 @@ block content
|
|||
+studentsTab
|
||||
else if activeTab === '#course-progress-tab'
|
||||
+courseProgressTab
|
||||
else if activeTab === '#enrollment-status-tab'
|
||||
else if activeTab === '#license-status-tab'
|
||||
+enrollmentStatusTab
|
||||
|
||||
else
|
||||
|
@ -215,7 +215,6 @@ mixin studentRow(student)
|
|||
div
|
||||
i
|
||||
span(data-i18n='teacher.latest_completed')
|
||||
span :
|
||||
div
|
||||
+longLevelName(student.latestCompleteLevel)
|
||||
td
|
||||
|
@ -231,8 +230,6 @@ mixin studentRow(student)
|
|||
//- - var level = ???
|
||||
- var label = courseLabelsArray[index];
|
||||
+studentCourseProgressDot(progress, levelsTotal, level, label)
|
||||
unless student.isEnrolled()
|
||||
+enrollStudentButton(student)
|
||||
//- td
|
||||
//- span.view-class-arrow.glyphicon.glyphicon-chevron-right
|
||||
td
|
||||
|
@ -244,10 +241,6 @@ mixin studentRow(student)
|
|||
div.glyphicon.glyphicon-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
|
||||
#course-progress-tab.m-t-3
|
||||
if view.courses
|
||||
|
@ -267,14 +260,11 @@ mixin courseProgressTab
|
|||
each student in state.get('students').models
|
||||
if _.contains(state.get('selectedCourse').members, student.id)
|
||||
+studentLevelsRow(student)
|
||||
//- TODO: If any students aren't assigned the course
|
||||
.unassigned-students.render-on-course-sync
|
||||
if state.get('selectedCourse') && state.get('selectedCourse').members.length < state.get('students').length
|
||||
h2
|
||||
span(data-i18n='TODO')
|
||||
| Students who have not been assigned
|
||||
|
|
||||
span= state.get('selectedCourse').get('name')
|
||||
- var courseName = i18n(state.get('selectedCourse').attributes, 'name');
|
||||
span= translate('teacher.students_not_assigned').replace('{{courseName}}', courseName)
|
||||
for student in state.get('students').models
|
||||
unless _.contains(state.get('selectedCourse').members, student.id)
|
||||
.row.unassigned-student-row.alternating-background
|
||||
|
@ -285,19 +275,11 @@ mixin courseProgressTab
|
|||
.col-sm-4
|
||||
.latest-completed.truncate.small
|
||||
i.m-r-1
|
||||
span(data-i18n='TODO')
|
||||
| Latest completed
|
||||
| :
|
||||
span(data-i18n='teacher.latest_completed')
|
||||
+longLevelName(student.latestCompleteLevel)
|
||||
.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)
|
||||
span(data-i18n='TODO')
|
||||
| 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
|
||||
.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='teacher.assign_course')
|
||||
|
||||
mixin courseOverview
|
||||
- var course = state.get('selectedCourse')
|
||||
|
@ -417,8 +399,6 @@ mixin bulkAssignControls
|
|||
.bulk-assign-controls.form-inline
|
||||
.no-students-selected.small-details(class=state.get('errors').assigningToNobody ? 'visible' : '')
|
||||
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(data-i18n='teacher.bulk_assign')
|
||||
span :
|
||||
|
@ -429,8 +409,6 @@ mixin bulkAssignControls
|
|||
= i18n(course.attributes, 'name')
|
||||
button.btn.btn-primary-alt.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
|
||||
// TODO: Have search input in all tabs
|
||||
|
@ -441,7 +419,7 @@ mixin enrollmentStatusTab
|
|||
// input#student-search.form-control.m-l-1(type="search")
|
||||
// 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
|
||||
// Checkbox code works, but don't need it yet.
|
||||
//th.checkbox-col.select-all
|
||||
|
@ -473,4 +451,4 @@ mixin enrollmentStatusTab
|
|||
strong(class= status === 'expired' ? 'text-danger' : '')= view.studentStatusString(student)
|
||||
td.enroll-col
|
||||
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
|
||||
.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
|
||||
li.m-t-1(data-i18n='teacher.how_to_enroll_blurb_1')
|
||||
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_2')
|
||||
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_3')
|
||||
p(data-i18n='teacher.how_to_apply_licenses_blurb_1')
|
||||
img.m-y-3(src="/images/pages/courses/how_to_apply_licenses.png")
|
||||
h5(data-i18n='teacher.how_to_apply_licenses_blurb_2')
|
||||
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'
|
||||
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
|
||||
|
||||
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'
|
||||
EditStudentModal = require 'views/teachers/EditStudentModal'
|
||||
RemoveStudentModal = require 'views/courses/RemoveStudentModal'
|
||||
CoursesNotAssignedModal = require './CoursesNotAssignedModal'
|
||||
|
||||
Campaigns = require 'collections/Campaigns'
|
||||
Classroom = require 'models/Classroom'
|
||||
|
@ -19,6 +20,7 @@ Course = require 'models/Course'
|
|||
Courses = require 'collections/Courses'
|
||||
CourseInstance = require 'models/CourseInstance'
|
||||
CourseInstances = require 'collections/CourseInstances'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
|
||||
module.exports = class TeacherClassView extends RootView
|
||||
id: 'teacher-class-view'
|
||||
|
@ -38,7 +40,6 @@ module.exports = class TeacherClassView extends RootView
|
|||
'click .assign-student-button': 'onClickAssignStudentButton'
|
||||
'click .enroll-student-button': 'onClickEnrollStudentButton'
|
||||
'click .assign-to-selected-students': 'onClickBulkAssign'
|
||||
'click .enroll-selected-students': 'onClickBulkEnroll'
|
||||
'click .export-student-progress-btn': 'onClickExportStudentProgress'
|
||||
'click .select-all': 'onClickSelectAll'
|
||||
'click .student-checkbox': 'onClickStudentCheckbox'
|
||||
|
@ -55,7 +56,6 @@ module.exports = class TeacherClassView extends RootView
|
|||
joinURL: ""
|
||||
errors:
|
||||
assigningToNobody: false
|
||||
assigningToUnenrolled: false
|
||||
selectedCourse: undefined
|
||||
checkboxStates: {}
|
||||
classStats:
|
||||
|
@ -85,6 +85,10 @@ module.exports = class TeacherClassView extends RootView
|
|||
@onKeyPressStudentSearch = _.debounce(@onKeyPressStudentSearch, 200)
|
||||
@sortedCourses = []
|
||||
|
||||
@prepaids = new Prepaids()
|
||||
@prepaids.comparator = 'endDate' # use prepaids in order of expiration
|
||||
@supermodel.trackRequest @prepaids.fetchByCreator(me.id)
|
||||
|
||||
@students = new Users()
|
||||
@listenTo @classroom, 'sync', ->
|
||||
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')
|
||||
@listenTo @courseInstances, 'sync change update', ->
|
||||
@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', ->
|
||||
# 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?
|
||||
|
@ -173,7 +175,7 @@ module.exports = class TeacherClassView extends RootView
|
|||
@listenTo @courseInstances, 'sync change update', @debouncedRender
|
||||
@listenTo @state, 'sync change', ->
|
||||
if _.isEmpty(_.omit(@state.changed, 'searchTerm'))
|
||||
@renderSelectors('#enrollment-status-table')
|
||||
@renderSelectors('#license-status-table')
|
||||
else
|
||||
@debouncedRender()
|
||||
@listenTo @students, 'sort', @debouncedRender
|
||||
|
@ -304,12 +306,6 @@ module.exports = class TeacherClassView extends RootView
|
|||
@enrollStudents(selectedUsers)
|
||||
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) ->
|
||||
modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students }
|
||||
@openModalView(modal)
|
||||
|
@ -390,35 +386,95 @@ module.exports = class TeacherClassView extends RootView
|
|||
onClickBulkAssign: ->
|
||||
courseID = @$('.bulk-course-select').val()
|
||||
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
|
||||
@state.set errors: { assigningToNobody, assigningToUnenrolled }
|
||||
@state.set errors: { 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']
|
||||
|
||||
# TODO: Move this to the model. Use promises/callbacks?
|
||||
assignCourse: (courseID, members) ->
|
||||
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
||||
if courseInstance
|
||||
courseInstance.addMembers members
|
||||
else
|
||||
courseInstance = new CourseInstance {
|
||||
courseID,
|
||||
classroomID: @classroom.id
|
||||
ownerID: @classroom.get('ownerID')
|
||||
aceConfig: {}
|
||||
}
|
||||
@courseInstances.add(courseInstance)
|
||||
courseInstance.save {}, {
|
||||
success: ->
|
||||
courseInstance.addMembers members
|
||||
}
|
||||
null
|
||||
courseInstance = null
|
||||
numberEnrolled = 0
|
||||
remainingSpots = 0
|
||||
|
||||
return Promise.resolve()
|
||||
.then =>
|
||||
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
||||
if not courseInstance
|
||||
courseInstance = new CourseInstance {
|
||||
courseID,
|
||||
classroomID: @classroom.id
|
||||
ownerID: @classroom.get('ownerID')
|
||||
aceConfig: {}
|
||||
}
|
||||
courseInstance.notyErrors = false # handling manually
|
||||
@courseInstances.add(courseInstance)
|
||||
return courseInstance.save()
|
||||
|
||||
.then =>
|
||||
availablePrepaids = @prepaids.filter((prepaid) -> prepaid.status() is 'available')
|
||||
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) ->
|
||||
e.preventDefault()
|
||||
|
|
|
@ -7,6 +7,7 @@ Courses = require 'collections/Courses'
|
|||
Levels = require 'collections/Levels'
|
||||
LevelSessions = require 'collections/LevelSessions'
|
||||
CourseInstances = require 'collections/CourseInstances'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
|
||||
describe '/teachers/classes/:handle', ->
|
||||
|
||||
|
@ -30,13 +31,15 @@ describe 'TeacherClassView', ->
|
|||
factories.makeCourse({name: 'Beta Course', releasePhase: 'beta'}),
|
||||
])
|
||||
@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()})
|
||||
@prepaids = new Prepaids([@available1, @available2, expired])
|
||||
@students = new Users([
|
||||
factories.makeUser({name: 'Abner'})
|
||||
factories.makeUser({name: 'Abigail'})
|
||||
factories.makeUser({name: 'Abby'}, {prepaid: available})
|
||||
factories.makeUser({name: 'Ben'}, {prepaid: available})
|
||||
factories.makeUser({name: 'Abby'}, {prepaid: @available1})
|
||||
factories.makeUser({name: 'Ben'}, {prepaid: @available1})
|
||||
factories.makeUser({name: 'Ned'}, {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.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
|
||||
|
@ -94,14 +98,6 @@ describe 'TeacherClassView', ->
|
|||
# it 'sorts correctly by Progress'
|
||||
|
||||
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) ->
|
||||
expect(@view.$('.no-students-selected').hasClass('visible')).toBe(false)
|
||||
@view.$('.assign-to-selected-students').click()
|
||||
|
@ -116,9 +112,10 @@ describe 'TeacherClassView', ->
|
|||
# it 'still shows the correct Course Overview progress'
|
||||
#
|
||||
|
||||
describe 'the Enrollment Status tab', ->
|
||||
beforeEach ->
|
||||
@view.state.set('activeTab', '#enrollment-status-tab')
|
||||
describe 'the License Status tab', ->
|
||||
beforeEach (done) ->
|
||||
@view.state.set('activeTab', '#license-status-tab')
|
||||
_.defer(done)
|
||||
|
||||
describe 'Enroll button', ->
|
||||
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.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
|
||||
|
@ -207,3 +205,110 @@ describe 'TeacherClassView', ->
|
|||
return true
|
||||
@view.$('.export-student-progress-btn').click()
|
||||
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