diff --git a/app/collections/Prepaids.coffee b/app/collections/Prepaids.coffee new file mode 100644 index 000000000..1ac346304 --- /dev/null +++ b/app/collections/Prepaids.coffee @@ -0,0 +1,23 @@ +CocoCollection = require 'collections/CocoCollection' +Prepaid = require 'models/Prepaid' + +sum = (numbers) -> _.reduce(numbers, (a, b) -> a + b) + +module.exports = class Prepaids extends CocoCollection + model: Prepaid + + url: "/db/prepaid" + + totalMaxRedeemers: -> + sum((prepaid.get('maxRedeemers') for prepaid in @models)) or 0 + + totalRedeemers: -> + sum((_.size(prepaid.get('redeemers')) for prepaid in @models)) or 0 + + totalAvailable: -> Math.max(@totalMaxRedeemers() - @totalRedeemers(), 0) + + fetchByCreator: (creatorID, opts) -> + opts ?= {} + opts.data ?= {} + opts.data.creator = creatorID + @fetch opts \ No newline at end of file diff --git a/app/templates/courses/activate-licenses-modal.jade b/app/templates/courses/activate-licenses-modal.jade index af4fe823b..1d8ed445f 100644 --- a/app/templates/courses/activate-licenses-modal.jade +++ b/app/templates/courses/activate-licenses-modal.jade @@ -39,6 +39,10 @@ block modal-body-content - var paid = user.get('coursePrepaidID') input(type="checkbox", disabled=paid, checked=true, data-user-id=user.id, name='user') span.spr= user.broadName() + if paid + span ( + span already enrolled + span ) #error-alert.alert.alert-danger.hide @@ -61,7 +65,7 @@ block modal-body-content | ) p - button#activate-licenses-btn.btn.btn-success.text-uppercase(type="submit") Activate Licenses + button#activate-licenses-btn.btn.btn-success.text-uppercase(type="submit") Enroll Students p a#get-more-licenses-btn.btn.btn-info.text-uppercase(href="/courses/purchase") Get More Licenses diff --git a/app/templates/courses/classroom-view.jade b/app/templates/courses/classroom-view.jade index e088e7815..15f9f1e18 100644 --- a/app/templates/courses/classroom-view.jade +++ b/app/templates/courses/classroom-view.jade @@ -22,7 +22,12 @@ block content - var stats = view.classStats() tr td(data-i18n="courses.total_students") - td= _.size(view.classroom.get('members')) + td + span.spr= _.size(view.classroom.get('members')) + span ( + span.spr enrolled in paid courses: + span= stats.enrolledUsers + span ) tr td(data-i18n="courses.average_time") td= stats.averagePlaytime @@ -40,9 +45,9 @@ block content | Students if view.teacherMode .pull-right#main-button-area - button#add-students-btn.btn.btn-success Add Students - button#activate-licenses-btn.btn.btn-warning Activate Licenses - a.btn.btn-warning(href="/courses/purchase?from-classroom="+view.classroom.id) Purchase Licenses + button#add-students-btn.btn.btn-primary.text-uppercase Add Students + button#activate-licenses-btn.btn.btn-info.text-uppercase Enroll Students in Paid Courses + a.btn.btn-success.text-uppercase(href="/courses/purchase?from-classroom="+view.classroom.id) Purchase Enrollments hr @@ -73,7 +78,7 @@ block content if !(course.get('free') || paidFor) - continue; .row - .col-sm-3.text-right= campaign.get('fullName') + .col-sm-3.text-right= course.get('name') .col-sm-9 if inCourse - var levels = campaign.get('levels'); @@ -97,14 +102,14 @@ block content else .progress-bar.progress-bar-default(style=css, data-content=content, data-toggle='popover')= i else if paidFor - button.enable-btn.btn.btn-info.btn-sm(data-user-id=user.id, data-course-instance-cid=courseInstance.cid) Enable + button.enable-btn.btn.btn-info.btn-sm.text-uppercase(data-user-id=user.id, data-course-instance-cid=courseInstance.cid) Assign if view.teacherMode && !paidFor .text-center p - em Activate a license to enable more courses for this student. + em Enroll this student to assign paid courses p - button.activate-single-license-btn.btn.btn-info.btn-sm(data-user-id=user.id) Activate + button.activate-single-license-btn.btn.btn-info.btn-sm.text-uppercase(data-user-id=user.id) Enroll hr diff --git a/app/templates/courses/purchase-courses-view.jade b/app/templates/courses/purchase-courses-view.jade index ef9347fc5..9d057862c 100644 --- a/app/templates/courses/purchase-courses-view.jade +++ b/app/templates/courses/purchase-courses-view.jade @@ -25,7 +25,7 @@ block content .alert.alert-danger= view.stateMessage p.text-center - strong How many enrollments do you need? + strong How many additional paid enrollments do you need? br p.text-center @@ -38,7 +38,9 @@ block content .container-fluid .row - .col-md-offset-3.col-md-6 One enrollment per student is required to assign them to paid CodeCombat courses. A single student does not need multiple enrollments to access all paid courses. + .col-md-offset-3.col-md-6 + | Each student in a class will get access to Course 2 and up once they are enrolled in paid courses. + | You may assign each course to each student individually. br p.text-center#price-form-group diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index 0f57c3a05..63a1201f4 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -54,6 +54,17 @@ block content br .section-header Available Courses + + p.text-center + strong.spr Unused enrollments available: + strong.spr= view.prepaids.totalAvailable() + a.btn.btn-success.btn(href="/courses/purchase") Purchase Enrollments + + p + | All students get access to Introduction to Computer Science for free. + | One enrollment per student is required to assign them to paid CodeCombat courses. + | A single student does not need multiple enrollments to access all paid courses. + .container-fluid - var courses = view.courses.models; - var i = 0; diff --git a/app/views/courses/ActivateLicensesModal.coffee b/app/views/courses/ActivateLicensesModal.coffee index 529734a6e..f97f19876 100644 --- a/app/views/courses/ActivateLicensesModal.coffee +++ b/app/views/courses/ActivateLicensesModal.coffee @@ -1,7 +1,7 @@ ModalView = require 'views/core/ModalView' template = require 'templates/courses/activate-licenses-modal' CocoCollection = require 'collections/CocoCollection' -Prepaid = require 'models/Prepaid' +Prepaids = require 'collections/Prepaids' User = require 'models/User' module.exports = class ActivateLicensesModal extends ModalView @@ -16,12 +16,10 @@ module.exports = class ActivateLicensesModal extends ModalView @classroom = options.classroom @users = options.users @user = options.user - @prepaids = new CocoCollection([], { url: "/db/prepaid", model: Prepaid }) - sum = (numbers) -> _.reduce(numbers, (a, b) -> a + b) - @prepaids.totalMaxRedeemers = -> sum((prepaid.get('maxRedeemers') for prepaid in @models)) or 0 - @prepaids.totalRedeemers = -> sum((_.size(prepaid.get('redeemers')) for prepaid in @models)) or 0 + @prepaids = new Prepaids() @prepaids.comparator = '_id' - @supermodel.loadCollection(@prepaids, 'prepaids', {data: {creator: me.id}}) + @prepaids.fetchByCreator(me.id) + @supermodel.loadCollection(@prepaids, 'prepaids') afterRender: -> super() @@ -39,7 +37,7 @@ module.exports = class ActivateLicensesModal extends ModalView depleted = remaining < 0 @$('#not-depleted-span').toggleClass('hide', depleted) @$('#depleted-span').toggleClass('hide', !depleted) - @$('#activate-licenses-btn').toggleClass('disabled', depleted) + @$('#activate-licenses-btn').toggleClass('disabled', depleted).toggleClass('btn-success', not depleted).toggleClass('btn-default', depleted) showProgress: -> @$('#submit-form-area').addClass('hide') diff --git a/app/views/courses/ClassroomSettingsModal.coffee b/app/views/courses/ClassroomSettingsModal.coffee index e9b210e61..96970fb05 100644 --- a/app/views/courses/ClassroomSettingsModal.coffee +++ b/app/views/courses/ClassroomSettingsModal.coffee @@ -11,6 +11,10 @@ module.exports = class AddLevelSystemModal extends ModalView initialize: (options) -> @classroom = options.classroom + if @classroom + application.tracker?.trackEvent 'Classroom started edit settings', category: 'Courses', classroomID: @classroom.id + else + application.tracker?.trackEvent 'Create new class', category: 'Courses' onClickSaveSettingsButton: -> name = $('.settings-name-input').val() diff --git a/app/views/courses/ClassroomView.coffee b/app/views/courses/ClassroomView.coffee index 011bcfac5..6a5c463d7 100644 --- a/app/views/courses/ClassroomView.coffee +++ b/app/views/courses/ClassroomView.coffee @@ -99,6 +99,7 @@ module.exports = class ClassroomView extends RootView }) @openModalView(modal) modal.once 'redeem-users', -> document.location.reload() + application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses' onClickActivateSingleLicenseButton: (e) -> userID = $(e.target).data('user-id') @@ -110,6 +111,7 @@ module.exports = class ClassroomView extends RootView }) @openModalView(modal) modal.once 'redeem-users', -> document.location.reload() + application.tracker?.trackEvent 'Classroom started enroll student', category: 'Courses', userID: userID onClickEditClassDetailsLink: -> modal = new ClassroomSettingsModal({classroom: @classroom}) @@ -144,16 +146,21 @@ module.exports = class ClassroomView extends RootView completeSessions = @sessions.filter (s) -> s.get('state')?.complete stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A' stats.totalLevelsComplete = _.size(completeSessions) + + enrolledUsers = @users.filter (user) -> user.get('coursePrepaidID') + stats.enrolledUsers = _.size(enrolledUsers) return stats onClickAddStudentsButton: (e) -> modal = new InviteToClassroomModal({classroom: @classroom}) @openModalView(modal) + application.tracker?.trackEvent 'Classroom started add students', category: 'Courses', classroomID: @classroom.id onClickEnableButton: (e) -> courseInstance = @courseInstances.get($(e.target).data('course-instance-cid')) userID = $(e.target).data('user-id') $(e.target).attr('disabled', true) + application.tracker?.trackEvent 'Course assign student', category: 'Courses', courseInstanceID: courseInstance.id, userID: userID onCourseInstanceCreated = => courseInstance.addMember(userID) @@ -179,6 +186,7 @@ module.exports = class ClassroomView extends RootView onStudentRemoved: (e) -> @users.remove(e.user) @render() + application.tracker?.trackEvent 'Classroom removed student', category: 'Courses', courseInstanceID: @courseInstance.id, userID: e.user.id levelPopoverContent: (level, session, i) -> return null unless level diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index 06fc6aaf4..cb119fd71 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -199,8 +199,8 @@ module.exports = class CourseDetailsView extends RootView @loadAllCourses() onClickPlayLevel: (e) -> - levelSlug = $(e.target).data('level-slug') - levelID = $(e.target).data('level-id') + levelSlug = $(e.target).closest('.btn-play-level').data('level-slug') + levelID = $(e.target).closest('.btn-play-level').data('level-id') level = @campaign.get('levels')[levelID] if level.type is 'course-ladder' viewClass = 'views/ladder/LadderView' diff --git a/app/views/courses/InviteToClassroomModal.coffee b/app/views/courses/InviteToClassroomModal.coffee index 9062665d9..e9db74611 100644 --- a/app/views/courses/InviteToClassroomModal.coffee +++ b/app/views/courses/InviteToClassroomModal.coffee @@ -4,7 +4,7 @@ template = require 'templates/courses/invite-to-classroom-modal' module.exports = class InviteToClassroomModal extends ModalView id: 'invite-to-classroom-modal' template: template - + events: 'click #send-invites-btn': 'onClickSendInvitesButton' 'click #copy-url-btn, #join-url-input': 'copyURL' @@ -23,6 +23,7 @@ module.exports = class InviteToClassroomModal extends ModalView url = @classroom.url() + '/invite-members' @$('#send-invites-btn, #invite-emails-textarea').addClass('hide') @$('#invite-emails-sending-alert').removeClass('hide') + application.tracker?.trackEvent 'Classroom invite via email', category: 'Courses', classroomID: @classroom.id, emails: emails $.ajax({ url: url @@ -39,6 +40,7 @@ module.exports = class InviteToClassroomModal extends ModalView try document.execCommand('copy') @$('#copied-alert').removeClass('hide') + application.tracker?.trackEvent 'Classroom copy URL', category: 'Courses', classroomID: @classroom.id, url: @joinURL catch err console.log('Oops, unable to copy', err) @$('#copy-failed-alert').removeClass('hide') diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index 81286c1e3..3913ba3d4 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -10,6 +10,7 @@ CourseInstance = require 'models/CourseInstance' RootView = require 'views/core/RootView' template = require 'templates/courses/teacher-courses-view' ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal' +Prepaids = require 'collections/Prepaids' module.exports = class TeacherCoursesView extends RootView id: 'teacher-courses-view' @@ -32,6 +33,11 @@ module.exports = class TeacherCoursesView extends RootView @courseInstances.comparator = 'courseID' @courseInstances.sliceWithMembers = -> return @filter (courseInstance) -> _.size(courseInstance.get('members')) and courseInstance.get('classroomID') @supermodel.loadCollection(@courseInstances, 'course_instances', {data: {ownerID: me.id}}) + @prepaids = new Prepaids() + @prepaids.comparator = '_id' + if not me.isAnonymous() + @prepaids.fetchByCreator(me.id) + @supermodel.loadCollection(@prepaids, 'prepaids') # just registers @members = new CocoCollection([], { model: User }) @listenTo @members, 'sync', @render @ @@ -51,6 +57,7 @@ module.exports = class TeacherCoursesView extends RootView return modal = new InviteToClassroomModal({classroom: classroom}) @openModalView(modal) + application.tracker?.trackEvent 'Classroom started add students', category: 'Courses', classroomID: classroom.id onClickCreateNewClassButton: -> return @openModalView new AuthModal() if me.get('anonymous') @@ -70,7 +77,7 @@ module.exports = class TeacherCoursesView extends RootView modal = new ClassroomSettingsModal({classroom: classroom}) @openModalView(modal) @listenToOnce modal, 'hide', @render - + onLoaded: -> super() @addFreeCourseInstances() @@ -92,4 +99,4 @@ module.exports = class TeacherCoursesView extends RootView courseInstance.save(null, {validate: false}) @courseInstances.add(courseInstance) @listenToOnce courseInstance, 'sync', @addFreeCourseInstances - return \ No newline at end of file + return