mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-05-02 00:43:34 -04:00
Merge branch 'master' into production
This commit is contained in:
commit
1b191b1255
11 changed files with 87 additions and 23 deletions
app
collections
templates/courses
views/courses
23
app/collections/Prepaids.coffee
Normal file
23
app/collections/Prepaids.coffee
Normal file
|
@ -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
|
|
@ -39,6 +39,10 @@ block modal-body-content
|
||||||
- var paid = user.get('coursePrepaidID')
|
- var paid = user.get('coursePrepaidID')
|
||||||
input(type="checkbox", disabled=paid, checked=true, data-user-id=user.id, name='user')
|
input(type="checkbox", disabled=paid, checked=true, data-user-id=user.id, name='user')
|
||||||
span.spr= user.broadName()
|
span.spr= user.broadName()
|
||||||
|
if paid
|
||||||
|
span (
|
||||||
|
span already enrolled
|
||||||
|
span )
|
||||||
|
|
||||||
#error-alert.alert.alert-danger.hide
|
#error-alert.alert.alert-danger.hide
|
||||||
|
|
||||||
|
@ -61,7 +65,7 @@ block modal-body-content
|
||||||
| )
|
| )
|
||||||
|
|
||||||
p
|
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
|
p
|
||||||
a#get-more-licenses-btn.btn.btn-info.text-uppercase(href="/courses/purchase") Get More Licenses
|
a#get-more-licenses-btn.btn.btn-info.text-uppercase(href="/courses/purchase") Get More Licenses
|
||||||
|
|
|
@ -22,7 +22,12 @@ block content
|
||||||
- var stats = view.classStats()
|
- var stats = view.classStats()
|
||||||
tr
|
tr
|
||||||
td(data-i18n="courses.total_students")
|
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
|
tr
|
||||||
td(data-i18n="courses.average_time")
|
td(data-i18n="courses.average_time")
|
||||||
td= stats.averagePlaytime
|
td= stats.averagePlaytime
|
||||||
|
@ -40,9 +45,9 @@ block content
|
||||||
| Students
|
| Students
|
||||||
if view.teacherMode
|
if view.teacherMode
|
||||||
.pull-right#main-button-area
|
.pull-right#main-button-area
|
||||||
button#add-students-btn.btn.btn-success Add Students
|
button#add-students-btn.btn.btn-primary.text-uppercase Add Students
|
||||||
button#activate-licenses-btn.btn.btn-warning Activate Licenses
|
button#activate-licenses-btn.btn.btn-info.text-uppercase Enroll Students in Paid Courses
|
||||||
a.btn.btn-warning(href="/courses/purchase?from-classroom="+view.classroom.id) Purchase Licenses
|
a.btn.btn-success.text-uppercase(href="/courses/purchase?from-classroom="+view.classroom.id) Purchase Enrollments
|
||||||
|
|
||||||
hr
|
hr
|
||||||
|
|
||||||
|
@ -73,7 +78,7 @@ block content
|
||||||
if !(course.get('free') || paidFor)
|
if !(course.get('free') || paidFor)
|
||||||
- continue;
|
- continue;
|
||||||
.row
|
.row
|
||||||
.col-sm-3.text-right= campaign.get('fullName')
|
.col-sm-3.text-right= course.get('name')
|
||||||
.col-sm-9
|
.col-sm-9
|
||||||
if inCourse
|
if inCourse
|
||||||
- var levels = campaign.get('levels');
|
- var levels = campaign.get('levels');
|
||||||
|
@ -97,14 +102,14 @@ block content
|
||||||
else
|
else
|
||||||
.progress-bar.progress-bar-default(style=css, data-content=content, data-toggle='popover')= i
|
.progress-bar.progress-bar-default(style=css, data-content=content, data-toggle='popover')= i
|
||||||
else if paidFor
|
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
|
if view.teacherMode && !paidFor
|
||||||
.text-center
|
.text-center
|
||||||
p
|
p
|
||||||
em Activate a license to enable more courses for this student.
|
em Enroll this student to assign paid courses
|
||||||
p
|
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
|
hr
|
||||||
|
|
|
@ -25,7 +25,7 @@ block content
|
||||||
.alert.alert-danger= view.stateMessage
|
.alert.alert-danger= view.stateMessage
|
||||||
|
|
||||||
p.text-center
|
p.text-center
|
||||||
strong How many enrollments do you need?
|
strong How many additional paid enrollments do you need?
|
||||||
br
|
br
|
||||||
|
|
||||||
p.text-center
|
p.text-center
|
||||||
|
@ -38,7 +38,9 @@ block content
|
||||||
|
|
||||||
.container-fluid
|
.container-fluid
|
||||||
.row
|
.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
|
br
|
||||||
|
|
||||||
p.text-center#price-form-group
|
p.text-center#price-form-group
|
||||||
|
|
|
@ -54,6 +54,17 @@ block content
|
||||||
|
|
||||||
br
|
br
|
||||||
.section-header Available Courses
|
.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
|
.container-fluid
|
||||||
- var courses = view.courses.models;
|
- var courses = view.courses.models;
|
||||||
- var i = 0;
|
- var i = 0;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ModalView = require 'views/core/ModalView'
|
ModalView = require 'views/core/ModalView'
|
||||||
template = require 'templates/courses/activate-licenses-modal'
|
template = require 'templates/courses/activate-licenses-modal'
|
||||||
CocoCollection = require 'collections/CocoCollection'
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
Prepaid = require 'models/Prepaid'
|
Prepaids = require 'collections/Prepaids'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
|
|
||||||
module.exports = class ActivateLicensesModal extends ModalView
|
module.exports = class ActivateLicensesModal extends ModalView
|
||||||
|
@ -16,12 +16,10 @@ module.exports = class ActivateLicensesModal extends ModalView
|
||||||
@classroom = options.classroom
|
@classroom = options.classroom
|
||||||
@users = options.users
|
@users = options.users
|
||||||
@user = options.user
|
@user = options.user
|
||||||
@prepaids = new CocoCollection([], { url: "/db/prepaid", model: Prepaid })
|
@prepaids = new Prepaids()
|
||||||
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.comparator = '_id'
|
@prepaids.comparator = '_id'
|
||||||
@supermodel.loadCollection(@prepaids, 'prepaids', {data: {creator: me.id}})
|
@prepaids.fetchByCreator(me.id)
|
||||||
|
@supermodel.loadCollection(@prepaids, 'prepaids')
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
super()
|
super()
|
||||||
|
@ -39,7 +37,7 @@ module.exports = class ActivateLicensesModal extends ModalView
|
||||||
depleted = remaining < 0
|
depleted = remaining < 0
|
||||||
@$('#not-depleted-span').toggleClass('hide', depleted)
|
@$('#not-depleted-span').toggleClass('hide', depleted)
|
||||||
@$('#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: ->
|
showProgress: ->
|
||||||
@$('#submit-form-area').addClass('hide')
|
@$('#submit-form-area').addClass('hide')
|
||||||
|
|
|
@ -11,6 +11,10 @@ module.exports = class AddLevelSystemModal extends ModalView
|
||||||
|
|
||||||
initialize: (options) ->
|
initialize: (options) ->
|
||||||
@classroom = options.classroom
|
@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: ->
|
onClickSaveSettingsButton: ->
|
||||||
name = $('.settings-name-input').val()
|
name = $('.settings-name-input').val()
|
||||||
|
|
|
@ -99,6 +99,7 @@ module.exports = class ClassroomView extends RootView
|
||||||
})
|
})
|
||||||
@openModalView(modal)
|
@openModalView(modal)
|
||||||
modal.once 'redeem-users', -> document.location.reload()
|
modal.once 'redeem-users', -> document.location.reload()
|
||||||
|
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
||||||
|
|
||||||
onClickActivateSingleLicenseButton: (e) ->
|
onClickActivateSingleLicenseButton: (e) ->
|
||||||
userID = $(e.target).data('user-id')
|
userID = $(e.target).data('user-id')
|
||||||
|
@ -110,6 +111,7 @@ module.exports = class ClassroomView extends RootView
|
||||||
})
|
})
|
||||||
@openModalView(modal)
|
@openModalView(modal)
|
||||||
modal.once 'redeem-users', -> document.location.reload()
|
modal.once 'redeem-users', -> document.location.reload()
|
||||||
|
application.tracker?.trackEvent 'Classroom started enroll student', category: 'Courses', userID: userID
|
||||||
|
|
||||||
onClickEditClassDetailsLink: ->
|
onClickEditClassDetailsLink: ->
|
||||||
modal = new ClassroomSettingsModal({classroom: @classroom})
|
modal = new ClassroomSettingsModal({classroom: @classroom})
|
||||||
|
@ -144,16 +146,21 @@ module.exports = class ClassroomView extends RootView
|
||||||
completeSessions = @sessions.filter (s) -> s.get('state')?.complete
|
completeSessions = @sessions.filter (s) -> s.get('state')?.complete
|
||||||
stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A'
|
stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A'
|
||||||
stats.totalLevelsComplete = _.size(completeSessions)
|
stats.totalLevelsComplete = _.size(completeSessions)
|
||||||
|
|
||||||
|
enrolledUsers = @users.filter (user) -> user.get('coursePrepaidID')
|
||||||
|
stats.enrolledUsers = _.size(enrolledUsers)
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
onClickAddStudentsButton: (e) ->
|
onClickAddStudentsButton: (e) ->
|
||||||
modal = new InviteToClassroomModal({classroom: @classroom})
|
modal = new InviteToClassroomModal({classroom: @classroom})
|
||||||
@openModalView(modal)
|
@openModalView(modal)
|
||||||
|
application.tracker?.trackEvent 'Classroom started add students', category: 'Courses', classroomID: @classroom.id
|
||||||
|
|
||||||
onClickEnableButton: (e) ->
|
onClickEnableButton: (e) ->
|
||||||
courseInstance = @courseInstances.get($(e.target).data('course-instance-cid'))
|
courseInstance = @courseInstances.get($(e.target).data('course-instance-cid'))
|
||||||
userID = $(e.target).data('user-id')
|
userID = $(e.target).data('user-id')
|
||||||
$(e.target).attr('disabled', true)
|
$(e.target).attr('disabled', true)
|
||||||
|
application.tracker?.trackEvent 'Course assign student', category: 'Courses', courseInstanceID: courseInstance.id, userID: userID
|
||||||
|
|
||||||
onCourseInstanceCreated = =>
|
onCourseInstanceCreated = =>
|
||||||
courseInstance.addMember(userID)
|
courseInstance.addMember(userID)
|
||||||
|
@ -179,6 +186,7 @@ module.exports = class ClassroomView extends RootView
|
||||||
onStudentRemoved: (e) ->
|
onStudentRemoved: (e) ->
|
||||||
@users.remove(e.user)
|
@users.remove(e.user)
|
||||||
@render()
|
@render()
|
||||||
|
application.tracker?.trackEvent 'Classroom removed student', category: 'Courses', courseInstanceID: @courseInstance.id, userID: e.user.id
|
||||||
|
|
||||||
levelPopoverContent: (level, session, i) ->
|
levelPopoverContent: (level, session, i) ->
|
||||||
return null unless level
|
return null unless level
|
||||||
|
|
|
@ -199,8 +199,8 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
@loadAllCourses()
|
@loadAllCourses()
|
||||||
|
|
||||||
onClickPlayLevel: (e) ->
|
onClickPlayLevel: (e) ->
|
||||||
levelSlug = $(e.target).data('level-slug')
|
levelSlug = $(e.target).closest('.btn-play-level').data('level-slug')
|
||||||
levelID = $(e.target).data('level-id')
|
levelID = $(e.target).closest('.btn-play-level').data('level-id')
|
||||||
level = @campaign.get('levels')[levelID]
|
level = @campaign.get('levels')[levelID]
|
||||||
if level.type is 'course-ladder'
|
if level.type is 'course-ladder'
|
||||||
viewClass = 'views/ladder/LadderView'
|
viewClass = 'views/ladder/LadderView'
|
||||||
|
|
|
@ -4,7 +4,7 @@ template = require 'templates/courses/invite-to-classroom-modal'
|
||||||
module.exports = class InviteToClassroomModal extends ModalView
|
module.exports = class InviteToClassroomModal extends ModalView
|
||||||
id: 'invite-to-classroom-modal'
|
id: 'invite-to-classroom-modal'
|
||||||
template: template
|
template: template
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click #send-invites-btn': 'onClickSendInvitesButton'
|
'click #send-invites-btn': 'onClickSendInvitesButton'
|
||||||
'click #copy-url-btn, #join-url-input': 'copyURL'
|
'click #copy-url-btn, #join-url-input': 'copyURL'
|
||||||
|
@ -23,6 +23,7 @@ module.exports = class InviteToClassroomModal extends ModalView
|
||||||
url = @classroom.url() + '/invite-members'
|
url = @classroom.url() + '/invite-members'
|
||||||
@$('#send-invites-btn, #invite-emails-textarea').addClass('hide')
|
@$('#send-invites-btn, #invite-emails-textarea').addClass('hide')
|
||||||
@$('#invite-emails-sending-alert').removeClass('hide')
|
@$('#invite-emails-sending-alert').removeClass('hide')
|
||||||
|
application.tracker?.trackEvent 'Classroom invite via email', category: 'Courses', classroomID: @classroom.id, emails: emails
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url
|
url: url
|
||||||
|
@ -39,6 +40,7 @@ module.exports = class InviteToClassroomModal extends ModalView
|
||||||
try
|
try
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
@$('#copied-alert').removeClass('hide')
|
@$('#copied-alert').removeClass('hide')
|
||||||
|
application.tracker?.trackEvent 'Classroom copy URL', category: 'Courses', classroomID: @classroom.id, url: @joinURL
|
||||||
catch err
|
catch err
|
||||||
console.log('Oops, unable to copy', err)
|
console.log('Oops, unable to copy', err)
|
||||||
@$('#copy-failed-alert').removeClass('hide')
|
@$('#copy-failed-alert').removeClass('hide')
|
||||||
|
|
|
@ -10,6 +10,7 @@ CourseInstance = require 'models/CourseInstance'
|
||||||
RootView = require 'views/core/RootView'
|
RootView = require 'views/core/RootView'
|
||||||
template = require 'templates/courses/teacher-courses-view'
|
template = require 'templates/courses/teacher-courses-view'
|
||||||
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
||||||
|
Prepaids = require 'collections/Prepaids'
|
||||||
|
|
||||||
module.exports = class TeacherCoursesView extends RootView
|
module.exports = class TeacherCoursesView extends RootView
|
||||||
id: 'teacher-courses-view'
|
id: 'teacher-courses-view'
|
||||||
|
@ -32,6 +33,11 @@ module.exports = class TeacherCoursesView extends RootView
|
||||||
@courseInstances.comparator = 'courseID'
|
@courseInstances.comparator = 'courseID'
|
||||||
@courseInstances.sliceWithMembers = -> return @filter (courseInstance) -> _.size(courseInstance.get('members')) and courseInstance.get('classroomID')
|
@courseInstances.sliceWithMembers = -> return @filter (courseInstance) -> _.size(courseInstance.get('members')) and courseInstance.get('classroomID')
|
||||||
@supermodel.loadCollection(@courseInstances, 'course_instances', {data: {ownerID: me.id}})
|
@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 })
|
@members = new CocoCollection([], { model: User })
|
||||||
@listenTo @members, 'sync', @render
|
@listenTo @members, 'sync', @render
|
||||||
@
|
@
|
||||||
|
@ -51,6 +57,7 @@ module.exports = class TeacherCoursesView extends RootView
|
||||||
return
|
return
|
||||||
modal = new InviteToClassroomModal({classroom: classroom})
|
modal = new InviteToClassroomModal({classroom: classroom})
|
||||||
@openModalView(modal)
|
@openModalView(modal)
|
||||||
|
application.tracker?.trackEvent 'Classroom started add students', category: 'Courses', classroomID: classroom.id
|
||||||
|
|
||||||
onClickCreateNewClassButton: ->
|
onClickCreateNewClassButton: ->
|
||||||
return @openModalView new AuthModal() if me.get('anonymous')
|
return @openModalView new AuthModal() if me.get('anonymous')
|
||||||
|
@ -70,7 +77,7 @@ module.exports = class TeacherCoursesView extends RootView
|
||||||
modal = new ClassroomSettingsModal({classroom: classroom})
|
modal = new ClassroomSettingsModal({classroom: classroom})
|
||||||
@openModalView(modal)
|
@openModalView(modal)
|
||||||
@listenToOnce modal, 'hide', @render
|
@listenToOnce modal, 'hide', @render
|
||||||
|
|
||||||
onLoaded: ->
|
onLoaded: ->
|
||||||
super()
|
super()
|
||||||
@addFreeCourseInstances()
|
@addFreeCourseInstances()
|
||||||
|
@ -92,4 +99,4 @@ module.exports = class TeacherCoursesView extends RootView
|
||||||
courseInstance.save(null, {validate: false})
|
courseInstance.save(null, {validate: false})
|
||||||
@courseInstances.add(courseInstance)
|
@courseInstances.add(courseInstance)
|
||||||
@listenToOnce courseInstance, 'sync', @addFreeCourseInstances
|
@listenToOnce courseInstance, 'sync', @addFreeCourseInstances
|
||||||
return
|
return
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue