Merge branch 'master' into production
Before Width: | Height: | Size: 710 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 349 KiB After Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 509 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 95 KiB |
|
@ -70,7 +70,8 @@ module.exports = class CocoRouter extends Backbone.Router
|
||||||
'courses/teachers': go('courses/TeacherCoursesView')
|
'courses/teachers': go('courses/TeacherCoursesView')
|
||||||
'courses/purchase': go('courses/PurchaseCoursesView')
|
'courses/purchase': go('courses/PurchaseCoursesView')
|
||||||
'courses/enroll(/:courseID)': go('courses/CourseEnrollView')
|
'courses/enroll(/:courseID)': go('courses/CourseEnrollView')
|
||||||
'courses/:courseID(/:courseInstanceID)': go('courses/CourseDetailsView')
|
'courses/:classroomID': go('courses/ClassroomView')
|
||||||
|
'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView')
|
||||||
|
|
||||||
'db/*path': 'routeToServer'
|
'db/*path': 'routeToServer'
|
||||||
'demo(/*subpath)': go('DemoView')
|
'demo(/*subpath)': go('DemoView')
|
||||||
|
|
|
@ -886,6 +886,7 @@
|
||||||
courses:
|
courses:
|
||||||
course: "Course"
|
course: "Course"
|
||||||
courses: "courses"
|
courses: "courses"
|
||||||
|
create_new_class: "Create New Class"
|
||||||
not_enrolled: "You are not enrolled in this course."
|
not_enrolled: "You are not enrolled in this course."
|
||||||
visit_pref: "Please visit the"
|
visit_pref: "Please visit the"
|
||||||
visit_suf: "page to enroll."
|
visit_suf: "page to enroll."
|
||||||
|
@ -1363,6 +1364,15 @@
|
||||||
campaigns: "Campaigns"
|
campaigns: "Campaigns"
|
||||||
poll: "Poll"
|
poll: "Poll"
|
||||||
user_polls_record: "Poll Voting History"
|
user_polls_record: "Poll Voting History"
|
||||||
|
course: "Course"
|
||||||
|
courses: "Courses"
|
||||||
|
course_instance: "Course Instance"
|
||||||
|
courses_instances: "Course Instances"
|
||||||
|
classroom: "Classroom"
|
||||||
|
classrooms: "Classrooms"
|
||||||
|
clan: "Clan"
|
||||||
|
clans: "Clans"
|
||||||
|
members: "Members"
|
||||||
|
|
||||||
concepts:
|
concepts:
|
||||||
advanced_strings: "Advanced Strings"
|
advanced_strings: "Advanced Strings"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
CocoModel = require './CocoModel'
|
CocoModel = require './CocoModel'
|
||||||
schema = require 'schemas/models/campaign.schema'
|
schema = require 'schemas/models/campaign.schema'
|
||||||
|
Level = require 'models/Level'
|
||||||
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
|
|
||||||
module.exports = class Campaign extends CocoModel
|
module.exports = class Campaign extends CocoModel
|
||||||
@className: 'Campaign'
|
@className: 'Campaign'
|
||||||
|
@ -8,3 +10,29 @@ module.exports = class Campaign extends CocoModel
|
||||||
saveBackups: true
|
saveBackups: true
|
||||||
@denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position', 'rewards']))
|
@denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position', 'rewards']))
|
||||||
@denormalizedCampaignProperties: ['name', 'i18n', 'slug']
|
@denormalizedCampaignProperties: ['name', 'i18n', 'slug']
|
||||||
|
|
||||||
|
statsForSessions: (sessions) ->
|
||||||
|
# common code for crunching stats for a user's progress on a campaign/course
|
||||||
|
return null unless sessions
|
||||||
|
stats = {}
|
||||||
|
sessions = sessions.models or sessions
|
||||||
|
sessions = _.sortBy sessions, (s) -> s.get('changed')
|
||||||
|
levels = _.values(@get('levels'))
|
||||||
|
levels = (level for level in levels when not _.contains(level.type, 'ladder'))
|
||||||
|
levelOriginals = _.pluck(levels, 'original')
|
||||||
|
sessionOriginals = (session.get('level').original for session in sessions when session.get('state').complete)
|
||||||
|
levelsLeft = _.size(_.difference(levelOriginals, sessionOriginals))
|
||||||
|
lastSession = _.last(sessions)
|
||||||
|
stats.levels = {
|
||||||
|
size: _.size(levels)
|
||||||
|
left: levelsLeft
|
||||||
|
done: levelsLeft is 0
|
||||||
|
numDone: _.size(levels) - levelsLeft
|
||||||
|
pctDone: (100 * (_.size(levels) - levelsLeft) / _.size(levels)).toFixed(1) + '%'
|
||||||
|
lastPlayed: if lastSession then _.findWhere levels, { original: lastSession.get('level').original } else null
|
||||||
|
first: _.first(levels)
|
||||||
|
arena: _.find _.values(@get('levels')), (level) -> _.contains(level.type, 'ladder')
|
||||||
|
}
|
||||||
|
sum = (nums) -> _.reduce(nums, (s, num) -> s + num) or 0
|
||||||
|
stats.playtime = sum((session.get('playtime') or 0 for session in sessions))
|
||||||
|
return stats
|
|
@ -5,3 +5,21 @@ module.exports = class Classroom extends CocoModel
|
||||||
@className: 'Classroom'
|
@className: 'Classroom'
|
||||||
@schema: schema
|
@schema: schema
|
||||||
urlRoot: '/db/classroom'
|
urlRoot: '/db/classroom'
|
||||||
|
|
||||||
|
joinWithCode: (code, opts) ->
|
||||||
|
options = {
|
||||||
|
url: _.result(@, 'url') + '/~/members'
|
||||||
|
type: 'POST'
|
||||||
|
data: { code: code }
|
||||||
|
}
|
||||||
|
_.extend options, opts
|
||||||
|
@fetch(options)
|
||||||
|
|
||||||
|
removeMember: (userID, opts) ->
|
||||||
|
options = {
|
||||||
|
url: _.result(@, 'url') + '/members'
|
||||||
|
type: 'DELETE'
|
||||||
|
data: { userID: userID }
|
||||||
|
}
|
||||||
|
_.extend options, opts
|
||||||
|
@fetch(options)
|
|
@ -5,3 +5,32 @@ module.exports = class CourseInstance extends CocoModel
|
||||||
@className: 'CourseInstance'
|
@className: 'CourseInstance'
|
||||||
@schema: schema
|
@schema: schema
|
||||||
urlRoot: '/db/course_instance'
|
urlRoot: '/db/course_instance'
|
||||||
|
|
||||||
|
upsertForHOC: (opts) ->
|
||||||
|
options = {
|
||||||
|
url: _.result(@, 'url') + '/~/create-for-hoc'
|
||||||
|
type: 'POST'
|
||||||
|
}
|
||||||
|
_.extend options, opts
|
||||||
|
@fetch(options)
|
||||||
|
|
||||||
|
addMember: (userID, opts) ->
|
||||||
|
options = {
|
||||||
|
method: 'POST'
|
||||||
|
url: _.result(@, 'url') + '/members'
|
||||||
|
data: { userID: userID }
|
||||||
|
}
|
||||||
|
_.extend options, opts
|
||||||
|
@fetch(options)
|
||||||
|
|
||||||
|
removeMember: (userID, opts) ->
|
||||||
|
options = {
|
||||||
|
url: _.result(@, 'url') + '/members'
|
||||||
|
type: 'DELETE'
|
||||||
|
data: { userID: userID }
|
||||||
|
}
|
||||||
|
_.extend options, opts
|
||||||
|
@fetch(options)
|
||||||
|
|
||||||
|
firstLevelURL: ->
|
||||||
|
"/play/level/dungeons-of-kithgard?course=#{@get('courseID')}&course-instance=#{@id}"
|
||||||
|
|
|
@ -16,6 +16,14 @@ module.exports = class User extends CocoModel
|
||||||
isInGodMode: -> 'godmode' in @get('permissions', true)
|
isInGodMode: -> 'godmode' in @get('permissions', true)
|
||||||
isAnonymous: -> @get('anonymous', true)
|
isAnonymous: -> @get('anonymous', true)
|
||||||
displayName: -> @get('name', true)
|
displayName: -> @get('name', true)
|
||||||
|
broadName: ->
|
||||||
|
name = @get('name')
|
||||||
|
return name if name
|
||||||
|
name = _.filter([@get('firstName'), @get('lastName')]).join('')
|
||||||
|
return name if name
|
||||||
|
email = @get('email')
|
||||||
|
return email if email
|
||||||
|
return ''
|
||||||
|
|
||||||
getPhotoURL: (size=80, useJobProfilePhoto=false, useEmployerPageAvatar=false) ->
|
getPhotoURL: (size=80, useJobProfilePhoto=false, useEmployerPageAvatar=false) ->
|
||||||
photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null
|
photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null
|
||||||
|
|
|
@ -393,3 +393,7 @@ body > iframe[src^="https://apis.google.com"]
|
||||||
top: 0
|
top: 0
|
||||||
left: 0
|
left: 0
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
|
|
||||||
|
// TODO: update Bootstrap, remove this
|
||||||
|
.text-uppercase
|
||||||
|
text-transform: uppercase
|
||||||
|
|
7
app/styles/courses/activate-licenses-modal.sass
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#activate-licenses-modal
|
||||||
|
h2
|
||||||
|
margin-top: -20px
|
||||||
|
|
||||||
|
.well
|
||||||
|
max-height: 200px
|
||||||
|
overflow: scroll
|
3
app/styles/courses/change-course-language-modal.sass
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#change-course-language-modal
|
||||||
|
p
|
||||||
|
margin: 30px 0
|
7
app/styles/courses/choose-language-modal.sass
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#choose-language-modal
|
||||||
|
button
|
||||||
|
margin: 20px 0 10px
|
||||||
|
|
||||||
|
.progress
|
||||||
|
width: 50%
|
||||||
|
margin: 50px 25%
|
7
app/styles/courses/classroom-view.sass
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#classroom-view
|
||||||
|
#main-button-area
|
||||||
|
.btn
|
||||||
|
margin-left: 10px
|
||||||
|
|
||||||
|
.progress
|
||||||
|
margin-bottom: 5px
|
|
@ -140,3 +140,25 @@
|
||||||
|
|
||||||
.settings-name-input
|
.settings-name-input
|
||||||
width: 50%
|
width: 50%
|
||||||
|
|
||||||
|
.jumbotron
|
||||||
|
form
|
||||||
|
margin-top: -40px
|
||||||
|
margin-bottom: 20px
|
||||||
|
|
||||||
|
.btn
|
||||||
|
margin-top: -15px
|
||||||
|
|
||||||
|
.no-school
|
||||||
|
width: 30%
|
||||||
|
|
||||||
|
.save-school
|
||||||
|
margin-left: 10%
|
||||||
|
width: 60%
|
||||||
|
|
||||||
|
.btn:not(.btn-submit)
|
||||||
|
white-space: normal
|
||||||
|
min-height: 200px
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size: 48px
|
||||||
|
|
|
@ -1,3 +1,50 @@
|
||||||
#courses-view
|
#courses-view
|
||||||
.row
|
#site-content-area
|
||||||
margin-top: 40px
|
padding-left: 250px
|
||||||
|
padding-right: 250px
|
||||||
|
|
||||||
|
h1
|
||||||
|
margin-bottom: 30px
|
||||||
|
|
||||||
|
#play-now-to-learn-header
|
||||||
|
margin-top: 60px
|
||||||
|
|
||||||
|
ul
|
||||||
|
margin: 0 auto 40px
|
||||||
|
width: 320px
|
||||||
|
|
||||||
|
#begin-hoc-area
|
||||||
|
width: 50%
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
hr
|
||||||
|
border-top: 1px solid grey
|
||||||
|
margin: 5px 0
|
||||||
|
|
||||||
|
.text-uppercase
|
||||||
|
margin-top: 40px
|
||||||
|
|
||||||
|
#just-added-text
|
||||||
|
color: #009999
|
||||||
|
|
||||||
|
.just-added
|
||||||
|
border: 1px solid #009999
|
||||||
|
margin: 0 -20px
|
||||||
|
padding: 0 20px
|
||||||
|
|
||||||
|
h3
|
||||||
|
margin-top: 20px
|
||||||
|
|
||||||
|
.course-instance-entry
|
||||||
|
padding-left: 40px
|
||||||
|
|
||||||
|
.progress-bar
|
||||||
|
min-width: 15%
|
||||||
|
|
||||||
|
.btn
|
||||||
|
margin-left: 20px
|
||||||
|
min-width: 180px
|
||||||
|
|
||||||
|
#join-class-form
|
||||||
|
.alert, .progress
|
||||||
|
margin-top: 20px
|
|
@ -1 +1,19 @@
|
||||||
//#hour-of-code-view
|
#hour-of-code-view
|
||||||
|
|
||||||
|
hr
|
||||||
|
border-top: 1px solid grey
|
||||||
|
margin: 30px 20px
|
||||||
|
|
||||||
|
#site-content-area
|
||||||
|
padding: 20px 300px
|
||||||
|
|
||||||
|
h1
|
||||||
|
margin-bottom: 40px
|
||||||
|
p
|
||||||
|
margin: 20px
|
||||||
|
|
||||||
|
h3
|
||||||
|
margin-top: 50px
|
||||||
|
|
||||||
|
ul
|
||||||
|
margin-bottom: 50px
|
15
app/styles/courses/invite-to-classroom-modal.sass
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#invite-to-classroom-modal
|
||||||
|
|
||||||
|
.modal-dialog
|
||||||
|
width: 700px
|
||||||
|
|
||||||
|
#class-code-well
|
||||||
|
margin: 0 auto
|
||||||
|
text-align: center
|
||||||
|
font-weight: bold
|
||||||
|
display: inline-block
|
||||||
|
|
||||||
|
#copied-alert, #copy-failed-alert
|
||||||
|
margin-top: 10px
|
||||||
|
padding: 5px
|
||||||
|
display: inline-block
|
14
app/styles/courses/mock1/purchase-courses-view.sass
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#purchase-courses-view
|
||||||
|
|
||||||
|
font-size: 18px
|
||||||
|
|
||||||
|
.enrollments-info
|
||||||
|
width: 300px
|
||||||
|
|
||||||
|
#students-input
|
||||||
|
width: 100px
|
||||||
|
font-size: 30px
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.uppercase
|
||||||
|
text-transform: uppercase
|
3
app/styles/courses/remove-student-modal.sass
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#remove-student-modal
|
||||||
|
.glyphicon-warning-sign
|
||||||
|
font-size: 40px
|
7
app/styles/courses/student-log-in-modal.sass
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#student-log-in-modal
|
||||||
|
#log-in-btn
|
||||||
|
min-width: 30%
|
||||||
|
margin-bottom: 10px
|
||||||
|
|
||||||
|
.form
|
||||||
|
margin: 0 25%
|
10
app/styles/courses/student-sign-up-modal.sass
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#student-sign-up-modal
|
||||||
|
#sign-up-btn
|
||||||
|
min-width: 30%
|
||||||
|
margin-bottom: 10px
|
||||||
|
|
||||||
|
.form
|
||||||
|
margin: 0 25%
|
||||||
|
|
||||||
|
.modal-dialog
|
||||||
|
margin-top: 0
|
|
@ -1,26 +1,70 @@
|
||||||
#teacher-courses-view
|
#teacher-courses-view
|
||||||
margin-bottom: 50px
|
margin-bottom: 50px
|
||||||
|
|
||||||
|
.active-courses
|
||||||
|
font-size: 12px
|
||||||
|
font-weight: bold
|
||||||
|
text-transform: uppercase
|
||||||
|
|
||||||
|
.class-count
|
||||||
|
font-size: 30px
|
||||||
|
|
||||||
|
.class-name
|
||||||
|
font-size: 20px
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.course-concept
|
||||||
|
display: inline-block
|
||||||
|
white-space: nowrap
|
||||||
|
font-size: 12px
|
||||||
|
line-height: 12px
|
||||||
|
border: 1px solid lightgray
|
||||||
|
margin-right: 4px
|
||||||
|
margin-top: 4px
|
||||||
|
padding: 6px
|
||||||
|
background-color: #AAEA6E
|
||||||
|
|
||||||
|
.course-enrolled
|
||||||
|
font-size: 12px
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.course-name
|
||||||
|
font-size: 18px
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.divider
|
||||||
|
border-bottom: 1px solid black
|
||||||
|
margin-bottom: 20px
|
||||||
|
|
||||||
img.media-object
|
img.media-object
|
||||||
width: 300px
|
width: 300px
|
||||||
|
|
||||||
.edit-classroom-small
|
.edit-classroom-small
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
&:hover
|
&:hover
|
||||||
color: grey
|
color: grey
|
||||||
|
|
||||||
#fixed-area
|
.no-students
|
||||||
position: fixed
|
font-size: 22px
|
||||||
bottom: 0
|
font-style: italic
|
||||||
left: 0
|
margin: 10px
|
||||||
right: 0
|
text-align: center
|
||||||
|
|
||||||
.well
|
.section-header
|
||||||
margin-bottom: 0
|
border-bottom: 1px solid black
|
||||||
padding: 5px
|
font-size: 20px
|
||||||
|
font-weight: bold
|
||||||
.col-sm-5
|
margin-bottom: 20px
|
||||||
padding-top: 8px
|
text-transform: uppercase
|
||||||
|
|
||||||
.progress
|
|
||||||
margin-bottom: 0
|
.text-center
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.uppercase
|
||||||
|
text-transform: uppercase
|
||||||
|
|
||||||
|
.welcome
|
||||||
|
font-size: 24px
|
||||||
|
font-weight: bold
|
||||||
|
margin-bottom: 20px
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
#teachers-view
|
#teachers-view
|
||||||
|
|
||||||
.main-content-area
|
.bigger-text
|
||||||
width: 650px
|
font-size: 18px
|
||||||
box-shadow: 0px 0px 0px
|
|
||||||
|
|
||||||
table
|
.uppercase
|
||||||
background-color: #F9F1DD
|
text-transform: uppercase
|
||||||
|
|
||||||
.discount-table
|
.btn-create-account
|
||||||
width: 50%
|
margin: 4px
|
||||||
|
min-width: 300px
|
||||||
|
|
||||||
.teachers-title
|
.btn-login-account
|
||||||
color: green
|
margin: 4px
|
||||||
|
min-width: 300px
|
||||||
|
|
||||||
|
.text-center
|
||||||
|
text-align: center
|
||||||
|
|
69
app/templates/courses/activate-licenses-modal.jade
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
extends /templates/core/modal-base
|
||||||
|
|
||||||
|
block modal-header-content
|
||||||
|
.clearfix
|
||||||
|
.text-center
|
||||||
|
h2 Activate Licenses
|
||||||
|
p= view.classroom.get('name')
|
||||||
|
|
||||||
|
block modal-body-content
|
||||||
|
- var usedLic = view.prepaids.totalRedeemers();
|
||||||
|
- var totalLic = view.prepaids.totalMaxRedeemers();
|
||||||
|
- var remainingLic = totalLic - usedLic;
|
||||||
|
.text-center
|
||||||
|
p
|
||||||
|
strong.spr Licenses remaining:
|
||||||
|
strong= remainingLic
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col-sm-10.col-sm-offset-1
|
||||||
|
form.form
|
||||||
|
if view.user
|
||||||
|
.form-group
|
||||||
|
.radio
|
||||||
|
label
|
||||||
|
input(type="radio", name="targets" value="given" checked=true)
|
||||||
|
span.spr Activate license for student:
|
||||||
|
span= view.user.get('name')
|
||||||
|
.radio
|
||||||
|
label
|
||||||
|
input(type="radio", name="targets" value="selection")
|
||||||
|
span.spr Activate licenses for the following students:
|
||||||
|
else
|
||||||
|
p Activate licenses for the following students:
|
||||||
|
|
||||||
|
.well.form-group
|
||||||
|
for user in view.users.models
|
||||||
|
.checkbox
|
||||||
|
label
|
||||||
|
- var paid = user.get('coursePrepaidID')
|
||||||
|
input(type="checkbox", disabled=paid, checked=true, data-user-id=user.id, name='user')
|
||||||
|
span.spr= user.broadName()
|
||||||
|
|
||||||
|
#error-alert.alert.alert-danger.hide
|
||||||
|
|
||||||
|
#progress-area.hide
|
||||||
|
.progress
|
||||||
|
.progress-bar
|
||||||
|
|
||||||
|
#submit-form-area.text-center
|
||||||
|
p
|
||||||
|
span.spr Total students:
|
||||||
|
span#total-selected-span.spr
|
||||||
|
span#not-depleted-span
|
||||||
|
| (
|
||||||
|
span.spr licenses remaining:
|
||||||
|
span#licenses-remaining-span
|
||||||
|
| )
|
||||||
|
span#depleted-span
|
||||||
|
| (
|
||||||
|
span insufficient licenses
|
||||||
|
| )
|
||||||
|
|
||||||
|
p
|
||||||
|
button#activate-licenses-btn.btn.btn-success.text-uppercase(type="submit") Activate Licenses
|
||||||
|
|
||||||
|
p
|
||||||
|
a#get-more-licenses-btn.btn.btn-info.text-uppercase(href="/courses/purchase") Get More Licenses
|
||||||
|
|
||||||
|
block modal-footer-content
|
28
app/templates/courses/change-course-language-modal.jade
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
extends /templates/core/modal-base
|
||||||
|
|
||||||
|
block modal-header-content
|
||||||
|
h3 Change Course Language
|
||||||
|
.clearfix
|
||||||
|
|
||||||
|
block modal-body-content
|
||||||
|
#choice-area.text-center
|
||||||
|
- var currentLanguage = (me.get('aceConfig') || {}).language || 'python';
|
||||||
|
button.lang-choice-btn.btn.btn-success.btn-lg(data-language='python')
|
||||||
|
if currentLanguage === 'python'
|
||||||
|
| Keep Using Python
|
||||||
|
else
|
||||||
|
| Switch To Python
|
||||||
|
|
||||||
|
p - OR -
|
||||||
|
|
||||||
|
button.lang-choice-btn.btn.btn-default(data-language='javascript')
|
||||||
|
if currentLanguage === 'javascript'
|
||||||
|
| Keep Using JavaScript
|
||||||
|
else
|
||||||
|
| Switch to JavaScript
|
||||||
|
|
||||||
|
#saving-progress.progress.progress-striped.active.hide
|
||||||
|
.progress-bar(style="width: 100%")
|
||||||
|
|
||||||
|
block modal-footer-content
|
||||||
|
|
21
app/templates/courses/choose-language-modal.jade
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
extends /templates/core/modal-base
|
||||||
|
|
||||||
|
block modal-header-content
|
||||||
|
.clearfix
|
||||||
|
.text-center
|
||||||
|
h2.modal-title Greetings!
|
||||||
|
h3(data-i18n="choose_hero.programming_language_description")
|
||||||
|
|
||||||
|
block modal-body-content
|
||||||
|
#choice-area.text-center
|
||||||
|
button.lang-choice-btn.btn.btn-success.btn-lg(data-language='python') Python
|
||||||
|
p(data-i18n="choose_hero.python_blurb")
|
||||||
|
|
||||||
|
button.lang-choice-btn.btn.btn-default(data-language='javascript') JavaScript
|
||||||
|
p(data-i18n="choose_hero.javascript_blurb")
|
||||||
|
|
||||||
|
#saving-progress.progress.progress-striped.active.hide
|
||||||
|
.progress-bar(style="width: 100%")
|
||||||
|
|
||||||
|
block modal-footer-content
|
||||||
|
|
|
@ -1,24 +1,30 @@
|
||||||
extends /templates/core/modal-base
|
extends /templates/core/modal-base
|
||||||
|
|
||||||
block modal-header-content
|
block modal-header-content
|
||||||
button.close(data-dismiss='modal')
|
if view.classroom
|
||||||
span ×
|
h3.modal-title(data-i18n="courses.edit_settings1")
|
||||||
h3.modal-title(data-i18n="courses.edit_settings1")
|
else
|
||||||
|
h3.modal-title(data-i18n="courses.create_new_class")
|
||||||
|
|
||||||
block modal-body-content
|
block modal-body-content
|
||||||
.form
|
.form
|
||||||
.form-group
|
.form-group
|
||||||
label(data-i18n="courses.title")
|
label(data-i18n="courses.title")
|
||||||
input.form-control.settings-name-input(type='text', value="#{view.classroom.get('name') || ''}")
|
- var name = view.classroom && view.classroom.get('name') ? view.classroom.get('name') : '';
|
||||||
|
input.form-control.settings-name-input(type='text', value="#{name}")
|
||||||
.form-group
|
.form-group
|
||||||
label(data-i18n="courses.description")
|
label(data-i18n="courses.description")
|
||||||
textarea.form-control.settings-description-input(rows=2)= view.classroom.get('description')
|
- var description = view.classroom && view.classroom.get('description') ? view.classroom.get('description') : '';
|
||||||
|
textarea.form-control.settings-description-input(rows=2)= description
|
||||||
.form-group
|
.form-group
|
||||||
label(data-i18n="choose_hero.programming_language")
|
label(data-i18n="choose_hero.programming_language")
|
||||||
select.form-control#programming-language-select
|
select.form-control#programming-language-select
|
||||||
- var aceConfig = view.classroom.get('aceConfig') || {};
|
- var aceConfig = view.classroom ? view.classroom.get('aceConfig') || {} : {};
|
||||||
option(value="python", selected=aceConfig.language==='python') Python
|
option(value="python", selected=aceConfig.language==='python') Learn Python
|
||||||
option(value="javascript", selected=aceConfig.language==='javascript') JavaScript
|
option(value="javascript", selected=aceConfig.language==='javascript') Learn JavaScript
|
||||||
|
|
||||||
block modal-footer-content
|
block modal-footer-content
|
||||||
button#save-settings-btn.btn(data-i18n="common.save_changes")
|
if view.classroom
|
||||||
|
button#save-settings-btn.btn(data-i18n="common.save_changes")
|
||||||
|
else
|
||||||
|
button#save-settings-btn.btn(data-i18n="courses.create_class")
|
||||||
|
|
65
app/templates/courses/classroom-view.jade
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
extends /templates/base
|
||||||
|
|
||||||
|
block content
|
||||||
|
|
||||||
|
- var isOwner = view.classroom.get('ownerID') === me.id;
|
||||||
|
if isOwner
|
||||||
|
a(href="/courses/teachers") Back to my classrooms
|
||||||
|
else
|
||||||
|
a(href="/courses") Back to my courses
|
||||||
|
|
||||||
|
h1
|
||||||
|
span.spr= view.classroom.get('name')
|
||||||
|
if isOwner
|
||||||
|
a#edit-class-details-link
|
||||||
|
small Edit class details
|
||||||
|
|
||||||
|
if view.classroom.get('description')
|
||||||
|
p= view.classroom.get('description')
|
||||||
|
|
||||||
|
// TODO: Add classroom statistics (grab from CourseDetailsView)
|
||||||
|
|
||||||
|
h1
|
||||||
|
| Students
|
||||||
|
.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
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
for user in view.users.models
|
||||||
|
a.remove-student-link.pull-right.text-uppercase(data-user-id=user.id)
|
||||||
|
span.glyphicon.glyphicon-remove
|
||||||
|
span.spl remove student
|
||||||
|
|
||||||
|
h2= user.broadName()
|
||||||
|
- var lastPlayedString = view.makeLastPlayedString(user);
|
||||||
|
if lastPlayedString
|
||||||
|
p Last Played: #{lastPlayedString}
|
||||||
|
|
||||||
|
- var paidFor = user.get('coursePrepaidID');
|
||||||
|
for courseInstance in view.courseInstances.models
|
||||||
|
- var inCourse = _.contains(courseInstance.get('members'), user.id);
|
||||||
|
- var course = view.courses.get(courseInstance.get('courseID'));
|
||||||
|
- var campaign = view.campaigns.get(course.get('campaignID'));
|
||||||
|
- var stats = campaign.statsForSessions(courseInstance.sessionsByUser[user.id] || []);
|
||||||
|
if !(course.get('free') || paidFor)
|
||||||
|
- continue;
|
||||||
|
.row
|
||||||
|
.col-sm-3.col-sm-offset-1= campaign.get('fullName')
|
||||||
|
.col-sm-7
|
||||||
|
if inCourse
|
||||||
|
.progress
|
||||||
|
.progress-bar(style='width:'+stats.levels.pctDone)
|
||||||
|
else if paidFor
|
||||||
|
button.enable-btn.btn.btn-info.btn-sm(data-user-id=user.id, data-course-instance-id=courseInstance.id) Enable
|
||||||
|
|
||||||
|
if !paidFor
|
||||||
|
.text-center
|
||||||
|
p
|
||||||
|
em Activate a license to enable more courses for this student.
|
||||||
|
p
|
||||||
|
button.activate-single-license-btn.btn.btn-info.btn-sm(data-user-id=user.id) Activate
|
||||||
|
|
||||||
|
hr
|
|
@ -2,11 +2,6 @@ extends /templates/base
|
||||||
|
|
||||||
block content
|
block content
|
||||||
|
|
||||||
div
|
|
||||||
span *UNDER CONSTRUCTION, send feedback to
|
|
||||||
a.spl(href='mailto:team@codecombat.com') team@codecombat.com
|
|
||||||
div(style='border-bottom: 1px solid black;')
|
|
||||||
|
|
||||||
if (noCourseInstance || noCourseInstanceSelected) && course
|
if (noCourseInstance || noCourseInstanceSelected) && course
|
||||||
h1= course.get('name')
|
h1= course.get('name')
|
||||||
if noCourseInstance
|
if noCourseInstance
|
||||||
|
@ -31,43 +26,110 @@ block content
|
||||||
else if !course || !courseInstance
|
else if !course || !courseInstance
|
||||||
h1(data-i18n="common.loading") Loading...
|
h1(data-i18n="common.loading") Loading...
|
||||||
else
|
else
|
||||||
h1
|
p
|
||||||
| #{course.get('name')}
|
// TODO: format this text all good and stuff
|
||||||
small.spl
|
strong
|
||||||
if courseInstance.get('name')
|
if courseInstance.get('name')
|
||||||
| (#{courseInstance.get('name')})
|
span= courseInstance.get('name')
|
||||||
else if view.classroom.get('name')
|
else if view.classroom.get('name')
|
||||||
| (#{view.classroom.get('name')})
|
span= view.classroom.get('name')
|
||||||
else
|
else
|
||||||
| (
|
|
||||||
span(data-i18n='courses.unnamed_class')
|
span(data-i18n='courses.unnamed_class')
|
||||||
| )
|
|
||||||
|
if !view.owner.isNew() && view.getOwnerName()
|
||||||
if !view.owner.isNew()
|
span.spl.spr - Teacher:
|
||||||
p
|
//a(href="/user/#{view.owner.id}") // Don't link to profiles until we improve them
|
||||||
span.spr Creator:
|
span
|
||||||
a(href="/user/#{view.owner.id}")
|
|
||||||
strong= view.getOwnerName()
|
strong= view.getOwnerName()
|
||||||
|
|
||||||
|
h1
|
||||||
|
| #{course.get('name')}
|
||||||
|
if view.courseComplete
|
||||||
|
span.spl - Complete!
|
||||||
|
|
||||||
p
|
p
|
||||||
if courseInstance.get('description')
|
if courseInstance.get('description')
|
||||||
each line in courseInstance.get('description').split('\n')
|
each line in courseInstance.get('description').split('\n')
|
||||||
div= line
|
div= line
|
||||||
|
|
||||||
div.well.well-sm(role='tabpanel')
|
if view.courseComplete && !view.teacherMode
|
||||||
ul.nav.nav-pills(role='tablist')
|
.jumbotron
|
||||||
if adminMode
|
if promptForSchool
|
||||||
li.active(role='presentation')
|
.row
|
||||||
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
|
.col-md-6.col-md-offset-3
|
||||||
li(role='presentation')
|
form.form#school-form
|
||||||
a(href='#levels', aria-controls='levels', role='tab', data-toggle='tab', data-i18n="nav.play")
|
.form-group
|
||||||
else
|
label.control-label(for="course-complete-school-input")
|
||||||
li.active(role='presentation')
|
span.spr(data-i18n="signup.school_name") School Name and City
|
||||||
a(href='#levels', aria-controls='levels', role='tab', data-toggle='tab', data-i18n="nav.play")
|
em.optional-note
|
||||||
li(role='presentation')
|
| (
|
||||||
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
|
span(data-i18n="signup.optional") optional
|
||||||
|
| ):
|
||||||
|
.input-border
|
||||||
|
input#course-complete-school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder")
|
||||||
|
button.btn.btn-primary.btn-submit.no-school(type="submit") None
|
||||||
|
button.btn.btn-info.btn-submit.save-school(type="submit") Save
|
||||||
|
.row
|
||||||
|
if view.singlePlayerMode && !me.isAnonymous()
|
||||||
|
.col-md-6.col-md-offset-3
|
||||||
|
a.btn.btn-lg.btn-success(href="/play")
|
||||||
|
h1 Play the Campaign
|
||||||
|
p You’re ready to take the next step! Explore hundreds of challenging levels, learn advanced programming skills, and compete in multiplayer arenas!
|
||||||
|
else if view.singlePlayerMode && me.isAnonymous()
|
||||||
|
.col-md-6
|
||||||
|
a.btn.btn-lg.btn-success.signup-button
|
||||||
|
h1 Create an Account
|
||||||
|
p Sign up for a FREE CodeCombat account and gain access to more levels, more programming skills, and more fun!
|
||||||
|
.col-md-6
|
||||||
|
a.btn.btn-lg.btn-success(href="/play")
|
||||||
|
h1 Preview Campaign
|
||||||
|
p Take a sneak peek at all that CodeCombat has to offer before signing up for your FREE account.
|
||||||
|
else if !view.singlePlayerMode
|
||||||
|
.col-md-6
|
||||||
|
if view.arenaLevel
|
||||||
|
a.btn.btn-lg.btn-success.btn-play-level(data-level-slug=view.arenaLevel.slug, data-level-id=view.arenaLevel.original)
|
||||||
|
h1
|
||||||
|
span Arena
|
||||||
|
| :
|
||||||
|
span.spl= view.arenaLevel.name
|
||||||
|
p= view.arenaLevel.description.replace(/!\[.*?\)/, '')
|
||||||
|
else
|
||||||
|
a.btn.btn-lg.btn-success.disabled
|
||||||
|
h1 Arena Coming Soon
|
||||||
|
p We are working on a multiplayer arena for classrooms at the end of #{course.get('name')}.
|
||||||
|
.col-md-6
|
||||||
|
if view.nextCourseInstance
|
||||||
|
a.btn.btn-lg.btn-success(href="/courses/#{view.nextCourse.id}/#{view.nextCourseInstance.id}")
|
||||||
|
h1= view.nextCourse.get('name')
|
||||||
|
p= view.nextCourse.get('description')
|
||||||
|
else if view.nextCourse
|
||||||
|
a.btn.btn-lg.btn-success.disabled
|
||||||
|
h1= view.nextCourse.get('name')
|
||||||
|
p
|
||||||
|
em NOT ENROLLED
|
||||||
|
p Ask your teacher to enroll you in the next course.
|
||||||
|
else
|
||||||
|
a.btn.btn-lg.btn-success(disabled=!view.nextCourse ? "disabled" : "")
|
||||||
|
h1 Next Course
|
||||||
|
p
|
||||||
|
em COMING SOON
|
||||||
|
p We are hard at work making more courses for you!
|
||||||
|
|
||||||
|
if !me.isAnonymous()
|
||||||
|
div.well.well-sm(role='tabpanel')
|
||||||
|
ul.nav.nav-pills(role='tablist')
|
||||||
|
if view.teacherMode
|
||||||
|
li.active(role='presentation')
|
||||||
|
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
|
||||||
|
li(role='presentation')
|
||||||
|
a(href='#levels', aria-controls='levels', role='tab', data-toggle='tab', data-i18n="nav.play")
|
||||||
|
else
|
||||||
|
li.active(role='presentation')
|
||||||
|
a(href='#levels', aria-controls='levels', role='tab', data-toggle='tab', data-i18n="nav.play")
|
||||||
|
li(role='presentation')
|
||||||
|
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
|
||||||
.tab-content
|
.tab-content
|
||||||
if adminMode
|
if view.teacherMode
|
||||||
.tab-pane.active#progress(role='tabpanel')
|
.tab-pane.active#progress(role='tabpanel')
|
||||||
+progress-tab
|
+progress-tab
|
||||||
.tab-pane#levels(role='tabpanel')
|
.tab-pane#levels(role='tabpanel')
|
||||||
|
@ -243,7 +305,7 @@ mixin progress-members-popup-completed(i, level, session)
|
||||||
p
|
p
|
||||||
span.spr(data-i18n="courses.completed")
|
span.spr(data-i18n="courses.completed")
|
||||||
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
|
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
|
||||||
if adminMode
|
if view.teacherMode || me.isAdmin()
|
||||||
strong(data-i18n="clans.view_solution")
|
strong(data-i18n="clans.view_solution")
|
||||||
|
|
||||||
mixin progress-members-popup-started(i, level, session)
|
mixin progress-members-popup-started(i, level, session)
|
||||||
|
@ -255,7 +317,7 @@ mixin progress-members-popup-started(i, level, session)
|
||||||
p
|
p
|
||||||
span.spr(data-i18n="clans.last_played")
|
span.spr(data-i18n="clans.last_played")
|
||||||
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
|
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
|
||||||
if adminMode
|
if view.teacherMode || me.isAdmin()
|
||||||
strong(data-i18n="clans.view_attempt")
|
strong(data-i18n="clans.view_attempt")
|
||||||
|
|
||||||
mixin levels-tab
|
mixin levels-tab
|
||||||
|
@ -273,7 +335,7 @@ mixin levels-tab
|
||||||
each level, levelID in campaign.get('levels')
|
each level, levelID in campaign.get('levels')
|
||||||
tr
|
tr
|
||||||
td
|
td
|
||||||
if lastLevelCompleted || adminMode
|
if lastLevelCompleted || view.teacherMode
|
||||||
- var i18n = level.type === 'course-ladder' ? 'play.compete' : 'home.play';
|
- var i18n = level.type === 'course-ladder' ? 'play.compete' : 'home.play';
|
||||||
button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n=i18n, data-level-id=levelID)
|
button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n=i18n, data-level-id=levelID)
|
||||||
td
|
td
|
||||||
|
@ -288,4 +350,3 @@ mixin levels-tab
|
||||||
each concept in course.get('concepts')
|
each concept in course.get('concepts')
|
||||||
if levelConceptMap[levelID][concept]
|
if levelConceptMap[levelID][concept]
|
||||||
span.spr.progress-level-cell.progress-level-cell-not-started(data-i18n="concepts." + concept)
|
span.spr.progress-level-cell.progress-level-cell-not-started(data-i18n="concepts." + concept)
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,165 @@
|
||||||
extends /templates/base
|
extends /templates/base
|
||||||
|
|
||||||
block content
|
block content
|
||||||
|
.pull-right
|
||||||
|
if me.isAnonymous()
|
||||||
|
a(href="/teachers") Teachers, click here!
|
||||||
|
else
|
||||||
|
a(href="/courses/teachers") Teachers, click here!
|
||||||
|
br
|
||||||
|
|
||||||
h1.text-center Welcome to CodeCombat Courses
|
#main-content
|
||||||
|
if me.isAnonymous()
|
||||||
.row
|
|
||||||
.col-sm-6.text-center
|
h1.text-center Adventurers, welcome to Courses!
|
||||||
a(href="/courses/students").btn.btn-default Students Click Here
|
|
||||||
|
.text-center
|
||||||
|
p
|
||||||
|
h3 Ready to play?
|
||||||
|
p
|
||||||
|
button#start-new-game-btn.btn.btn-default Start New Game
|
||||||
|
p - OR -
|
||||||
|
p
|
||||||
|
button#log-in-btn.btn.btn-default(data-i18n="login.log_in")
|
||||||
|
|
||||||
|
h3#play-now-to-learn-header.text-center PLAY NOW TO LEARN
|
||||||
|
ul
|
||||||
|
li basic syntax to control your character
|
||||||
|
li while loops to solve pesky puzzles
|
||||||
|
li strings & variables to customize actions
|
||||||
|
li how to defeat an ogre (important life skills!)
|
||||||
|
|
||||||
.col-sm-6.text-center
|
else
|
||||||
a(href="/courses/teachers").btn.btn-default Teachers Click Here
|
|
||||||
|
- var showHOCComplete = false;
|
||||||
|
if view.hocCourseInstance
|
||||||
|
- var course = view.courses.get(view.hocCourseInstance.get('courseID'));
|
||||||
|
- var campaign = view.campaigns.get(course.get('campaignID'));
|
||||||
|
- var stats = campaign.statsForSessions(view.hocCourseInstance.sessions);
|
||||||
|
- showHOCComplete = stats.levels.done && !view.classrooms.size();
|
||||||
|
|
||||||
|
.text-center
|
||||||
|
if !showHOCComplete
|
||||||
|
h1 Welcome to your Courses page!
|
||||||
|
else
|
||||||
|
h1 Amazing! You've completed the Hour of Code course!
|
||||||
|
h2 Ready for more? Play the campaign mode!
|
||||||
|
ul.text-left
|
||||||
|
li Use gems to unlock new items!
|
||||||
|
li Play through brand new worlds and challenges
|
||||||
|
li Learn even more programming!
|
||||||
|
a.btn.btn-lg.btn-success(href="/play") Play Now
|
||||||
|
|
||||||
|
if view.hocCourseInstance && !view.classrooms.size()
|
||||||
|
h3 Saved Games
|
||||||
|
hr
|
||||||
|
|
||||||
|
.course-instance-entry
|
||||||
|
h3
|
||||||
|
span.spr Hour of Code: Course 1
|
||||||
|
span.spr= (me.get('aceConfig') || {}).language === 'python' ? 'Python' : 'JavaScript'
|
||||||
|
small
|
||||||
|
a#change-language-link change language
|
||||||
|
+course-instance-body(view.hocCourseInstance)
|
||||||
|
.clearfix
|
||||||
|
|
||||||
|
else if view.classrooms.size()
|
||||||
|
h3.text-uppercase My Classes
|
||||||
|
hr
|
||||||
|
|
||||||
|
for classroom in view.classrooms.models
|
||||||
|
- var justAdded = classroom.id === view.classroomJustAdded;
|
||||||
|
- var classroomClass = justAdded ? 'just-added' : '';
|
||||||
|
if justAdded
|
||||||
|
#just-added-text.text-center Class successfully added!
|
||||||
|
|
||||||
|
//- sigh
|
||||||
|
div(class=classroomClass)
|
||||||
|
h3
|
||||||
|
span.spr= classroom.get('name')
|
||||||
|
span.spr (#{(classroom.get('aceConfig') || {}).language === 'python' ? 'Python' : 'JavaScript'})
|
||||||
|
a(href="/courses/"+classroom.id) view class
|
||||||
|
|
||||||
|
- var courseInstances = view.courseInstances.where({classroomID: classroom.id});
|
||||||
|
for courseInstance in courseInstances
|
||||||
|
|
||||||
|
.course-instance-entry
|
||||||
|
- var course = view.courses.get(courseInstance.get('courseID'));
|
||||||
|
h3
|
||||||
|
span.spr= course.get('name')
|
||||||
|
small
|
||||||
|
a(href="/courses/"+courseInstance.get('courseID')+'/'+courseInstance.id+'#levels') view levels
|
||||||
|
+course-instance-body(courseInstance)
|
||||||
|
.clearfix
|
||||||
|
|
||||||
|
else
|
||||||
|
.text-center
|
||||||
|
button#start-new-game-btn.btn.btn-success.btn-lg Start New Game
|
||||||
|
|
||||||
|
h3.text-uppercase Join A Class
|
||||||
|
hr
|
||||||
|
|
||||||
|
form#join-class-form.form-inline
|
||||||
|
.help-block
|
||||||
|
em Ask your teacher if you have a CodeCombat class code! If so, enter it below:
|
||||||
|
.form-group
|
||||||
|
input#class-code-input.form-control(placeholder="<Enter Class Code>", value=view.classCode)
|
||||||
|
input#join-class-button.btn.btn-default(type="submit" value="Join")
|
||||||
|
|
||||||
|
if view.state === 'enrolling'
|
||||||
|
.progress.progress-striped.active
|
||||||
|
.progress-bar(style="width: 100%") Joining class
|
||||||
|
|
||||||
|
if view.errorMessage
|
||||||
|
.alert.alert-danger= view.errorMessage
|
||||||
|
|
||||||
|
|
||||||
|
#begin-hoc-area.hide
|
||||||
|
h3.text-center(data-i18n="common.loading")
|
||||||
|
.progress.progress-striped.active
|
||||||
|
.progress-bar(style="width: 100%")
|
||||||
|
|
||||||
|
|
||||||
|
mixin course-instance-body(courseInstance)
|
||||||
|
- var course = view.courses.get(courseInstance.get('courseID'));
|
||||||
|
- var campaign = view.campaigns.get(course.get('campaignID'));
|
||||||
|
- var stats = campaign.statsForSessions(courseInstance.sessions);
|
||||||
|
if stats.levels.done
|
||||||
|
.text-success
|
||||||
|
span.glyphicon.glyphicon-ok
|
||||||
|
span.spl Course Complete!
|
||||||
|
.pull-right
|
||||||
|
if stats.levels.done
|
||||||
|
- var arenaLevel = stats.levels.arena;
|
||||||
|
if arenaLevel
|
||||||
|
- var arenaURL = "/play/ladder/"+arenaLevel.slug+"/course/"+courseInstance.id;
|
||||||
|
a.btn.btn-warning.btn-lg(href=arenaURL)
|
||||||
|
| Play Arena
|
||||||
|
else
|
||||||
|
a.btn.btn-default.btn-lg(disabled=true) Course Complete
|
||||||
|
else if courseInstance.sessions.size()
|
||||||
|
- var lastLevel = stats.levels.lastPlayed;
|
||||||
|
- var levelURL = "/play/level/"+lastLevel.slug+"?course="+courseInstance.get('courseID')+"&course-instance="+courseInstance.id;
|
||||||
|
a.btn.btn-success.btn-lg(href=levelURL)
|
||||||
|
| Continue
|
||||||
|
else
|
||||||
|
- var firstLevel = stats.levels.first;
|
||||||
|
- var levelURL = "/play/level/"+firstLevel.slug+"?course="+courseInstance.get('courseID')+"&course-instance="+courseInstance.id;
|
||||||
|
a.btn.btn-info.btn-lg(href=levelURL)
|
||||||
|
| Start
|
||||||
|
|
||||||
|
div
|
||||||
|
span Playtime
|
||||||
|
span.spr :
|
||||||
|
span= moment.duration(stats.playtime, 'seconds').humanize()
|
||||||
|
|
||||||
|
if stats.levels.lastPlayed
|
||||||
|
div
|
||||||
|
span Last Level
|
||||||
|
span.spr :
|
||||||
|
span= stats.levels.lastPlayed.name
|
||||||
|
|
||||||
|
.progress
|
||||||
|
.progress-bar(style="width:"+stats.levels.pctDone)= stats.levels.pctDone
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,66 @@
|
||||||
extends /templates/base
|
extends /templates/base
|
||||||
|
|
||||||
block content
|
block content
|
||||||
h1.text-center Welcome to CodeCombat's Hour of Code!
|
.pull-right
|
||||||
|
if me.isAnonymous()
|
||||||
|
a(href="/teachers") Teachers, click here!
|
||||||
|
else
|
||||||
|
a(href="/courses/teachers") Teachers, click here!
|
||||||
br
|
br
|
||||||
.container-fluid
|
|
||||||
.row
|
h1.text-center Adventurers, welcome to our Hour of Code!
|
||||||
.col-md-6.text-center
|
|
||||||
button#student-btn.btn.btn-lg.btn-success(data-i18n="courses.students_click")
|
#main-content
|
||||||
.col-md-6.text-center
|
.well.text-center
|
||||||
a.btn.btn-lg.btn-default(data-i18n="courses.teachers_click", href="/courses/teachers?hoc=true")
|
if !me.isAnonymous()
|
||||||
|
p
|
||||||
|
strong.spr Logged in as:
|
||||||
|
strong= me.get('name') || me.get('email')
|
||||||
|
|
||||||
|
p
|
||||||
|
span.spr This isn't you?
|
||||||
|
a#log-out-link Logout
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
if !view.lastLevel
|
||||||
|
p
|
||||||
|
strong Ready to play?
|
||||||
|
|
||||||
|
p
|
||||||
|
button#start-new-game-btn.btn.btn-success.btn-lg Start New Game
|
||||||
|
else
|
||||||
|
|
||||||
|
p
|
||||||
|
strong Hi adventurer, welcome back!
|
||||||
|
p
|
||||||
|
a#continue-playing-btn.btn.btn-default(href=view.continuePlayingLink()) Continue playing
|
||||||
|
p
|
||||||
|
em.spr
|
||||||
|
span.spr Last Played:
|
||||||
|
span= view.lastLevel.get('name').replace('Course :', '')
|
||||||
|
|
||||||
|
if me.isAnonymous()
|
||||||
|
p
|
||||||
|
strong More options:
|
||||||
|
p
|
||||||
|
button#start-new-game-btn.btn.btn-default Start New Game
|
||||||
|
|
||||||
|
if me.isAnonymous()
|
||||||
|
p - OR -
|
||||||
|
|
||||||
|
p
|
||||||
|
button#log-in-btn.btn.btn-default(data-i18n="login.log_in")
|
||||||
|
|
||||||
|
#begin-hoc-area.hide
|
||||||
|
h2.text-center(data-i18n="common.loading")
|
||||||
|
.progress.progress-striped.active
|
||||||
|
.progress-bar(style="width: 100%")
|
||||||
|
|
||||||
|
|
||||||
|
h3.text-center PLAY NOW TO LEARN
|
||||||
|
ul
|
||||||
|
li basic syntax to control your character
|
||||||
|
li while loops to solve pesky puzzles
|
||||||
|
li strings & variables to customize actions
|
||||||
|
li how to defeat an ogre (important life skills!)
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
extends /templates/core/modal-base
|
extends /templates/core/modal-base
|
||||||
|
|
||||||
block modal-header-content
|
block modal-header-content
|
||||||
h2 Invite Students to Classroom
|
h2 Add Students
|
||||||
h3= view.classroom.get('name')
|
h3= view.classroom.get('name')
|
||||||
|
|
||||||
block modal-body-content
|
block modal-body-content
|
||||||
p(data-i18n="courses.invite_students")
|
h3 Option 1: Invite students via email
|
||||||
h3(data-i18n="courses.invite_link_header")
|
p Students will automatically be sent an invitation to join this class, and will
|
||||||
p(data-i18n="courses.invite_link_p_1")
|
| need to create an account with a username and password.
|
||||||
.alert.alert-info
|
|
||||||
|
|
||||||
strong= document.location.origin + "/courses/students?_cc=" + (view.classroom.get('codeCamel') || view.classroom.get('code'))
|
|
||||||
p(data-i18n="courses.invite_link_p_2")
|
|
||||||
.form
|
.form
|
||||||
.form-group
|
.form-group
|
||||||
textarea#invite-emails-textarea.form-control
|
textarea#invite-emails-textarea.form-control
|
||||||
|
@ -20,3 +16,21 @@ block modal-body-content
|
||||||
button#send-invites-btn.btn.btn-success(data-i18n="courses.send_invites")
|
button#send-invites-btn.btn.btn-success(data-i18n="courses.send_invites")
|
||||||
#invite-emails-sending-alert.alert.alert-info.hide(data-i18n="common.sending")
|
#invite-emails-sending-alert.alert.alert-info.hide(data-i18n="common.sending")
|
||||||
#invite-emails-success-alert.alert.alert-success.hide(data-i18n="play_level.done")
|
#invite-emails-success-alert.alert.alert-success.hide(data-i18n="play_level.done")
|
||||||
|
|
||||||
|
h3 Option 2: Send URL to your students
|
||||||
|
p Students will be asked to enter an email address, username and password to create an account.
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col-sm-9
|
||||||
|
input.form-control#join-url-input(value=view.joinURL)
|
||||||
|
.col-sm-3
|
||||||
|
button#copy-url-btn.btn.btn-fixed.btn-default.text-uppercase Copy URL
|
||||||
|
#copied-alert.alert.alert-info.hide Copied
|
||||||
|
#copy-failed-alert.alert.alert-danger.hide Error copying
|
||||||
|
|
||||||
|
h3 Option 3: Direct students to codecombat.com/courses
|
||||||
|
p Give students the following passcode to enter along with an email address,
|
||||||
|
| username and password when they create an account.
|
||||||
|
|
||||||
|
.text-center
|
||||||
|
#class-code-well.well= view.classCode
|
||||||
|
|
|
@ -6,36 +6,43 @@ block content
|
||||||
p.text-center Purchasing...
|
p.text-center Purchasing...
|
||||||
.progress.progress-striped.active
|
.progress.progress-striped.active
|
||||||
.progress-bar(style="width: 100%")
|
.progress-bar(style="width: 100%")
|
||||||
|
|
||||||
else if view.state === 'purchased'
|
else if view.state === 'purchased'
|
||||||
p Thank you for your purchase! You can now assign (more) students to paid courses.
|
p Thank you for your purchase! You can now assign #{view.numberOfStudents} more students to paid courses.
|
||||||
|
|
||||||
p.text-center
|
p.text-center
|
||||||
a(href="/courses/teachers") Return to course management.
|
if view.fromClassroom
|
||||||
|
a(href="/courses/"+view.fromClassroom) Return to classroom
|
||||||
|
else
|
||||||
|
a(href="/courses/teachers") Return to course management.
|
||||||
|
|
||||||
else
|
else
|
||||||
h3.text-center Purchase Courses for Students
|
|
||||||
|
h2.text-center Purchase Student Enrollments
|
||||||
|
br
|
||||||
|
|
||||||
if view.state === 'error'
|
if view.state === 'error'
|
||||||
.alert.alert-danger= view.stateMessage
|
.alert.alert-danger= view.stateMessage
|
||||||
|
|
||||||
.form-horizontal
|
p.text-center
|
||||||
.form-group
|
strong How many enrollments do you need?
|
||||||
label.col-sm-3.control-label Students
|
br
|
||||||
.col-sm-6
|
|
||||||
input#students-input.form-control(
|
p.text-center
|
||||||
placeholder='<number of seats>'
|
input#students-input(
|
||||||
value=view.numberOfStudents
|
placeholder='<number of enrollments>'
|
||||||
type='number'
|
value=view.numberOfStudents
|
||||||
)
|
type='number'
|
||||||
.help-block Each student will have access to all courses.
|
)
|
||||||
|
br
|
||||||
#price-form-group.form-group
|
|
||||||
label.col-sm-3.control-label Price
|
.container-fluid
|
||||||
.col-sm-6
|
.row
|
||||||
.form-control-static
|
.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.
|
||||||
| #{view.getPriceString()} ($#{view.pricePerStudent.toFixed(2)} per student)
|
br
|
||||||
|
|
||||||
.form-group
|
p.text-center#price-form-group
|
||||||
.col-sm-offset-3.col-sm-10
|
strong Total: #{view.numberOfStudents} enrollments x $#{view.pricePerStudent.toFixed(2)} = #{view.getPriceString()}
|
||||||
button#purchase-btn.btn.btn-primary Purchase
|
|
||||||
|
p.text-center
|
||||||
|
button#purchase-btn.btn.btn-lg.btn-success.uppercase Purchase Now
|
||||||
|
|
27
app/templates/courses/remove-student-modal.jade
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
extends /templates/core/modal-base
|
||||||
|
|
||||||
|
block modal-header-content
|
||||||
|
.text-center
|
||||||
|
h3.modal-title Remove Student
|
||||||
|
span.glyphicon.glyphicon-warning-sign.text-danger
|
||||||
|
h3 Are you sure you want to remove this student from this class?
|
||||||
|
|
||||||
|
block modal-body-content
|
||||||
|
p.text-center
|
||||||
|
| Student will lose access to this classroom and assigned classes.
|
||||||
|
| Progress and gameplay is NOT lost, and the student can be added back to the classroom at any time.
|
||||||
|
if view.user.get('coursePrepaidID')
|
||||||
|
| The activated paid license will not be returned.
|
||||||
|
|
||||||
|
block modal-footer-content
|
||||||
|
#remove-student-buttons.text-center
|
||||||
|
p
|
||||||
|
button.btn.btn-lg.btn-success.text-uppercase(data-dismiss="modal") Keep Student
|
||||||
|
p - OR -
|
||||||
|
p
|
||||||
|
button#remove-student-btn.btn.btn-lg.btn-default.text-uppercase Remove Student
|
||||||
|
|
||||||
|
#remove-student-progress.text-center.hide
|
||||||
|
.progress
|
||||||
|
.progress-bar
|
||||||
|
p.text-info Removing user
|
28
app/templates/courses/student-log-in-modal.jade
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
extends /templates/core/modal-base
|
||||||
|
|
||||||
|
block modal-header-content
|
||||||
|
.clearfix
|
||||||
|
|
||||||
|
|
||||||
|
block modal-body-content
|
||||||
|
.text-center
|
||||||
|
h2.modal-title(data-i18n="login.log_in")
|
||||||
|
|
||||||
|
form.form
|
||||||
|
.form-group
|
||||||
|
label.control-label(for="email")
|
||||||
|
span(data-i18n="general.email")
|
||||||
|
input#email.input-large.form-control(name="email", type="email")
|
||||||
|
.form-group
|
||||||
|
label.control-label(for="password")
|
||||||
|
span(data-i18n="general.password") Password
|
||||||
|
input#password.input-large.form-control(name="password", type="password")
|
||||||
|
|
||||||
|
#errors-alert.alert.alert-danger.hide
|
||||||
|
|
||||||
|
.text-center
|
||||||
|
input#log-in-btn.btn.btn-default(data-i18n="[value]login.log_in", type="submit")
|
||||||
|
p
|
||||||
|
a#create-new-account-link(data-i18n="login.signup_switch")
|
||||||
|
|
||||||
|
block modal-footer-content
|
51
app/templates/courses/student-sign-up-modal.jade
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
extends /templates/core/modal-base
|
||||||
|
|
||||||
|
block modal-header-content
|
||||||
|
.clearfix
|
||||||
|
|
||||||
|
|
||||||
|
block modal-body-content
|
||||||
|
.text-center
|
||||||
|
h2.modal-title(data-i18n="signup.sign_up")
|
||||||
|
|
||||||
|
form.form
|
||||||
|
.form-group
|
||||||
|
label.control-label(for="email")
|
||||||
|
span(data-i18n="general.email")
|
||||||
|
input#email.input-large.form-control(name="email", type="email")
|
||||||
|
.help-block use your school email if you have one
|
||||||
|
.form-group
|
||||||
|
label.control-label(for="name")
|
||||||
|
span(data-i18n="general.name") Name
|
||||||
|
if me.get('name')
|
||||||
|
input#name.input-large.form-control(name="name", type="text", value="#{me.get('name')}")
|
||||||
|
else
|
||||||
|
input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater")
|
||||||
|
.help-block a unique name no one has chosen
|
||||||
|
.form-group
|
||||||
|
label.control-label(for="password")
|
||||||
|
span(data-i18n="general.password") Password
|
||||||
|
input#password.input-large.form-control(name="password", type="password")
|
||||||
|
.help-block pick something you can remember
|
||||||
|
.form-group
|
||||||
|
label.control-label(for="class-code-input")
|
||||||
|
span Class Code
|
||||||
|
input#class-code-input.input-large.form-control(name="classCode", value=view.classCode)
|
||||||
|
.help-block optional - ask your teacher to give you one!
|
||||||
|
.form-group
|
||||||
|
label.control-label(for="school-input")
|
||||||
|
span(data-i18n="signup.school_name") School Name and City
|
||||||
|
input#school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder")
|
||||||
|
.help-block optional - what school do you go to?
|
||||||
|
|
||||||
|
#errors-alert.alert.alert-danger.hide
|
||||||
|
|
||||||
|
.text-center
|
||||||
|
if view.willPlay
|
||||||
|
input#sign-up-btn.btn.btn-default(type="submit", value="Start Playing")
|
||||||
|
p
|
||||||
|
a#skip-link Skip this, I'll create an account later!
|
||||||
|
else
|
||||||
|
input#sign-up-btn.btn.btn-default(data-i18n="[value]signup.sign_up", type="submit")
|
||||||
|
|
||||||
|
block modal-footer-content
|
|
@ -2,188 +2,104 @@ extends /templates/base
|
||||||
|
|
||||||
block content
|
block content
|
||||||
|
|
||||||
if view.hoc
|
.text-center
|
||||||
h1 Welcome to Hour of Code!
|
if me.isAnonymous() || !me.get('name')
|
||||||
|
.welcome Welcome!
|
||||||
p
|
else
|
||||||
| Thank you for choosing CodeCombat for your students.
|
.welcome Welcome, #{me.get('name')}!
|
||||||
span.spr.spl To get your kids started, simply send them to
|
|
||||||
a(href="/hoc") https://codecombat.com/hoc
|
|
||||||
span .
|
|
||||||
|
|
||||||
p
|
|
||||||
| If you'd like to use our courses system to view their progress:
|
|
||||||
|
|
||||||
ol
|
|
||||||
li Login/create your account if you have not already.
|
|
||||||
li Create a classroom on this page.
|
|
||||||
li Invite your students to the classroom.
|
|
||||||
|
|
||||||
p
|
|
||||||
| You can invite your students even if they've already started playing CodeCombat.
|
|
||||||
|
|
||||||
p
|
|
||||||
span.spr If you have any problems, please email
|
|
||||||
a(href="mailto:team@codecombat.com") team@codecombat.com
|
|
||||||
span .
|
|
||||||
|
|
||||||
if !me.isAnonymous()
|
|
||||||
ul.nav.nav-tabs(role='tablist')
|
|
||||||
li.active(role='presentation')
|
|
||||||
a(href="#courses-tab-pane" aria-controls="courses" role="tab" data-toggle="tab") Courses
|
|
||||||
li(role='presentation')
|
|
||||||
a(href="#manage-tab-pane" aria-controls="manage" role="tab" data-toggle="tab") Manage
|
|
||||||
|
|
||||||
.tab-content
|
|
||||||
#courses-tab-pane.tab-pane.well.active
|
|
||||||
h3 Your Courses
|
|
||||||
- var courseInstances = view.courseInstances.sliceWithMembers();
|
|
||||||
|
|
||||||
if me.isAnonymous()
|
|
||||||
.alert.alert-info
|
|
||||||
strong Please click "Create Account" or "Log In" above to view and manage your courses.
|
|
||||||
|
|
||||||
else if !_.size(courseInstances)
|
|
||||||
.alert.alert-info
|
|
||||||
span.spr You currently have no students assigned to courses.
|
|
||||||
a#manage-tab-link Go to the manage tab to get set up.
|
|
||||||
|
|
||||||
else
|
|
||||||
table.table
|
|
||||||
tr
|
|
||||||
th Class
|
|
||||||
th Course
|
|
||||||
th Size
|
|
||||||
th
|
|
||||||
for courseInstance in courseInstances
|
|
||||||
tr
|
|
||||||
td
|
|
||||||
- var classroom = view.classrooms.get(courseInstance.get('classroomID'));
|
|
||||||
if classroom
|
|
||||||
| #{classroom.get('name')}
|
|
||||||
td
|
|
||||||
- var course = view.courses.get(courseInstance.get('courseID'))
|
|
||||||
if course
|
|
||||||
| #{course.get('name')}
|
|
||||||
td= _.size(courseInstance.get('members'))
|
|
||||||
td
|
|
||||||
a.btn.btn-primary.btn-sm(href='/courses/#{courseInstance.get("courseID")}/#{courseInstance.id}') Enter
|
|
||||||
|
|
||||||
h3 Available Courses
|
|
||||||
|
|
||||||
for course in view.courses.models
|
|
||||||
.media
|
|
||||||
.pull-left
|
|
||||||
img.media-object(src=course.get('screenshot'))
|
|
||||||
.media-body
|
|
||||||
h3.media-heading
|
|
||||||
span.spr= course.get('name')
|
|
||||||
if course.get('free')
|
|
||||||
em (free!)
|
|
||||||
p= course.get('description')
|
|
||||||
p
|
|
||||||
strong.spr Concepts:
|
|
||||||
span= (course.get('concepts') || []).join(', ')
|
|
||||||
p
|
|
||||||
strong.spr Length:
|
|
||||||
span #{course.get('duration') || 0} hours
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#manage-tab-pane.tab-pane.well
|
|
||||||
|
|
||||||
p Create a class and add students to it.
|
.section-header Your Classes
|
||||||
|
|
||||||
- var totalRedeemers = view.prepaids.totalRedeemers();
|
if view.classrooms.models.length > 0
|
||||||
- var totalMaxRedeemers = view.prepaids.totalMaxRedeemers();
|
.container-fluid
|
||||||
|
each classroom in view.classrooms.models
|
||||||
.text-right
|
+classroom(classroom)
|
||||||
span.spr Used paid seats: #{totalRedeemers}/#{totalMaxRedeemers}
|
else
|
||||||
a.btn.btn-default.btn-xs(href="/courses/purchase") Add
|
.no-students No classes yet!
|
||||||
|
|
||||||
for classroom in view.classrooms.models
|
.text-center
|
||||||
h2
|
button.btn.btn-lg.btn-success.uppercase.create-new-class create new class
|
||||||
span.spr= classroom.get('name')
|
|
||||||
- var language = (classroom.get('aceConfig') || {}).language || 'python';
|
br
|
||||||
if language === 'python'
|
.section-header Available Courses
|
||||||
img(src="/images/common/code_languages/python_icon.png")
|
.container-fluid
|
||||||
if language === 'javascript'
|
- var courses = view.courses.models;
|
||||||
img(src="/images/common/code_languages/javascript_icon.png")
|
- var i = 0;
|
||||||
small.spl.edit-classroom-small(data-classroom-id=classroom.id)
|
while i < courses.length
|
||||||
span.glyphicon.glyphicon-pencil
|
- var course = courses[i];
|
||||||
|
- i++;
|
||||||
- var courseInstances = view.courseInstances.where({classroomID: classroom.id})
|
|
||||||
|
|
||||||
if classroom.saving || classroom.filling
|
|
||||||
.progress.progress-striped.active
|
|
||||||
.progress-bar(style="width: 100%")
|
|
||||||
|
|
||||||
else
|
|
||||||
- var description = classroom.get('description');
|
|
||||||
if description
|
|
||||||
p= description
|
|
||||||
table.table
|
|
||||||
tr
|
|
||||||
th Student
|
|
||||||
for courseInstance in courseInstances
|
|
||||||
th
|
|
||||||
if courseInstance.course
|
|
||||||
| #{courseInstance.course.get('name')}
|
|
||||||
|
|
||||||
if !_.size(classroom.get('members'))
|
|
||||||
tr
|
|
||||||
td(colspan=1+view.courses.size())
|
|
||||||
em No students in this class yet.
|
|
||||||
|
|
||||||
for member in classroom.get('members') || []
|
|
||||||
- var user = view.members.get(member);
|
|
||||||
if !user
|
|
||||||
- continue;
|
|
||||||
tr
|
|
||||||
td= user.get('name')
|
|
||||||
for courseInstance in courseInstances
|
|
||||||
td
|
|
||||||
if _.contains(courseInstance.get('members'), user.id)
|
|
||||||
span.glyphicon.glyphicon-ok
|
|
||||||
else
|
|
||||||
input.course-instance-membership-checkbox(
|
|
||||||
type='checkbox'
|
|
||||||
data-course-instance-id=courseInstance.id
|
|
||||||
data-user-id=user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
button.add-students-btn.btn.btn-sm(data-classroom-id=classroom.id) Add Students
|
|
||||||
|
|
||||||
hr
|
|
||||||
|
|
||||||
.row
|
.row
|
||||||
.col-sm-3.col-sm-offset-3
|
.col-md-6
|
||||||
button#create-new-class-btn.btn.btn-default.btn-block Create New Class
|
+course-info(course)
|
||||||
.col-sm-3
|
if i < courses.length
|
||||||
input#new-classroom-name-input.form-control(placeholder='new class name')
|
- course = courses[i];
|
||||||
|
- i++;
|
||||||
#fixed-area
|
.col-md-6
|
||||||
.container
|
+course-info(course)
|
||||||
.row.well
|
|
||||||
if view.state === 'saving-changes'
|
block footer
|
||||||
p Saving changes
|
|
||||||
- var total = view.membershipAdditions.originalSize + view.usersToRedeem.originalSize;
|
mixin classroom(classroom)
|
||||||
- var left = view.membershipAdditions.size() + view.usersToRedeem.size();
|
.row
|
||||||
- var pct = Math.max(10, (100 * (total - left) / total)).toFixed(1) + '%';
|
- var classMemberCount = classroom.get('members') ? classroom.get('members').length : 0;
|
||||||
.progress.progress-striped.active
|
if classMemberCount > 0
|
||||||
.progress-bar(style="width: #{pct}")
|
.col-md-8
|
||||||
else
|
p
|
||||||
- var seatsLeft = totalMaxRedeemers - totalRedeemers - view.usersToRedeem.size();
|
span.spr.class-name= classroom.get('name')
|
||||||
if seatsLeft < 0
|
if classroom.get('aceConfig') && classroom.get('aceConfig').language === 'javascript'
|
||||||
.alert.alert-danger
|
span.spr.class-name (JavaScript)
|
||||||
span.spr You do not have enough seats to accommodate all students you have selected.
|
else
|
||||||
a(href="/courses/purchase") Buy more seats.
|
span.spr.class-name (Python)
|
||||||
else
|
a.edit-classroom-small(data-i18n="courses.edit_settings", data-classroom-id="#{classroom.id}")
|
||||||
.col-sm-2
|
.active-courses active courses
|
||||||
button#save-changes-btn.btn.btn-primary.btn-block(disabled=!view.numCourseInstancesToAddTo) Save Changes
|
- var courseInstances = view.courseInstances.where({classroomID: classroom.id});
|
||||||
.col-sm-5
|
each courseInstance in courseInstances
|
||||||
| Students to add to courses: #{view.numCourseInstancesToAddTo || 0}
|
+course(courseInstance, classMemberCount)
|
||||||
.col-sm-5
|
else
|
||||||
| Seats to expend: #{view.usersToRedeem.size()} (will have #{seatsLeft} seats left)
|
.col-md-12
|
||||||
|
p
|
||||||
block footer
|
span.spr.class-name= classroom.get('name')
|
||||||
|
if classroom.get('aceConfig') && classroom.get('aceConfig').language === 'javascript'
|
||||||
|
span.spr.class-name (JavaScript)
|
||||||
|
else
|
||||||
|
span.spr.class-name (Python)
|
||||||
|
a.edit-classroom-small(data-i18n="courses.edit_settings", data-classroom-id="#{classroom.id}")
|
||||||
|
.no-students No students yet!
|
||||||
|
.text-center
|
||||||
|
button.btn.btn-info.uppercase.btn-add-students(data-classroom-id="#{classroom.id}") add students
|
||||||
|
br
|
||||||
|
if classMemberCount > 0
|
||||||
|
.col-md-4.text-center
|
||||||
|
.class-count= classMemberCount
|
||||||
|
.active-courses(style='margin:6px;') students
|
||||||
|
a.btn.btn-info.uppercase(href='/courses/#{classroom.id}') view/edit
|
||||||
|
.row
|
||||||
|
.col-md-12
|
||||||
|
.divider
|
||||||
|
|
||||||
|
mixin course(courseInstance, classMemberCount)
|
||||||
|
- var courseMemberCount = courseInstance.get('members') ? courseInstance.get('members').length : 0;
|
||||||
|
if courseMemberCount > 0
|
||||||
|
- var course = view.courses.get(courseInstance.get('courseID'));
|
||||||
|
p
|
||||||
|
.course-name= course.get('name')
|
||||||
|
.course-enrolled #{courseMemberCount} / #{classMemberCount} students enrolled
|
||||||
|
each concept in course.get('concepts')
|
||||||
|
span.spr.course-concept(data-i18n="concepts." + concept)
|
||||||
|
|
||||||
|
mixin course-info(course)
|
||||||
|
.media
|
||||||
|
img.media-object(src=course.get('screenshot'))
|
||||||
|
.media-body
|
||||||
|
span.spr.course-name= course.get('name')
|
||||||
|
p= course.get('description')
|
||||||
|
p
|
||||||
|
strong.spr Concepts:
|
||||||
|
each concept in course.get('concepts')
|
||||||
|
span(data-i18n="concepts." + concept)
|
||||||
|
if course.get('concepts').indexOf(concept) !== course.get('concepts').length - 1
|
||||||
|
span.spr ,
|
||||||
|
p
|
||||||
|
strong.spr Length:
|
||||||
|
span #{course.get('duration') || 0} hours
|
||||||
|
|
|
@ -28,7 +28,7 @@ block outer_content
|
||||||
#hour-of-code
|
#hour-of-code
|
||||||
h1(data-i18n="home.hoc_title")
|
h1(data-i18n="home.hoc_title")
|
||||||
div
|
div
|
||||||
a.btn.btn-illustrated.btn-lg.btn-primary.btn-class-code(href='/courses/students', data-i18n="home.hoc_class_code")
|
a.btn.btn-illustrated.btn-lg.btn-primary.btn-class-code(href='/courses', data-i18n="home.hoc_class_code")
|
||||||
a.btn.btn-illustrated.btn-lg.btn-success.btn-enter(href='/hoc', data-i18n="home.hoc_enter")
|
a.btn.btn-illustrated.btn-lg.btn-success.btn-enter(href='/hoc', data-i18n="home.hoc_enter")
|
||||||
|
|
||||||
//- #slogan(data-i18n="home.slogan")
|
//- #slogan(data-i18n="home.slogan")
|
||||||
|
|
|
@ -12,10 +12,11 @@ block modal-body-content
|
||||||
#victory-text= victoryText
|
#victory-text= victoryText
|
||||||
|
|
||||||
if isCourseLevel
|
if isCourseLevel
|
||||||
if currentCourseName
|
.course-name-container
|
||||||
p
|
if currentCourseName
|
||||||
span.spr.level-title(data-i18n="play_level.course")
|
p
|
||||||
span.level-name= currentCourseName
|
span.spr.level-title(data-i18n="play_level.course")
|
||||||
|
span.level-name= currentCourseName
|
||||||
.container-fluid
|
.container-fluid
|
||||||
.row
|
.row
|
||||||
.col-md-6
|
.col-md-6
|
||||||
|
@ -26,6 +27,10 @@ block modal-body-content
|
||||||
if nextLevelName
|
if nextLevelName
|
||||||
.level-title(data-i18n="play_level.next_level")
|
.level-title(data-i18n="play_level.next_level")
|
||||||
.level-name= nextLevelName.replace('Course: ', '')
|
.level-name= nextLevelName.replace('Course: ', '')
|
||||||
|
else
|
||||||
|
.level-title(data-i18n="play_level.course")
|
||||||
|
.level-name(data-i18n="play_level.victory_title_suffix")
|
||||||
|
|
||||||
br
|
br
|
||||||
|
|
||||||
#level-feedback
|
#level-feedback
|
||||||
|
|
|
@ -1,35 +1,43 @@
|
||||||
extends /templates/base
|
extends /templates/base
|
||||||
|
|
||||||
block content
|
block content
|
||||||
|
|
||||||
h2 Hour of Code(Combat)
|
|
||||||
p
|
|
||||||
strong Hi Teachers!
|
|
||||||
p We're excited to participate in Hour of Code this year!
|
|
||||||
p
|
|
||||||
span.spr Navigate to the
|
|
||||||
a(href='/courses/teachers?hoc=true') CodeCombat Courses
|
|
||||||
span.spl page to get started.
|
|
||||||
p
|
|
||||||
span.spr If you have any problems, please email
|
|
||||||
a(href='mailto:team@codecombat.com') team@codecombat.com
|
|
||||||
br
|
|
||||||
|
|
||||||
h2(data-i18n="teachers.more_info")
|
|
||||||
p(data-i18n="teachers.intro_1")
|
|
||||||
p(data-i18n="teachers.intro_2")
|
|
||||||
|
|
||||||
h3(data-i18n="teachers.free_title")
|
p.bigger-text
|
||||||
p
|
strong Teachers
|
||||||
span.spr(data-i18n="teachers.free_3")
|
span - share CodeCombat's Hour of Code with your class!
|
||||||
a(href='/courses', data-i18n="teachers.free_4")
|
|
||||||
span(data-i18n="teachers.free_5")
|
.container-fluid
|
||||||
p(data-i18n="teachers.free_6")
|
.row
|
||||||
p
|
.col-md-6
|
||||||
span.spr For more details, please email
|
.bigger-text
|
||||||
a(href='mailto:team@codecombat.com') team@codecombat.com
|
div In just one hour, students will learn:
|
||||||
|
ul
|
||||||
h3.teachers-title(data-i18n="teachers.teacher_subs_title")
|
li basic Python syntex
|
||||||
|
li arguments
|
||||||
|
li strings
|
||||||
|
li while loops
|
||||||
|
li variables
|
||||||
|
.col-md-5
|
||||||
|
.well
|
||||||
|
strong.uppercase These easy steps to get started:
|
||||||
|
ol
|
||||||
|
if me.isAnonymous()
|
||||||
|
li
|
||||||
|
a.spr.link-register Register
|
||||||
|
span for a free teacher account to manage classes and monitor student progress
|
||||||
|
else
|
||||||
|
li Register (you've already done this)
|
||||||
|
li
|
||||||
|
a.spr(href='/courses/teachers') Create a class
|
||||||
|
span and invite students via email, unique passcode, or URL.
|
||||||
|
|
||||||
|
if me.isAnonymous()
|
||||||
|
.text-center
|
||||||
|
button.btn.btn-lg.btn-success.uppercase.btn-create-account Create Teacher account
|
||||||
|
.text-center
|
||||||
|
button.btn.btn-lg.btn-primary.uppercase.btn-login-account Log into Teacher Account
|
||||||
|
|
||||||
|
h2 Free Trial for Teachers!
|
||||||
p
|
p
|
||||||
strong.spr Hour of Code Special!
|
strong.spr Hour of Code Special!
|
||||||
span Complete the survey by December 31st and enroll all your students in the paid courses for 2 months.
|
span Complete the survey by December 31st and enroll all your students in the paid courses for 2 months.
|
||||||
|
@ -39,23 +47,28 @@ block content
|
||||||
a(href='/teachers/freetrial', data-i18n="teachers.teacher_subs_2")
|
a(href='/teachers/freetrial', data-i18n="teachers.teacher_subs_2")
|
||||||
span.spl(data-i18n="teachers.teacher_subs_3")
|
span.spl(data-i18n="teachers.teacher_subs_3")
|
||||||
|
|
||||||
|
h2 FAQ
|
||||||
|
|
||||||
|
h3(data-i18n="teachers.who_for_title")
|
||||||
|
p(data-i18n="teachers.who_for_1")
|
||||||
|
p(data-i18n="teachers.who_for_2")
|
||||||
|
|
||||||
|
h3 Does it cost anything to play Hour of Code?
|
||||||
|
p No! The first hour of CodeCombat is completely free.
|
||||||
|
p Teachers, please see the free trial information above for further details.
|
||||||
|
|
||||||
|
//- h3 How much are the paid courses?
|
||||||
|
//- p
|
||||||
|
//- span.spr(data-i18n="teachers.free_3")
|
||||||
|
//- a(href='/courses', data-i18n="teachers.free_4")
|
||||||
|
//- span(data-i18n="teachers.free_5")
|
||||||
|
//- p(data-i18n="teachers.free_6")
|
||||||
|
//- p
|
||||||
|
//- span.spr For more details, please email
|
||||||
|
//- a(href='mailto:team@codecombat.com') team@codecombat.com
|
||||||
|
|
||||||
h3(data-i18n="teachers.more_info_title")
|
h3(data-i18n="teachers.more_info_title")
|
||||||
p
|
p
|
||||||
span.spr(data-i18n="teachers.more_info_1")
|
span.spr(data-i18n="teachers.more_info_1")
|
||||||
a(href='http://discourse.codecombat.com/c/teachers', data-i18n="teachers.more_info_2")
|
a(href='http://discourse.codecombat.com/c/teachers', data-i18n="teachers.more_info_2")
|
||||||
span.spl(data-i18n="teachers.more_info_3")
|
span.spl(data-i18n="teachers.more_info_3")
|
||||||
|
|
||||||
h3(data-i18n="teachers.sys_requirements_title")
|
|
||||||
p(data-i18n="teachers.sys_requirements_1")
|
|
||||||
p(data-i18n="teachers.sys_requirements_2")
|
|
||||||
|
|
||||||
if (me.get('preferredLanguage', true) || 'en-US') == 'pl'
|
|
||||||
h3 Fundacja IT Leader Club Polska
|
|
||||||
p
|
|
||||||
| CodeCombat w marcu 2015 roku nawiązał współpracę z Fundacją IT Leader Club Polska celem promowania nauki programowania wśród najmłodszych w Polsce. Język programowania stał się drugim językiem, bez którego bardzo trudno będzie młodym ludziom w przyszłości zaistnieć na konkurencyjnym rynku pracy.
|
|
||||||
p
|
|
||||||
| Poznając podstawy języka programowania z Fundacją IT Leader Club Polska z wykorzystaniem CodeCombat, polskie dzieci będą bardzo szybko potrafiły poznać kluczowe zagadnienia programowania. Poprzez zabawę i interaktywną grę, dzieci szybko przyswoją niezbędną teorię i zaczną czerpać z programowania nie tylko wiedzę ale i pasję na całe życie - czego życzymy wszystkim naszym młodym studentom. W przypadku pytań prosimy o kontakt z Panem Arkadiuszem Lefanowiczem, Przewodniczącym Fundacji IT Leader Club Polska,
|
|
||||||
a(href="http://www.itleader.org.pl") itleader.org.pl
|
|
||||||
| ,
|
|
||||||
a(href="mailto:arkadiusz.lefanowicz@itleader.org.pl") arkadiusz.lefanowicz@itleader.org.pl
|
|
||||||
| .
|
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
|
AuthModal = require 'views/core/AuthModal'
|
||||||
RootView = require 'views/core/RootView'
|
RootView = require 'views/core/RootView'
|
||||||
template = require 'templates/teachers'
|
template = require 'templates/teachers'
|
||||||
|
|
||||||
module.exports = class TeachersView extends RootView
|
module.exports = class TeachersView extends RootView
|
||||||
id: 'teachers-view'
|
id: 'teachers-view'
|
||||||
template: template
|
template: template
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .btn-create-account': 'onClickSignup'
|
||||||
|
'click .btn-login-account': 'onClickLogin'
|
||||||
|
'click .link-register': 'onClickSignup'
|
||||||
|
|
||||||
|
onClickLogin: (e) ->
|
||||||
|
@openModalView new AuthModal(mode: 'login') if me.get('anonymous')
|
||||||
|
|
||||||
|
onClickSignup: (e) ->
|
||||||
|
@openModalView new AuthModal() if me.get('anonymous')
|
||||||
|
|
97
app/views/courses/ActivateLicensesModal.coffee
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
ModalView = require 'views/core/ModalView'
|
||||||
|
template = require 'templates/courses/activate-licenses-modal'
|
||||||
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
|
Prepaid = require 'models/Prepaid'
|
||||||
|
User = require 'models/User'
|
||||||
|
|
||||||
|
module.exports = class ActivateLicensesModal extends ModalView
|
||||||
|
id: 'activate-licenses-modal'
|
||||||
|
template: template
|
||||||
|
|
||||||
|
events:
|
||||||
|
'change input': 'updateSelectionSpans'
|
||||||
|
'submit form': 'onSubmitForm'
|
||||||
|
|
||||||
|
initialize: (options) ->
|
||||||
|
@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.comparator = '_id'
|
||||||
|
@supermodel.loadCollection(@prepaids, 'prepaids', {data: {creator: me.id}})
|
||||||
|
|
||||||
|
afterRender: ->
|
||||||
|
super()
|
||||||
|
@updateSelectionSpans()
|
||||||
|
|
||||||
|
updateSelectionSpans: ->
|
||||||
|
targets = @$('input[name="targets"]:checked').val()
|
||||||
|
if targets is 'given'
|
||||||
|
numToActivate = 1
|
||||||
|
else
|
||||||
|
numToActivate = @$('input[name="user"]:checked:not(:disabled)').length
|
||||||
|
@$('#total-selected-span').text(numToActivate)
|
||||||
|
remaining = @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() - numToActivate
|
||||||
|
@$('#licenses-remaining-span').text(remaining)
|
||||||
|
depleted = remaining < 0
|
||||||
|
@$('#not-depleted-span').toggleClass('hide', depleted)
|
||||||
|
@$('#depleted-span').toggleClass('hide', !depleted)
|
||||||
|
@$('#activate-licenses-btn').toggleClass('disabled', depleted)
|
||||||
|
|
||||||
|
showProgress: ->
|
||||||
|
@$('#submit-form-area').addClass('hide')
|
||||||
|
@$('#progress-area').removeClass('hide')
|
||||||
|
|
||||||
|
hideProgress: ->
|
||||||
|
@$('#submit-form-area').removeClass('hide')
|
||||||
|
@$('#progress-area').addClass('hide')
|
||||||
|
|
||||||
|
onSubmitForm: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
@$('#error-alert').addClass('hide')
|
||||||
|
@usersToRedeem = new CocoCollection([], {model: User})
|
||||||
|
targets = @$('input[name="targets"]:checked').val()
|
||||||
|
if targets is 'given'
|
||||||
|
@usersToRedeem.add(@user)
|
||||||
|
else
|
||||||
|
checkedBoxes = @$('input[name="user"]:checked:not(:disabled)')
|
||||||
|
_.each checkedBoxes, (el) =>
|
||||||
|
$el = $(el)
|
||||||
|
userID = $el.data('user-id')
|
||||||
|
@usersToRedeem.add @users.get(userID)
|
||||||
|
return unless @usersToRedeem.size()
|
||||||
|
@usersToRedeem.originalSize = @usersToRedeem.size()
|
||||||
|
@showProgress()
|
||||||
|
@redeemUsers()
|
||||||
|
|
||||||
|
redeemUsers: ->
|
||||||
|
if not @usersToRedeem.size()
|
||||||
|
@finishRedeemUsers()
|
||||||
|
return
|
||||||
|
|
||||||
|
user = @usersToRedeem.first()
|
||||||
|
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots())
|
||||||
|
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots()) unless prepaid
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST'
|
||||||
|
url: _.result(prepaid, 'url') + '/redeemers'
|
||||||
|
data: { userID: user.id }
|
||||||
|
context: @
|
||||||
|
success: ->
|
||||||
|
@usersToRedeem.remove(user)
|
||||||
|
pct = 100 * (@usersToRedeem.originalSize - @usersToRedeem.size() / @usersToRedeem.originalSize)
|
||||||
|
@$('#progress-area .progress-bar').css('width', "#{pct.toFixed(1)}%")
|
||||||
|
@redeemUsers()
|
||||||
|
error: (jqxhr, textStatus, errorThrown) ->
|
||||||
|
if jqxhr.status is 402
|
||||||
|
message = arguments[2]
|
||||||
|
else
|
||||||
|
message = "#{jqxhr.status}: #{jqxhr.responseText}"
|
||||||
|
@$('#error-alert').text(message).removeClass('hide')
|
||||||
|
})
|
||||||
|
|
||||||
|
finishRedeemUsers: ->
|
||||||
|
@trigger 'redeem-users'
|
26
app/views/courses/ChangeCourseLanguageModal.coffee
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
ModalView = require 'views/core/ModalView'
|
||||||
|
template = require 'templates/courses/change-course-language-modal'
|
||||||
|
|
||||||
|
module.exports = class ChangeCourseLanguageModal extends ModalView
|
||||||
|
id: 'change-course-language-modal'
|
||||||
|
template: template
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .lang-choice-btn': 'onClickLanguageChoiceButton'
|
||||||
|
|
||||||
|
onClickLanguageChoiceButton: (e) ->
|
||||||
|
@chosenLanguage = $(e.target).data('language')
|
||||||
|
aceConfig = _.clone(me.get('aceConfig') or {})
|
||||||
|
aceConfig.language = @chosenLanguage
|
||||||
|
me.set('aceConfig', aceConfig)
|
||||||
|
res = me.patch()
|
||||||
|
if res
|
||||||
|
@$('#choice-area').hide()
|
||||||
|
@$('#saving-progress').removeClass('hide')
|
||||||
|
@listenToOnce me, 'sync', @onLanguageSettingSaved
|
||||||
|
else
|
||||||
|
@onLanguageSettingSaved()
|
||||||
|
|
||||||
|
onLanguageSettingSaved: ->
|
||||||
|
@trigger('set-language')
|
||||||
|
@hide()
|
51
app/views/courses/ChooseLanguageModal.coffee
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
ModalView = require 'views/core/ModalView'
|
||||||
|
template = require 'templates/courses/choose-language-modal'
|
||||||
|
|
||||||
|
module.exports = class ChooseLanguageModal extends ModalView
|
||||||
|
id: 'choose-language-modal'
|
||||||
|
template: template
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .lang-choice-btn': 'onClickLanguageChoiceButton'
|
||||||
|
|
||||||
|
initialize: (options) ->
|
||||||
|
options ?= {}
|
||||||
|
@logoutFirst = options.logoutFirst
|
||||||
|
|
||||||
|
onClickLanguageChoiceButton: (e) ->
|
||||||
|
@chosenLanguage = $(e.target).data('language')
|
||||||
|
if @logoutFirst
|
||||||
|
@logoutUser()
|
||||||
|
else
|
||||||
|
@saveLanguageSetting()
|
||||||
|
|
||||||
|
logoutUser: ->
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST'
|
||||||
|
url: '/auth/logout'
|
||||||
|
context: @
|
||||||
|
success: @onUserLoggedOut
|
||||||
|
})
|
||||||
|
|
||||||
|
onUserLoggedOut: ->
|
||||||
|
me.clear()
|
||||||
|
me.fetch({
|
||||||
|
url: '/auth/whoami'
|
||||||
|
})
|
||||||
|
@listenToOnce me, 'sync', @saveLanguageSetting
|
||||||
|
|
||||||
|
saveLanguageSetting: ->
|
||||||
|
aceConfig = _.clone(me.get('aceConfig') or {})
|
||||||
|
aceConfig.language = @chosenLanguage
|
||||||
|
me.set('aceConfig', aceConfig)
|
||||||
|
res = me.patch()
|
||||||
|
if res
|
||||||
|
@$('#choice-area').hide()
|
||||||
|
@$('#saving-progress').removeClass('hide')
|
||||||
|
@listenToOnce me, 'sync', @onLanguageSettingSaved
|
||||||
|
else
|
||||||
|
@onLanguageSettingSaved()
|
||||||
|
|
||||||
|
onLanguageSettingSaved: ->
|
||||||
|
@trigger('set-language')
|
||||||
|
@hide()
|
|
@ -1,10 +1,11 @@
|
||||||
|
Classroom = require 'models/Classroom'
|
||||||
ModalView = require 'views/core/ModalView'
|
ModalView = require 'views/core/ModalView'
|
||||||
template = require 'templates/courses/classroom-settings-modal'
|
template = require 'templates/courses/classroom-settings-modal'
|
||||||
|
|
||||||
module.exports = class AddLevelSystemModal extends ModalView
|
module.exports = class AddLevelSystemModal extends ModalView
|
||||||
id: 'classroom-settings-modal'
|
id: 'classroom-settings-modal'
|
||||||
template: template
|
template: template
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click #save-settings-btn': 'onClickSaveSettingsButton'
|
'click #save-settings-btn': 'onClickSaveSettingsButton'
|
||||||
|
|
||||||
|
@ -12,15 +13,16 @@ module.exports = class AddLevelSystemModal extends ModalView
|
||||||
@classroom = options.classroom
|
@classroom = options.classroom
|
||||||
|
|
||||||
onClickSaveSettingsButton: ->
|
onClickSaveSettingsButton: ->
|
||||||
return unless @classroom
|
name = $('.settings-name-input').val()
|
||||||
if name = $('.settings-name-input').val()
|
unless @classroom
|
||||||
|
return unless name
|
||||||
|
@classroom = new Classroom({ name: name })
|
||||||
|
if name
|
||||||
@classroom.set('name', name)
|
@classroom.set('name', name)
|
||||||
description = $('.settings-description-input').val()
|
description = $('.settings-description-input').val()
|
||||||
@classroom.set('description', description)
|
@classroom.set('description', description)
|
||||||
@classroom.set('aceConfig', {
|
@classroom.set('aceConfig', {
|
||||||
language: @$('#programming-language-select').val()
|
language: @$('#programming-language-select').val()
|
||||||
})
|
})
|
||||||
@classroom.patch()
|
@classroom.save()
|
||||||
@hide()
|
@hide()
|
||||||
|
|
||||||
|
|
126
app/views/courses/ClassroomView.coffee
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
Campaign = require 'models/Campaign'
|
||||||
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
|
Course = require 'models/Course'
|
||||||
|
CourseInstance = require 'models/CourseInstance'
|
||||||
|
Classroom = require 'models/Classroom'
|
||||||
|
LevelSession = require 'models/LevelSession'
|
||||||
|
RootView = require 'views/core/RootView'
|
||||||
|
template = require 'templates/courses/classroom-view'
|
||||||
|
User = require 'models/User'
|
||||||
|
utils = require 'core/utils'
|
||||||
|
Prepaid = require 'models/Prepaid'
|
||||||
|
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
||||||
|
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
||||||
|
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
||||||
|
RemoveStudentModal = require 'views/courses/RemoveStudentModal'
|
||||||
|
|
||||||
|
module.exports = class ClassroomView extends RootView
|
||||||
|
id: 'classroom-view'
|
||||||
|
template: template
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click #edit-class-details-link': 'onClickEditClassDetailsLink'
|
||||||
|
'click #activate-licenses-btn': 'onClickActivateLicensesButton'
|
||||||
|
'click .activate-single-license-btn': 'onClickActivateSingleLicenseButton'
|
||||||
|
'click #add-students-btn': 'onClickAddStudentsButton'
|
||||||
|
'click .enable-btn': 'onClickEnableButton'
|
||||||
|
'click .remove-student-link': 'onClickRemoveStudentLink'
|
||||||
|
|
||||||
|
initialize: (options, classroomID) ->
|
||||||
|
@classroom = new Classroom({_id: classroomID})
|
||||||
|
@supermodel.loadModel @classroom, 'classroom'
|
||||||
|
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
||||||
|
@courses.comparator = '_id'
|
||||||
|
@supermodel.loadCollection(@courses, 'courses')
|
||||||
|
@campaigns = new CocoCollection([], { url: "/db/campaign", model: Campaign })
|
||||||
|
@courses.comparator = '_id'
|
||||||
|
@supermodel.loadCollection(@campaigns, 'campaigns', { data: { type: 'course' }})
|
||||||
|
@courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance})
|
||||||
|
@courseInstances.comparator = 'courseID'
|
||||||
|
@supermodel.loadCollection(@courseInstances, 'course_instances', { data: { classroomID: classroomID } })
|
||||||
|
@users = new CocoCollection([], { url: "/db/classroom/#{classroomID}/members", model: User })
|
||||||
|
@users.comparator = (user) => user.broadName().toLowerCase()
|
||||||
|
@supermodel.loadCollection(@users, 'users')
|
||||||
|
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
|
||||||
|
|
||||||
|
onCourseInstancesSync: ->
|
||||||
|
@sessions = new CocoCollection([], { model: LevelSession })
|
||||||
|
for courseInstance in @courseInstances.models
|
||||||
|
sessions = new CocoCollection([], { url: "/db/course_instance/#{courseInstance.id}/level_sessions", model: LevelSession })
|
||||||
|
@supermodel.loadCollection(sessions, 'sessions')
|
||||||
|
courseInstance.sessions = sessions
|
||||||
|
sessions.courseInstance = courseInstance
|
||||||
|
@listenToOnce sessions, 'sync', (sessions) ->
|
||||||
|
@sessions.add(sessions.slice())
|
||||||
|
sessions.courseInstance.sessionsByUser = sessions.groupBy('creator')
|
||||||
|
|
||||||
|
onLoaded: ->
|
||||||
|
userSessions = @sessions.groupBy('creator')
|
||||||
|
for user in @users.models
|
||||||
|
user.sessions = new CocoCollection(userSessions[user.id], { model: LevelSession })
|
||||||
|
user.sessions.comparator = 'changed'
|
||||||
|
user.sessions.sort()
|
||||||
|
for courseInstance in @courseInstances.models
|
||||||
|
courseID = courseInstance.get('courseID')
|
||||||
|
course = @courses.get(courseID)
|
||||||
|
campaignID = course.get('campaignID')
|
||||||
|
campaign = @campaigns.get(campaignID)
|
||||||
|
courseInstance.sessions.campaign = campaign
|
||||||
|
super()
|
||||||
|
|
||||||
|
onClickActivateLicensesButton: ->
|
||||||
|
modal = new ActivateLicensesModal({
|
||||||
|
classroom: @classroom
|
||||||
|
users: @users
|
||||||
|
})
|
||||||
|
@openModalView(modal)
|
||||||
|
modal.once 'redeem-users', -> document.location.reload()
|
||||||
|
|
||||||
|
onClickActivateSingleLicenseButton: (e) ->
|
||||||
|
userID = $(e.target).data('user-id')
|
||||||
|
user = @users.get(userID)
|
||||||
|
modal = new ActivateLicensesModal({
|
||||||
|
classroom: @classroom
|
||||||
|
users: @users
|
||||||
|
user: user
|
||||||
|
})
|
||||||
|
@openModalView(modal)
|
||||||
|
modal.once 'redeem-users', -> document.location.reload()
|
||||||
|
|
||||||
|
onClickEditClassDetailsLink: ->
|
||||||
|
modal = new ClassroomSettingsModal({classroom: @classroom})
|
||||||
|
@openModalView(modal)
|
||||||
|
@listenToOnce modal, 'hidden', @render
|
||||||
|
|
||||||
|
makeLastPlayedString: (user) ->
|
||||||
|
session = user.sessions.last()
|
||||||
|
return '' if not session
|
||||||
|
campaign = session.collection.campaign
|
||||||
|
levelOriginal = session.get('level').original
|
||||||
|
campaignLevel = campaign.get('levels')[levelOriginal]
|
||||||
|
return "#{campaign.get('fullName')}, #{campaignLevel.name}"
|
||||||
|
|
||||||
|
onClickAddStudentsButton: (e) ->
|
||||||
|
modal = new InviteToClassroomModal({classroom: @classroom})
|
||||||
|
@openModalView(modal)
|
||||||
|
|
||||||
|
onClickEnableButton: (e) ->
|
||||||
|
courseInstance = @courseInstances.get($(e.target).data('course-instance-id'))
|
||||||
|
userID = $(e.target).data('user-id')
|
||||||
|
courseInstance.addMember(userID)
|
||||||
|
$(e.target).attr('disabled', true)
|
||||||
|
@listenToOnce courseInstance, 'sync', @render
|
||||||
|
|
||||||
|
onClickRemoveStudentLink: (e) ->
|
||||||
|
user = @users.get($(e.target).closest('a').data('user-id'))
|
||||||
|
modal = new RemoveStudentModal({
|
||||||
|
classroom: @classroom
|
||||||
|
user: user
|
||||||
|
courseInstances: @courseInstances
|
||||||
|
})
|
||||||
|
@openModalView(modal)
|
||||||
|
modal.once 'remove-student', @onStudentRemoved, @
|
||||||
|
|
||||||
|
onStudentRemoved: (e) ->
|
||||||
|
@users.remove(e.user)
|
||||||
|
@render()
|
|
@ -9,12 +9,16 @@ template = require 'templates/courses/course-details'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
utils = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
Prepaid = require 'models/Prepaid'
|
Prepaid = require 'models/Prepaid'
|
||||||
|
storage = require 'core/storage'
|
||||||
|
|
||||||
autoplayedOnce = false
|
autoplayedOnce = false
|
||||||
|
|
||||||
module.exports = class CourseDetailsView extends RootView
|
module.exports = class CourseDetailsView extends RootView
|
||||||
id: 'course-details-view'
|
id: 'course-details-view'
|
||||||
template: template
|
template: template
|
||||||
|
teacherMode: false
|
||||||
|
singlePlayerMode: false
|
||||||
|
memberSort: 'nameAsc'
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'change .progress-expand-checkbox': 'onCheckExpandedProgress'
|
'change .progress-expand-checkbox': 'onCheckExpandedProgress'
|
||||||
|
@ -25,14 +29,13 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
'click .progress-level-cell': 'onClickProgressLevelCell'
|
'click .progress-level-cell': 'onClickProgressLevelCell'
|
||||||
'mouseenter .progress-level-cell': 'onMouseEnterPoint'
|
'mouseenter .progress-level-cell': 'onMouseEnterPoint'
|
||||||
'mouseleave .progress-level-cell': 'onMouseLeavePoint'
|
'mouseleave .progress-level-cell': 'onMouseLeavePoint'
|
||||||
|
'submit #school-form': 'onSubmitSchoolForm'
|
||||||
|
|
||||||
constructor: (options, @courseID, @courseInstanceID) ->
|
constructor: (options, @courseID, @courseInstanceID) ->
|
||||||
super options
|
super options
|
||||||
@courseID ?= options.courseID
|
@courseID ?= options.courseID
|
||||||
@courseInstanceID ?= options.courseInstanceID
|
@courseInstanceID ?= options.courseInstanceID
|
||||||
@classroom = new Classroom()
|
@classroom = new Classroom()
|
||||||
@adminMode = me.isAdmin()
|
|
||||||
@memberSort = 'nameAsc'
|
|
||||||
@course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID
|
@course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID
|
||||||
@listenTo @course, 'sync', @onCourseSync
|
@listenTo @course, 'sync', @onCourseSync
|
||||||
@prepaid = new Prepaid()
|
@prepaid = new Prepaid()
|
||||||
|
@ -43,7 +46,6 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
|
|
||||||
getRenderData: ->
|
getRenderData: ->
|
||||||
context = super()
|
context = super()
|
||||||
context.adminMode = @adminMode ? false
|
|
||||||
context.campaign = @campaign
|
context.campaign = @campaign
|
||||||
context.conceptsCompleted = @conceptsCompleted ? {}
|
context.conceptsCompleted = @conceptsCompleted ? {}
|
||||||
context.course = @course if @course?.loaded
|
context.course = @course if @course?.loaded
|
||||||
|
@ -62,13 +64,22 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
context.userConceptStateMap = @userConceptStateMap ? {}
|
context.userConceptStateMap = @userConceptStateMap ? {}
|
||||||
context.userLevelStateMap = @userLevelStateMap ? {}
|
context.userLevelStateMap = @userLevelStateMap ? {}
|
||||||
context.document = document
|
context.document = document
|
||||||
|
context.promptForSchool = @courseComplete and not me.isAnonymous() and not me.get('schoolName') and not storage.load('no-school')
|
||||||
context
|
context
|
||||||
|
|
||||||
|
afterRender: ->
|
||||||
|
super()
|
||||||
|
if @supermodel.finished() and @courseComplete and me.isAnonymous() and @options.justBeatLevel
|
||||||
|
# TODO: Make an intermediate modal that tells them they've finished HoC and has some snazzy stuff for convincing players to sign up instead of just throwing up the bare AuthModal
|
||||||
|
AuthModal = require 'views/core/AuthModal'
|
||||||
|
@openModalView new AuthModal showSignupRationale: true
|
||||||
|
|
||||||
onCourseSync: ->
|
onCourseSync: ->
|
||||||
|
return if @destroyed
|
||||||
# console.log 'onCourseSync'
|
# console.log 'onCourseSync'
|
||||||
if me.isAnonymous() and (not me.get('hourOfCode') and not @course.get('hourOfCode'))
|
if me.isAnonymous() and (not me.get('hourOfCode') and not @course.get('hourOfCode'))
|
||||||
@noCourseInstance = true
|
@noCourseInstance = true
|
||||||
@render?()
|
@render()
|
||||||
return
|
return
|
||||||
return if @campaign?
|
return if @campaign?
|
||||||
campaignID = @course.get('campaignID')
|
campaignID = @course.get('campaignID')
|
||||||
|
@ -78,24 +89,36 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
@onCampaignSync()
|
@onCampaignSync()
|
||||||
else
|
else
|
||||||
@supermodel.loadModel @campaign, 'campaign'
|
@supermodel.loadModel @campaign, 'campaign'
|
||||||
@render?()
|
@render()
|
||||||
|
|
||||||
onCampaignSync: ->
|
onCampaignSync: ->
|
||||||
|
return if @destroyed
|
||||||
# console.log 'onCampaignSync'
|
# console.log 'onCampaignSync'
|
||||||
if @courseInstanceID
|
if @courseInstanceID
|
||||||
@loadCourseInstance(@courseInstanceID)
|
@loadCourseInstance(@courseInstanceID)
|
||||||
else unless me.isAnonymous()
|
else unless me.isAnonymous()
|
||||||
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
@loadCourseInstances()
|
||||||
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
|
|
||||||
@supermodel.loadCollection(@courseInstances, 'course_instances')
|
|
||||||
@levelConceptMap = {}
|
@levelConceptMap = {}
|
||||||
for levelID, level of @campaign.get('levels')
|
for levelID, level of @campaign.get('levels')
|
||||||
@levelConceptMap[levelID] ?= {}
|
@levelConceptMap[levelID] ?= {}
|
||||||
for concept in level.concepts
|
for concept in level.concepts
|
||||||
@levelConceptMap[levelID][concept] = true
|
@levelConceptMap[levelID][concept] = true
|
||||||
@render?()
|
if level.type is 'course-ladder'
|
||||||
|
@arenaLevel = level
|
||||||
|
@render()
|
||||||
|
|
||||||
|
loadCourseInstances: ->
|
||||||
|
@courseInstances = new CocoCollection [], {url: "/db/user/#{me.id}/course_instances", model: CourseInstance, comparator: 'courseID'}
|
||||||
|
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
|
||||||
|
@supermodel.loadCollection @courseInstances, 'course_instances'
|
||||||
|
|
||||||
|
loadAllCourses: ->
|
||||||
|
@allCourses = new CocoCollection [], {url: "/db/course", model: Course, comparator: '_id'}
|
||||||
|
@listenToOnce @allCourses, 'sync', @onAllCoursesSync
|
||||||
|
@supermodel.loadCollection @allCourses, 'courses'
|
||||||
|
|
||||||
loadCourseInstance: (courseInstanceID) ->
|
loadCourseInstance: (courseInstanceID) ->
|
||||||
|
return if @destroyed
|
||||||
# console.log 'loadCourseInstance'
|
# console.log 'loadCourseInstance'
|
||||||
return if @courseInstance?
|
return if @courseInstance?
|
||||||
@courseInstanceID = courseInstanceID
|
@courseInstanceID = courseInstanceID
|
||||||
|
@ -107,23 +130,29 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
@courseInstance = @supermodel.loadModel(@courseInstance, 'course_instance').model
|
@courseInstance = @supermodel.loadModel(@courseInstance, 'course_instance').model
|
||||||
|
|
||||||
onCourseInstancesSync: ->
|
onCourseInstancesSync: ->
|
||||||
|
return if @destroyed
|
||||||
# console.log 'onCourseInstancesSync'
|
# console.log 'onCourseInstancesSync'
|
||||||
if @courseInstances.models.length is 1
|
@findNextCourseInstance()
|
||||||
@loadCourseInstance(@courseInstances.models[0].id)
|
if not @courseInstance
|
||||||
else
|
# We are loading these to find the one we want to display.
|
||||||
if @courseInstances.models.length is 0
|
if @courseInstances.models.length is 1
|
||||||
@noCourseInstance = true
|
@loadCourseInstance(@courseInstances.models[0].id)
|
||||||
else
|
else
|
||||||
@noCourseInstanceSelected = true
|
if @courseInstances.models.length is 0
|
||||||
@render?()
|
@noCourseInstance = true
|
||||||
|
else
|
||||||
|
@noCourseInstanceSelected = true
|
||||||
|
@render()
|
||||||
|
|
||||||
onCourseInstanceSync: ->
|
onCourseInstanceSync: ->
|
||||||
|
return if @destroyed
|
||||||
# console.log 'onCourseInstanceSync'
|
# console.log 'onCourseInstanceSync'
|
||||||
if @courseInstance.get('classroomID')
|
if @courseInstance.get('classroomID')
|
||||||
@classroom = new Classroom({_id: @courseInstance.get('classroomID')})
|
@classroom = new Classroom({_id: @courseInstance.get('classroomID')})
|
||||||
@supermodel.loadModel @classroom, 'classroom'
|
@supermodel.loadModel @classroom, 'classroom'
|
||||||
@adminMode = true if @courseInstance.get('ownerID') is me.id and @courseInstance.get('name') isnt 'Single Player'
|
@singlePlayerMode = @courseInstance.get('name') is 'Single Player'
|
||||||
@levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator:'_id' })
|
@teacherMode = @courseInstance.get('ownerID') is me.id and not @singlePlayerMode
|
||||||
|
@levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator: '_id' })
|
||||||
@listenToOnce @levelSessions, 'sync', @onLevelSessionsSync
|
@listenToOnce @levelSessions, 'sync', @onLevelSessionsSync
|
||||||
@supermodel.loadCollection @levelSessions, 'level_sessions', cache: false
|
@supermodel.loadCollection @levelSessions, 'level_sessions', cache: false
|
||||||
@members = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/members", model: User, comparator: 'nameLower' })
|
@members = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/members", model: User, comparator: 'nameLower' })
|
||||||
|
@ -131,19 +160,22 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
@supermodel.loadCollection @members, 'members', cache: false
|
@supermodel.loadCollection @members, 'members', cache: false
|
||||||
@owner = new User({_id: @courseInstance.get('ownerID')})
|
@owner = new User({_id: @courseInstance.get('ownerID')})
|
||||||
@supermodel.loadModel @owner, 'user'
|
@supermodel.loadModel @owner, 'user'
|
||||||
if @adminMode and prepaidID = @courseInstance.get('prepaidID')
|
if @teacherMode and prepaidID = @courseInstance.get('prepaidID')
|
||||||
@prepaid = @supermodel.getModel(Prepaid, prepaidID) or new Prepaid _id: prepaidID
|
@prepaid = @supermodel.getModel(Prepaid, prepaidID) or new Prepaid _id: prepaidID
|
||||||
@listenTo @prepaid, 'sync', @onPrepaidSync
|
@listenTo @prepaid, 'sync', @onPrepaidSync
|
||||||
if @prepaid.loaded
|
if @prepaid.loaded
|
||||||
@onPrepaidSync()
|
@onPrepaidSync()
|
||||||
else
|
else
|
||||||
@supermodel.loadModel @prepaid, 'prepaid'
|
@supermodel.loadModel @prepaid, 'prepaid'
|
||||||
@render?()
|
@render()
|
||||||
|
|
||||||
onPrepaidSync: ->
|
onPrepaidSync: ->
|
||||||
@render?()
|
return if @destroyed
|
||||||
|
# TODO: why do we rerender here? Template doesn't use prepaid.
|
||||||
|
@render()
|
||||||
|
|
||||||
onLevelSessionsSync: ->
|
onLevelSessionsSync: ->
|
||||||
|
return if @destroyed
|
||||||
# console.log 'onLevelSessionsSync'
|
# console.log 'onLevelSessionsSync'
|
||||||
@instanceStats = averageLevelsCompleted: 0, furthestLevelCompleted: '', totalLevelsCompleted: 0, totalPlayTime: 0
|
@instanceStats = averageLevelsCompleted: 0, furthestLevelCompleted: '', totalLevelsCompleted: 0, totalPlayTime: 0
|
||||||
@memberStats = {}
|
@memberStats = {}
|
||||||
|
@ -152,17 +184,28 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
@userLevelStateMap = {}
|
@userLevelStateMap = {}
|
||||||
levelStateMap = {}
|
levelStateMap = {}
|
||||||
for levelSession in @levelSessions.models
|
for levelSession in @levelSessions.models
|
||||||
|
continue if levelSession.skipMe # Don't track second arena session as another completed level
|
||||||
userID = levelSession.get('creator')
|
userID = levelSession.get('creator')
|
||||||
levelID = levelSession.get('level').original
|
levelID = levelSession.get('level').original
|
||||||
state = if levelSession.get('state')?.complete then 'complete' else 'started'
|
state = if levelSession.get('state')?.complete then 'complete' else 'started'
|
||||||
|
playtime = parseInt(levelSession.get('playtime') ? 0, 10)
|
||||||
|
do (userID, levelID) =>
|
||||||
|
secondSessionForLevel = _.find(@levelSessions.models, ((otherSession) ->
|
||||||
|
otherSession.get('creator') is userID and otherSession.get('level').original is levelID and otherSession.id isnt levelSession.id
|
||||||
|
))
|
||||||
|
if secondSessionForLevel
|
||||||
|
state = 'complete' if secondSessionForLevel.get('state')?.complete
|
||||||
|
playtime = playtime + parseInt(secondSessionForLevel.get('playtime') ? 0, 10)
|
||||||
|
secondSessionForLevel.skipMe = true
|
||||||
|
|
||||||
levelStateMap[levelID] = state
|
levelStateMap[levelID] = state
|
||||||
|
|
||||||
@instanceStats.totalLevelsCompleted++ if state is 'complete'
|
@instanceStats.totalLevelsCompleted++ if state is 'complete'
|
||||||
@instanceStats.totalPlayTime += parseInt(levelSession.get('playtime') ? 0)
|
@instanceStats.totalPlayTime += playtime
|
||||||
|
|
||||||
@memberStats[userID] ?= totalLevelsCompleted: 0, totalPlayTime: 0
|
@memberStats[userID] ?= totalLevelsCompleted: 0, totalPlayTime: 0
|
||||||
@memberStats[userID].totalLevelsCompleted++ if state is 'complete'
|
@memberStats[userID].totalLevelsCompleted++ if state is 'complete'
|
||||||
@memberStats[userID].totalPlayTime += parseInt(levelSession.get('playtime') ? 0)
|
@memberStats[userID].totalPlayTime += playtime
|
||||||
|
|
||||||
@userConceptStateMap[userID] ?= {}
|
@userConceptStateMap[userID] ?= {}
|
||||||
for concept of @levelConceptMap[levelID]
|
for concept of @levelConceptMap[levelID]
|
||||||
|
@ -185,36 +228,58 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
for concept, state of conceptStateMap
|
for concept, state of conceptStateMap
|
||||||
@conceptsCompleted[concept] ?= 0
|
@conceptsCompleted[concept] ?= 0
|
||||||
@conceptsCompleted[concept]++
|
@conceptsCompleted[concept]++
|
||||||
@render?()
|
|
||||||
|
if @memberStats[me.id]?.totalLevelsCompleted >= _.size(@campaign.get('levels')) - 1 # Don't need to complete arena
|
||||||
|
@courseComplete = true
|
||||||
|
@loadCourseInstances() unless @courseInstances # Find the next course instance to do.
|
||||||
|
|
||||||
|
@render()
|
||||||
|
|
||||||
# If we just joined a single-player course for Hour of Code, we automatically play.
|
# If we just joined a single-player course for Hour of Code, we automatically play.
|
||||||
if @instanceStats.totalLevelsCompleted is 0 and @instanceStats.totalPlayTime is 0 and @courseInstance.get('members').length is 1 and me.get('hourOfCode') and not @adminMode and not autoplayedOnce
|
if @instanceStats.totalLevelsCompleted is 0 and @instanceStats.totalPlayTime is 0 and @singlePlayerMode and not autoplayedOnce
|
||||||
autoplayedOnce = true
|
autoplayedOnce = true
|
||||||
@$el.find('button.btn-play-level').click()
|
@$el.find('button.btn-play-level').click()
|
||||||
|
|
||||||
onMembersSync: ->
|
onMembersSync: ->
|
||||||
|
return if @destroyed
|
||||||
# console.log 'onMembersSync'
|
# console.log 'onMembersSync'
|
||||||
@memberUserMap = {}
|
@memberUserMap = {}
|
||||||
for user in @members.models
|
for user in @members.models
|
||||||
@memberUserMap[user.id] = user
|
@memberUserMap[user.id] = user
|
||||||
@sortMembers()
|
@sortMembers()
|
||||||
@render?()
|
@render()
|
||||||
|
|
||||||
|
onAllCoursesSync: ->
|
||||||
|
@findNextCourseInstance()
|
||||||
|
|
||||||
|
findNextCourseInstance: ->
|
||||||
|
@nextCourseInstance = _.find @courseInstances.models, (ci) =>
|
||||||
|
# Sorted by courseID
|
||||||
|
ci.get('classroomID') is @courseInstance.get('classroomID') and ci.id isnt @courseInstance.id and ci.get('courseID') > @course.id
|
||||||
|
if @nextCourseInstance
|
||||||
|
nextCourseID = @nextCourseInstance.get('courseID')
|
||||||
|
@nextCourse = @supermodel.getModel(Course, nextCourseID) or new Course _id: nextCourseID
|
||||||
|
@nextCourse = @supermodel.loadModel(@nextCourse, 'course').model
|
||||||
|
else if @allCourses?.loaded
|
||||||
|
@nextCourse = _.find @allCourses.models, (course) => course.id > @course.id
|
||||||
|
else
|
||||||
|
@loadAllCourses()
|
||||||
|
|
||||||
onCheckExpandedProgress: (e) ->
|
onCheckExpandedProgress: (e) ->
|
||||||
@showExpandedProgress = $('.progress-expand-checkbox').prop('checked')
|
@showExpandedProgress = $('.progress-expand-checkbox').prop('checked')
|
||||||
# TODO: why does render reset the checkbox to be unchecked?
|
# TODO: why does render reset the checkbox to be unchecked?
|
||||||
@render?()
|
@render()
|
||||||
$('.progress-expand-checkbox').attr('checked', @showExpandedProgress)
|
$('.progress-expand-checkbox').attr('checked', @showExpandedProgress)
|
||||||
|
|
||||||
onClickMemberHeader: (e) ->
|
onClickMemberHeader: (e) ->
|
||||||
@memberSort = if @memberSort is 'nameAsc' then 'nameDesc' else 'nameAsc'
|
@memberSort = if @memberSort is 'nameAsc' then 'nameDesc' else 'nameAsc'
|
||||||
@sortMembers()
|
@sortMembers()
|
||||||
@render?()
|
@render()
|
||||||
|
|
||||||
onClickProgressHeader: (e) ->
|
onClickProgressHeader: (e) ->
|
||||||
@memberSort = if @memberSort is 'progressAsc' then 'progressDesc' else 'progressAsc'
|
@memberSort = if @memberSort is 'progressAsc' then 'progressDesc' else 'progressAsc'
|
||||||
@sortMembers()
|
@sortMembers()
|
||||||
@render?()
|
@render()
|
||||||
|
|
||||||
onClickPlayLevel: (e) ->
|
onClickPlayLevel: (e) ->
|
||||||
levelSlug = $(e.target).data('level-slug')
|
levelSlug = $(e.target).data('level-slug')
|
||||||
|
@ -224,7 +289,7 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
viewClass = 'views/ladder/LadderView'
|
viewClass = 'views/ladder/LadderView'
|
||||||
viewArgs = [{supermodel: @supermodel}, levelSlug]
|
viewArgs = [{supermodel: @supermodel}, levelSlug]
|
||||||
route = '/play/ladder/' + levelSlug
|
route = '/play/ladder/' + levelSlug
|
||||||
if @courseInstance.get('members').length > 1 # No league for solo courses
|
unless @singlePlayerMode # No league for solo courses
|
||||||
route += '/course/' + @courseInstance.id
|
route += '/course/' + @courseInstance.id
|
||||||
viewArgs = viewArgs.concat ['course', @courseInstance.id]
|
viewArgs = viewArgs.concat ['course', @courseInstance.id]
|
||||||
else
|
else
|
||||||
|
@ -242,7 +307,7 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
@loadCourseInstance(courseInstanceID)
|
@loadCourseInstance(courseInstanceID)
|
||||||
|
|
||||||
onClickProgressLevelCell: (e) ->
|
onClickProgressLevelCell: (e) ->
|
||||||
return unless @adminMode
|
return unless @teacherMode or me.isAdmin()
|
||||||
levelID = $(e.currentTarget).data('level-id')
|
levelID = $(e.currentTarget).data('level-id')
|
||||||
levelSlug = $(e.currentTarget).data('level-slug')
|
levelSlug = $(e.currentTarget).data('level-slug')
|
||||||
userID = $(e.currentTarget).data('user-id')
|
userID = $(e.currentTarget).data('user-id')
|
||||||
|
@ -302,8 +367,17 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
aName.localeCompare(bName)
|
aName.localeCompare(bName)
|
||||||
|
|
||||||
getOwnerName: ->
|
getOwnerName: ->
|
||||||
if @owner.isNew()
|
return if @owner.isNew()
|
||||||
return '?'
|
|
||||||
if @owner.get('firstName') and @owner.get('lastName')
|
if @owner.get('firstName') and @owner.get('lastName')
|
||||||
return "#{@owner.get('firstName')} #{@owner.get('lastName')}"
|
return "#{@owner.get('firstName')} #{@owner.get('lastName')}"
|
||||||
return @owner.get('name') or @owner.get('email') or '?'
|
@owner.get('name') or @owner.get('email')
|
||||||
|
|
||||||
|
onSubmitSchoolForm: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
schoolName = @$el.find('#course-complete-school-input').val().trim()
|
||||||
|
if schoolName and schoolName isnt me.get('schoolName')
|
||||||
|
me.set 'schoolName', schoolName
|
||||||
|
me.patch()
|
||||||
|
else
|
||||||
|
storage.save 'no-school', true
|
||||||
|
@$el.find('#school-form').slideUp('slow')
|
||||||
|
|
|
@ -2,7 +2,157 @@ app = require 'core/application'
|
||||||
AuthModal = require 'views/core/AuthModal'
|
AuthModal = require 'views/core/AuthModal'
|
||||||
RootView = require 'views/core/RootView'
|
RootView = require 'views/core/RootView'
|
||||||
template = require 'templates/courses/courses-view'
|
template = require 'templates/courses/courses-view'
|
||||||
|
StudentLogInModal = require 'views/courses/StudentLogInModal'
|
||||||
|
StudentSignUpModal = require 'views/courses/StudentSignUpModal'
|
||||||
|
ChangeCourseLanguageModal = require 'views/courses/ChangeCourseLanguageModal'
|
||||||
|
ChooseLanguageModal = require 'views/courses/ChooseLanguageModal'
|
||||||
|
CourseInstance = require 'models/CourseInstance'
|
||||||
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
|
Course = require 'models/Course'
|
||||||
|
Classroom = require 'models/Classroom'
|
||||||
|
LevelSession = require 'models/LevelSession'
|
||||||
|
Campaign = require 'models/Campaign'
|
||||||
|
utils = require 'core/utils'
|
||||||
|
|
||||||
|
# TODO: Test everything
|
||||||
|
|
||||||
module.exports = class CoursesView extends RootView
|
module.exports = class CoursesView extends RootView
|
||||||
id: 'courses-view'
|
id: 'courses-view'
|
||||||
template: template
|
template: template
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click #log-in-btn': 'onClickLogInButton'
|
||||||
|
'click #start-new-game-btn': 'onClickStartNewGameButton'
|
||||||
|
'click #join-class-btn': 'onClickJoinClassButton'
|
||||||
|
'submit #join-class-form': 'onSubmitJoinClassForm'
|
||||||
|
'click #change-language-link': 'onClickChangeLanguageLink'
|
||||||
|
|
||||||
|
initialize: ->
|
||||||
|
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
||||||
|
@courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID')
|
||||||
|
@listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded
|
||||||
|
@supermodel.loadCollection(@courseInstances, 'course_instances')
|
||||||
|
@classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom })
|
||||||
|
@supermodel.loadCollection(@classrooms, 'classrooms', { data: {memberID: me.id} })
|
||||||
|
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
||||||
|
@supermodel.loadCollection(@courses, 'courses')
|
||||||
|
@campaigns = new CocoCollection([], { url: "/db/campaign", model: Campaign })
|
||||||
|
@supermodel.loadCollection(@campaigns, 'campaigns', { data: { type: 'course' }})
|
||||||
|
|
||||||
|
onCourseInstancesLoaded: ->
|
||||||
|
map = {}
|
||||||
|
for courseInstance in @courseInstances.models
|
||||||
|
courseID = courseInstance.get('courseID')
|
||||||
|
if map[courseID]
|
||||||
|
courseInstance.sessions = map[courseID]
|
||||||
|
continue
|
||||||
|
map[courseID] = courseInstance.sessions = new CocoCollection([], {
|
||||||
|
url: courseInstance.url() + '/my-course-level-sessions',
|
||||||
|
model: LevelSession
|
||||||
|
})
|
||||||
|
courseInstance.sessions.comparator = 'changed'
|
||||||
|
@supermodel.loadCollection(courseInstance.sessions, 'sessions', { data: { project: 'state.complete level.original playtime changed' }})
|
||||||
|
|
||||||
|
@hocCourseInstance = @courseInstances.findWhere({hourOfCode: true})
|
||||||
|
if @hocCourseInstance
|
||||||
|
@courseInstances.remove(@hocCourseInstance)
|
||||||
|
|
||||||
|
onLoaded: ->
|
||||||
|
super()
|
||||||
|
if utils.getQueryVariable('_cc', false)
|
||||||
|
@joinClass()
|
||||||
|
|
||||||
|
onClickStartNewGameButton: ->
|
||||||
|
if me.isAnonymous()
|
||||||
|
@openSignUpModal()
|
||||||
|
else
|
||||||
|
modal = new ChooseLanguageModal()
|
||||||
|
@openModalView(modal)
|
||||||
|
@listenToOnce modal, 'set-language', @startHourOfCodePlay
|
||||||
|
|
||||||
|
onClickLogInButton: ->
|
||||||
|
modal = new StudentLogInModal()
|
||||||
|
@openModalView(modal)
|
||||||
|
modal.on 'want-to-create-account', @openSignUpModal, @
|
||||||
|
|
||||||
|
openSignUpModal: ->
|
||||||
|
modal = new StudentSignUpModal({ willPlay: true })
|
||||||
|
@openModalView(modal)
|
||||||
|
modal.once 'click-skip-link', @startHourOfCodePlay, @
|
||||||
|
|
||||||
|
startHourOfCodePlay: ->
|
||||||
|
@$('#main-content').hide()
|
||||||
|
@$('#begin-hoc-area').removeClass('hide')
|
||||||
|
hocCourseInstance = new CourseInstance()
|
||||||
|
hocCourseInstance.upsertForHOC()
|
||||||
|
@listenToOnce hocCourseInstance, 'sync', ->
|
||||||
|
url = hocCourseInstance.firstLevelURL()
|
||||||
|
app.router.navigate(url, { trigger: true })
|
||||||
|
|
||||||
|
onSubmitJoinClassForm: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
@joinClass()
|
||||||
|
|
||||||
|
onClickJoinClassButton: (e) ->
|
||||||
|
@joinClass()
|
||||||
|
|
||||||
|
joinClass: ->
|
||||||
|
return if @state
|
||||||
|
@state = 'enrolling'
|
||||||
|
@errorMessage = null
|
||||||
|
@classCode = @$('#class-code-input').val() or utils.getQueryVariable('_cc', false)
|
||||||
|
if not @classCode
|
||||||
|
@state = null
|
||||||
|
@errorMessage = 'Please enter a code.'
|
||||||
|
@renderSelectors '#join-class-form'
|
||||||
|
return
|
||||||
|
@renderSelectors '#join-class-form'
|
||||||
|
newClassroom = new Classroom()
|
||||||
|
newClassroom.joinWithCode(@classCode)
|
||||||
|
newClassroom.on 'sync', @onJoinClassroomSuccess, @
|
||||||
|
newClassroom.on 'error', @onJoinClassroomError, @
|
||||||
|
|
||||||
|
onJoinClassroomError: (classroom, jqxhr, options) ->
|
||||||
|
@state = null
|
||||||
|
application.tracker?.trackEvent 'Failed to join classroom with code', status: jqxhr.status
|
||||||
|
if jqxhr.status is 422
|
||||||
|
@errorMessage = 'Please enter a code.'
|
||||||
|
else if jqxhr.status is 404
|
||||||
|
@errorMessage = 'Code not found.'
|
||||||
|
else
|
||||||
|
@errorMessage = "#{jqxhr.responseText}"
|
||||||
|
@renderSelectors '#join-class-form'
|
||||||
|
|
||||||
|
onJoinClassroomSuccess: (newClassroom, jqxhr, options) ->
|
||||||
|
application.tracker?.trackEvent 'Joined classroom', {
|
||||||
|
classroomID: newClassroom.id,
|
||||||
|
classroomName: newClassroom.get('name')
|
||||||
|
ownerID: newClassroom.get('ownerID')
|
||||||
|
}
|
||||||
|
@classrooms.add(newClassroom)
|
||||||
|
@render()
|
||||||
|
@classroomJustAdded = newClassroom.id
|
||||||
|
|
||||||
|
classroomCourseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance })
|
||||||
|
classroomCourseInstances.fetch({ data: {classroomID: newClassroom.id} })
|
||||||
|
@listenToOnce classroomCourseInstances, 'sync', ->
|
||||||
|
# join any course instances in the classroom which are free to join
|
||||||
|
jqxhrs = []
|
||||||
|
for courseInstance in classroomCourseInstances.models
|
||||||
|
course = @courses.get(courseInstance.get('courseID'))
|
||||||
|
if course.get('free')
|
||||||
|
jqxhrs.push = courseInstance.addMember(me.id)
|
||||||
|
courseInstance.sessions = new Backbone.Collection()
|
||||||
|
@courseInstances.add(courseInstance)
|
||||||
|
$.when(jqxhrs...).done =>
|
||||||
|
@state = null
|
||||||
|
@render()
|
||||||
|
location.hash = ''
|
||||||
|
f = -> location.hash = '#just-added-text'
|
||||||
|
# quick and dirty scroll to just-added classroom
|
||||||
|
setTimeout(f, 10)
|
||||||
|
|
||||||
|
onClickChangeLanguageLink: ->
|
||||||
|
modal = new ChangeCourseLanguageModal()
|
||||||
|
@openModalView(modal)
|
||||||
|
modal.once 'hidden', @render, @
|
|
@ -6,7 +6,12 @@ CourseInstance = require 'models/CourseInstance'
|
||||||
RootView = require 'views/core/RootView'
|
RootView = require 'views/core/RootView'
|
||||||
template = require 'templates/courses/hour-of-code-view'
|
template = require 'templates/courses/hour-of-code-view'
|
||||||
utils = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
|
LevelSession = require 'models/LevelSession'
|
||||||
|
Level = require 'models/Level'
|
||||||
|
ChooseLanguageModal = require 'views/courses/ChooseLanguageModal'
|
||||||
|
StudentLogInModal = require 'views/courses/StudentLogInModal'
|
||||||
|
StudentSignUpModal = require 'views/courses/StudentSignUpModal'
|
||||||
|
auth = require 'core/auth'
|
||||||
|
|
||||||
module.exports = class HourOfCodeView extends RootView
|
module.exports = class HourOfCodeView extends RootView
|
||||||
id: 'hour-of-code-view'
|
id: 'hour-of-code-view'
|
||||||
|
@ -14,11 +19,40 @@ module.exports = class HourOfCodeView extends RootView
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click #student-btn': 'onClickStudentButton'
|
'click #student-btn': 'onClickStudentButton'
|
||||||
|
'click #start-new-game-btn': 'onClickStartNewGameButton'
|
||||||
|
'click #log-in-btn': 'onClickLogInButton'
|
||||||
|
'click #log-out-link': 'onClickLogOutLink'
|
||||||
|
|
||||||
constructor: (options) ->
|
initialize: ->
|
||||||
super(options)
|
|
||||||
@setUpHourOfCode()
|
@setUpHourOfCode()
|
||||||
|
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
||||||
|
@listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded
|
||||||
|
@courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID')
|
||||||
|
@supermodel.loadCollection(@courseInstances, 'course_instances')
|
||||||
|
|
||||||
|
onCourseInstancesLoaded: ->
|
||||||
|
@hourOfCodeCourseInstance = @courseInstances.findWhere({hourOfCode: true})
|
||||||
|
if @hourOfCodeCourseInstance
|
||||||
|
@sessions = new CocoCollection([], {
|
||||||
|
url: "/db/course_instance/#{@hourOfCodeCourseInstance.id}/level_sessions"
|
||||||
|
model: LevelSession
|
||||||
|
})
|
||||||
|
@sessions.comparator = 'created'
|
||||||
|
@listenTo @sessions, 'sync', @onSessionsLoaded
|
||||||
|
@supermodel.loadCollection(@sessions, 'sessions')
|
||||||
|
|
||||||
|
onSessionsLoaded: ->
|
||||||
|
@lastSession = @sessions.last()
|
||||||
|
if @lastSession
|
||||||
|
@lastLevel = new Level()
|
||||||
|
levelData = @lastSession.get('level')
|
||||||
|
@supermodel.loadModel(@lastLevel, 'level', {
|
||||||
|
url: "/db/level/#{levelData.original}/version/#{levelData.majorVersion}"
|
||||||
|
data: {
|
||||||
|
project: 'name,slug'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
setUpHourOfCode: ->
|
setUpHourOfCode: ->
|
||||||
# If we haven't tracked this player as an hourOfCode player yet, and it's a new account, we do that now.
|
# If we haven't tracked this player as an hourOfCode player yet, and it's a new account, we do that now.
|
||||||
elapsed = new Date() - new Date(me.get('dateCreated'))
|
elapsed = new Date() - new Date(me.get('dateCreated'))
|
||||||
|
@ -28,18 +62,36 @@ module.exports = class HourOfCodeView extends RootView
|
||||||
$('body').append($('<img src="https://code.org/api/hour/begin_codecombat.png" style="visibility: hidden;">'))
|
$('body').append($('<img src="https://code.org/api/hour/begin_codecombat.png" style="visibility: hidden;">'))
|
||||||
application.tracker?.trackEvent 'Hour of Code Begin'
|
application.tracker?.trackEvent 'Hour of Code Begin'
|
||||||
|
|
||||||
onClickStudentButton: ->
|
onClickStartNewGameButton: ->
|
||||||
@state = 'enrolling'
|
# user without hour of code course instance, creates one, starts playing
|
||||||
@stateMessage = undefined
|
modal = new ChooseLanguageModal({
|
||||||
@render?()
|
logoutFirst: @hourOfCodeCourseInstance?
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
method: 'POST'
|
|
||||||
url: '/db/course_instance/-/create-for-hoc'
|
|
||||||
context: @
|
|
||||||
success: (data) ->
|
|
||||||
application.tracker?.trackEvent 'Finished HoC student course creation', {courseID: data.courseID}
|
|
||||||
app.router.navigate("/courses/#{data.courseID}/#{data._id}", {
|
|
||||||
trigger: true
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@openModalView(modal)
|
||||||
|
@listenToOnce modal, 'set-language', @startHourOfCodePlay
|
||||||
|
|
||||||
|
continuePlayingLink: ->
|
||||||
|
ci = @hourOfCodeCourseInstance
|
||||||
|
"/play/level/#{@lastLevel.get('slug')}?course=#{ci.get('courseID')}&course-instance=#{ci.id}"
|
||||||
|
|
||||||
|
startHourOfCodePlay: ->
|
||||||
|
@$('#main-content').hide()
|
||||||
|
@$('#begin-hoc-area').removeClass('hide')
|
||||||
|
hocCourseInstance = new CourseInstance()
|
||||||
|
hocCourseInstance.upsertForHOC()
|
||||||
|
@listenToOnce hocCourseInstance, 'sync', ->
|
||||||
|
url = hocCourseInstance.firstLevelURL()
|
||||||
|
app.router.navigate(url, { trigger: true })
|
||||||
|
|
||||||
|
onClickLogInButton: ->
|
||||||
|
modal = new StudentLogInModal()
|
||||||
|
@openModalView(modal)
|
||||||
|
modal.on 'want-to-create-account', @onWantToCreateAccount, @
|
||||||
|
|
||||||
|
onWantToCreateAccount: ->
|
||||||
|
modal = new StudentSignUpModal()
|
||||||
|
@openModalView(modal)
|
||||||
|
|
||||||
|
onClickLogOutLink: ->
|
||||||
|
auth.logoutUser()
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,12 @@ module.exports = class InviteToClassroomModal extends ModalView
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click #send-invites-btn': 'onClickSendInvitesButton'
|
'click #send-invites-btn': 'onClickSendInvitesButton'
|
||||||
|
'click #copy-url-btn': 'onClickCopyURLButton'
|
||||||
|
|
||||||
initialize: (options) ->
|
initialize: (options) ->
|
||||||
@classroom = options.classroom
|
@classroom = options.classroom
|
||||||
|
@classCode = @classroom.get('codeCamel') || @classroom.get('code')
|
||||||
|
@joinURL = document.location.origin + "/courses?_cc=" + @classCode
|
||||||
|
|
||||||
onClickSendInvitesButton: ->
|
onClickSendInvitesButton: ->
|
||||||
emails = @$('#invite-emails-textarea').val()
|
emails = @$('#invite-emails-textarea').val()
|
||||||
|
@ -30,3 +33,12 @@ module.exports = class InviteToClassroomModal extends ModalView
|
||||||
@$('#invite-emails-sending-alert').addClass('hide')
|
@$('#invite-emails-sending-alert').addClass('hide')
|
||||||
@$('#invite-emails-success-alert').removeClass('hide')
|
@$('#invite-emails-success-alert').removeClass('hide')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onClickCopyURLButton: ->
|
||||||
|
@$('#join-url-input').val(@joinURL).select()
|
||||||
|
try
|
||||||
|
document.execCommand('copy')
|
||||||
|
@$('#copied-alert').removeClass('hide')
|
||||||
|
catch err
|
||||||
|
console.log('Oops, unable to copy', err)
|
||||||
|
@$('#copy-failed-alert').removeClass('hide')
|
||||||
|
|
|
@ -15,6 +15,7 @@ module.exports = class PurchaseCoursesView extends RootView
|
||||||
|
|
||||||
initialize: (options) ->
|
initialize: (options) ->
|
||||||
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
|
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
|
||||||
|
@fromClassroom = utils.getQueryVariable('from-classroom')
|
||||||
super(options)
|
super(options)
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
|
37
app/views/courses/RemoveStudentModal.coffee
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
ModalView = require 'views/core/ModalView'
|
||||||
|
template = require 'templates/courses/remove-student-modal'
|
||||||
|
|
||||||
|
module.exports = class RemoveStudentModal extends ModalView
|
||||||
|
id: 'remove-student-modal'
|
||||||
|
template: template
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click #remove-student-btn': 'onClickRemoveStudentButton'
|
||||||
|
|
||||||
|
initialize: (options) ->
|
||||||
|
@classroom = options.classroom
|
||||||
|
@user = options.user
|
||||||
|
@courseInstances = options.courseInstances
|
||||||
|
|
||||||
|
onClickRemoveStudentButton: ->
|
||||||
|
@$('#remove-student-buttons').addClass('hide')
|
||||||
|
@$('#remove-student-progress').removeClass('hide')
|
||||||
|
userID = @user.id
|
||||||
|
@toRemove = @courseInstances.filter (courseInstance) -> _.contains(courseInstance.get('members'), userID)
|
||||||
|
@toRemove.push @classroom
|
||||||
|
@totalJobs = _.size(@toRemove)
|
||||||
|
@removeStudent()
|
||||||
|
|
||||||
|
removeStudent: ->
|
||||||
|
model = @toRemove.shift()
|
||||||
|
if not model
|
||||||
|
@trigger 'remove-student', { user: @user }
|
||||||
|
@hide()
|
||||||
|
return
|
||||||
|
|
||||||
|
model.removeMember(@user.id)
|
||||||
|
pct = (100 * (@totalJobs - @toRemove.length) / @totalJobs).toFixed(1) + '%'
|
||||||
|
@$('#remove-student-progress .progress-bar').css('width', pct)
|
||||||
|
@listenToOnce model, 'sync', ->
|
||||||
|
@removeStudent()
|
||||||
|
|
34
app/views/courses/StudentLogInModal.coffee
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
ModalView = require 'views/core/ModalView'
|
||||||
|
template = require 'templates/courses/student-log-in-modal'
|
||||||
|
auth = require 'core/auth'
|
||||||
|
forms = require 'core/forms'
|
||||||
|
User = require 'models/User'
|
||||||
|
|
||||||
|
module.exports = class StudentSignInModal extends ModalView
|
||||||
|
id: 'student-log-in-modal'
|
||||||
|
template: template
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click #log-in-btn': 'onClickLogInButton'
|
||||||
|
'submit form': 'onSubmitForm'
|
||||||
|
'click #create-new-account-link': 'onClickCreateNewAccountLink'
|
||||||
|
|
||||||
|
onSubmitForm: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
@login()
|
||||||
|
|
||||||
|
onClickLogInButton: ->
|
||||||
|
@login()
|
||||||
|
|
||||||
|
login: ->
|
||||||
|
data = forms.formToObject @$el
|
||||||
|
@enableModalInProgress(@$el)
|
||||||
|
auth.loginUser data, (jqxhr) =>
|
||||||
|
error = jqxhr.responseJSON[0]
|
||||||
|
message = _.filter([error.property, error.message]).join(' ')
|
||||||
|
@disableModalInProgress(@$el)
|
||||||
|
@$('#errors-alert').text(message).removeClass('hide')
|
||||||
|
|
||||||
|
onClickCreateNewAccountLink: ->
|
||||||
|
@trigger 'want-to-create-account'
|
||||||
|
@hide?()
|
96
app/views/courses/StudentSignUpModal.coffee
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
ModalView = require 'views/core/ModalView'
|
||||||
|
template = require 'templates/courses/student-sign-up-modal'
|
||||||
|
auth = require 'core/auth'
|
||||||
|
forms = require 'core/forms'
|
||||||
|
User = require 'models/User'
|
||||||
|
Classroom = require 'models/Classroom'
|
||||||
|
utils = require 'core/utils'
|
||||||
|
|
||||||
|
module.exports = class StudentSignUpModal extends ModalView
|
||||||
|
id: 'student-sign-up-modal'
|
||||||
|
template: template
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click #sign-up-btn': 'onClickSignUpButton'
|
||||||
|
'submit form': 'onSubmitForm'
|
||||||
|
'click #skip-link': 'onClickSkipLink'
|
||||||
|
|
||||||
|
initialize: (options) ->
|
||||||
|
options ?= {}
|
||||||
|
@willPlay = options.willPlay
|
||||||
|
@classCode = utils.getQueryVariable('_cc') or ''
|
||||||
|
|
||||||
|
onClickSkipLink: ->
|
||||||
|
@trigger 'click-skip-link' # defer to view that opened this modal
|
||||||
|
@hide?()
|
||||||
|
|
||||||
|
onSubmitForm: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
@signupClassroomPrecheck()
|
||||||
|
|
||||||
|
onClickSignUpButton: ->
|
||||||
|
@signupClassroomPrecheck()
|
||||||
|
|
||||||
|
emailCheck: ->
|
||||||
|
email = @$('#email').val()
|
||||||
|
filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i # https://news.ycombinator.com/item?id=5763990
|
||||||
|
unless filter.test(email)
|
||||||
|
@$('#errors-alert').text($.i18n.t('share_progress_modal.email_invalid')).removeClass('hide')
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
signupClassroomPrecheck: ->
|
||||||
|
if not _.all([@$('#email').val(), @$('#password').val(), @$('#name').val()])
|
||||||
|
@$('#errors-alert').text('Enter email, username and password').removeClass('hide')
|
||||||
|
return
|
||||||
|
classCode = @$('#class-code-input').val()
|
||||||
|
if not classCode
|
||||||
|
return @signup()
|
||||||
|
classroom = new Classroom()
|
||||||
|
classroom.fetch({ url: '/db/classroom?code='+classCode })
|
||||||
|
classroom.once 'sync', @signup, @
|
||||||
|
classroom.once 'error', @onClassroomFetchError, @
|
||||||
|
@enableModalInProgress(@$el)
|
||||||
|
|
||||||
|
onClassroomFetchError: ->
|
||||||
|
@disableModalInProgress(@$el)
|
||||||
|
@$('#errors-alert').text('Classroom code could not be found').removeClass('hide')
|
||||||
|
|
||||||
|
signup: ->
|
||||||
|
return unless @emailCheck()
|
||||||
|
# TODO: consolidate with AuthModal logic, or make user creation process less magical, more RESTful
|
||||||
|
data = forms.formToObject @$el
|
||||||
|
delete data.classCode
|
||||||
|
for key, val of me.attributes when key in ['preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1', 'name', 'music', 'volume', 'emails', 'schoolName']
|
||||||
|
data[key] ?= val
|
||||||
|
Backbone.Mediator.publish "auth:signed-up", {}
|
||||||
|
data.emails ?= {}
|
||||||
|
data.emails.generalNews ?= {}
|
||||||
|
data.emails.generalNews.enabled = false
|
||||||
|
window.tracker?.trackEvent 'Finished Student Signup', label: 'CodeCombat'
|
||||||
|
@enableModalInProgress(@$el)
|
||||||
|
user = new User(data)
|
||||||
|
user.notyErrors = false
|
||||||
|
user.save({}, {
|
||||||
|
validate: false # make server deal with everything
|
||||||
|
error: @onCreateUserError
|
||||||
|
success: @onCreateUserSuccess
|
||||||
|
})
|
||||||
|
|
||||||
|
onCreateUserError: (model, jqxhr) =>
|
||||||
|
# really need to make our server errors uniform
|
||||||
|
if jqxhr.responseJSON
|
||||||
|
error = jqxhr.responseJSON
|
||||||
|
error = error[0] if _.isArray(error)
|
||||||
|
message = _.filter([error.property, error.message]).join(' ')
|
||||||
|
else
|
||||||
|
message = jqxhr.responseText
|
||||||
|
@disableModalInProgress(@$el)
|
||||||
|
@$('#errors-alert').text(message).removeClass('hide')
|
||||||
|
|
||||||
|
onCreateUserSuccess: =>
|
||||||
|
classCode = @$('#class-code-input').val()
|
||||||
|
if classCode
|
||||||
|
url = "/courses?_cc="+classCode
|
||||||
|
application.router.navigate(url)
|
||||||
|
window.location.reload()
|
|
@ -4,13 +4,11 @@ CocoCollection = require 'collections/CocoCollection'
|
||||||
CocoModel = require 'models/CocoModel'
|
CocoModel = require 'models/CocoModel'
|
||||||
Course = require 'models/Course'
|
Course = require 'models/Course'
|
||||||
Classroom = require 'models/Classroom'
|
Classroom = require 'models/Classroom'
|
||||||
|
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
Prepaid = require 'models/Prepaid'
|
|
||||||
CourseInstance = require 'models/CourseInstance'
|
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'
|
||||||
utils = require 'core/utils'
|
|
||||||
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
|
||||||
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
||||||
|
|
||||||
module.exports = class TeacherCoursesView extends RootView
|
module.exports = class TeacherCoursesView extends RootView
|
||||||
|
@ -18,11 +16,8 @@ module.exports = class TeacherCoursesView extends RootView
|
||||||
template: template
|
template: template
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click #create-new-class-btn': 'onClickCreateNewclassButton'
|
'click .btn-add-students': 'onClickAddStudents'
|
||||||
'click .add-students-btn': 'onClickAddStudentsButton'
|
'click .create-new-class': 'onClickCreateNewClassButton'
|
||||||
'click .course-instance-membership-checkbox': 'onClickCourseInstanceMembershipCheckbox'
|
|
||||||
'click #save-changes-btn': 'onClickSaveChangesButton'
|
|
||||||
'click #manage-tab-link': 'onClickManageTabLink'
|
|
||||||
'click .edit-classroom-small': 'onClickEditClassroomSmall'
|
'click .edit-classroom-small': 'onClickEditClassroomSmall'
|
||||||
|
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
|
@ -38,15 +33,7 @@ module.exports = class TeacherCoursesView extends RootView
|
||||||
@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}})
|
||||||
@members = new CocoCollection([], { model: User })
|
@members = new CocoCollection([], { model: User })
|
||||||
@prepaids = new CocoCollection([], { url: "/db/prepaid", model: Prepaid })
|
@listenTo @members, 'sync', @render
|
||||||
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'
|
|
||||||
@supermodel.loadCollection(@prepaids, 'prepaids', {data: {creator: me.id}})
|
|
||||||
@listenTo @members, 'sync', @renderManageTab
|
|
||||||
@usersToRedeem = new CocoCollection([], { model: User })
|
|
||||||
@hoc = utils.getQueryVariable('hoc')
|
|
||||||
@
|
@
|
||||||
|
|
||||||
onceClassroomsSync: ->
|
onceClassroomsSync: ->
|
||||||
|
@ -56,160 +43,28 @@ module.exports = class TeacherCoursesView extends RootView
|
||||||
url: "/db/classroom/#{classroom.id}/members"
|
url: "/db/classroom/#{classroom.id}/members"
|
||||||
})
|
})
|
||||||
|
|
||||||
onClickCreateNewclassButton: ->
|
onClickAddStudents: (e) ->
|
||||||
name = @$('#new-classroom-name-input').val()
|
|
||||||
return unless name
|
|
||||||
classroom = new Classroom({ name: name })
|
|
||||||
classroom.save()
|
|
||||||
@classrooms.add(classroom)
|
|
||||||
classroom.saving = true
|
|
||||||
@renderManageTab()
|
|
||||||
@listenTo classroom, 'sync', ->
|
|
||||||
classroom.saving = false
|
|
||||||
@fillMissingCourseInstances()
|
|
||||||
|
|
||||||
renderManageTab: ->
|
|
||||||
isActive = @$('#manage-tab-pane').hasClass('active')
|
|
||||||
@renderSelectors('#manage-tab-pane')
|
|
||||||
@$('#manage-tab-pane').toggleClass('active', isActive)
|
|
||||||
|
|
||||||
onClickEditClassroomSmall: (e) ->
|
|
||||||
classroomID = $(e.target).closest('small').data('classroom-id')
|
|
||||||
classroom = @classrooms.get(classroomID)
|
|
||||||
modal = new ClassroomSettingsModal({classroom: classroom})
|
|
||||||
@openModalView(modal)
|
|
||||||
@listenToOnce modal, 'hide', @renderManageTab
|
|
||||||
|
|
||||||
onClickAddStudentsButton: (e) ->
|
|
||||||
classroomID = $(e.target).data('classroom-id')
|
classroomID = $(e.target).data('classroom-id')
|
||||||
classroom = @classrooms.get(classroomID)
|
classroom = @classrooms.get(classroomID)
|
||||||
|
unless classroom
|
||||||
|
console.error 'No classroom ID found.'
|
||||||
|
return
|
||||||
modal = new InviteToClassroomModal({classroom: classroom})
|
modal = new InviteToClassroomModal({classroom: classroom})
|
||||||
@openModalView(modal)
|
@openModalView(modal)
|
||||||
|
|
||||||
onLoaded: ->
|
onClickCreateNewClassButton: ->
|
||||||
super()
|
return @openModalView new AuthModal() if me.get('anonymous')
|
||||||
@linkCourseIntancesToCourses()
|
modal = new ClassroomSettingsModal({})
|
||||||
@fillMissingCourseInstances()
|
@openModalView(modal)
|
||||||
|
@listenToOnce modal, 'hide', =>
|
||||||
|
# TODO: how to get new classroom from modal?
|
||||||
|
@classrooms.add(modal.classroom)
|
||||||
|
# TODO: will this definitely fire after modal saves new classroom?
|
||||||
|
@listenToOnce modal.classroom, 'sync', @render
|
||||||
|
|
||||||
linkCourseIntancesToCourses: ->
|
onClickEditClassroomSmall: (e) ->
|
||||||
for courseInstance in @courseInstances.models
|
classroomID = $(e.target).data('classroom-id')
|
||||||
courseInstance.course = @courses.get(courseInstance.get('courseID'))
|
classroom = @classrooms.get(classroomID)
|
||||||
|
modal = new ClassroomSettingsModal({classroom: classroom})
|
||||||
fillMissingCourseInstances: ->
|
@openModalView(modal)
|
||||||
# TODO: Give teachers control over which courses are enabled for a given class.
|
@listenToOnce modal, 'hide', @render
|
||||||
# Add/remove course instances and columns in the view to match.
|
|
||||||
for classroom in @classrooms.models
|
|
||||||
classroom.filling = false
|
|
||||||
for course in @courses.models
|
|
||||||
courseInstance = @courseInstances.findWhere({classroomID: classroom.id, courseID: course.id})
|
|
||||||
if not courseInstance
|
|
||||||
classroom.filling = true
|
|
||||||
courseInstance = new CourseInstance({
|
|
||||||
classroomID: classroom.id
|
|
||||||
courseID: course.id
|
|
||||||
})
|
|
||||||
# TODO: figure out a better way to get around triggering validation errors for properties
|
|
||||||
# that the server will end up filling in, like an empty members array, ownerID
|
|
||||||
courseInstance.save(null, {validate: false})
|
|
||||||
courseInstance.course = course
|
|
||||||
@courseInstances.add(courseInstance)
|
|
||||||
@listenToOnce courseInstance, 'sync', @fillMissingCourseInstances
|
|
||||||
@renderManageTab()
|
|
||||||
return
|
|
||||||
@renderManageTab()
|
|
||||||
|
|
||||||
onClickCourseInstanceMembershipCheckbox: ->
|
|
||||||
usersToRedeem = {}
|
|
||||||
checkedBoxes = @$('.course-instance-membership-checkbox:checked')
|
|
||||||
_.each checkedBoxes, (el) =>
|
|
||||||
$el = $(el)
|
|
||||||
userID = $el.data('user-id')
|
|
||||||
return if usersToRedeem[userID]
|
|
||||||
user = @members.get(userID)
|
|
||||||
return if user.get('coursePrepaidID')
|
|
||||||
courseInstanceID = $el.data('course-instance-id')
|
|
||||||
courseInstance = @courseInstances.get(courseInstanceID)
|
|
||||||
return if courseInstance.course.get('free')
|
|
||||||
usersToRedeem[userID] = user
|
|
||||||
|
|
||||||
@usersToRedeem = new CocoCollection(_.values(usersToRedeem), {model: User})
|
|
||||||
@numCourseInstancesToAddTo = checkedBoxes.length
|
|
||||||
@renderSelectors '#fixed-area'
|
|
||||||
|
|
||||||
onClickSaveChangesButton: ->
|
|
||||||
@$('.course-instance-membership-checkbox').attr('disabled', true)
|
|
||||||
checkedBoxes = @$('.course-instance-membership-checkbox:checked')
|
|
||||||
raw = _.map checkedBoxes, (el) =>
|
|
||||||
$el = $(el)
|
|
||||||
userID = $el.data('user-id')
|
|
||||||
courseInstanceID = $el.data('course-instance-id')
|
|
||||||
courseInstance = @courseInstances.get(courseInstanceID)
|
|
||||||
return {
|
|
||||||
courseInstance: courseInstance
|
|
||||||
userID: userID
|
|
||||||
}
|
|
||||||
@membershipAdditions = new CocoCollection(raw, { model: User }) # TODO: Allow collections not to have models defined?
|
|
||||||
@membershipAdditions.originalSize = @membershipAdditions.size()
|
|
||||||
@usersToRedeem.originalSize = @usersToRedeem.size()
|
|
||||||
@state = 'saving-changes'
|
|
||||||
@renderSelectors '#fixed-area'
|
|
||||||
@redeemUsers()
|
|
||||||
|
|
||||||
redeemUsers: ->
|
|
||||||
if not @usersToRedeem.size()
|
|
||||||
@addMemberships()
|
|
||||||
return
|
|
||||||
|
|
||||||
user = @usersToRedeem.first()
|
|
||||||
|
|
||||||
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots())
|
|
||||||
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots()) unless prepaid
|
|
||||||
$.ajax({
|
|
||||||
method: 'POST'
|
|
||||||
url: _.result(prepaid, 'url') + '/redeemers'
|
|
||||||
data: { userID: user.id }
|
|
||||||
context: @
|
|
||||||
success: ->
|
|
||||||
@usersToRedeem.remove(user)
|
|
||||||
@renderSelectors '#fixed-area'
|
|
||||||
@redeemUsers()
|
|
||||||
error: (jqxhr, textStatus, errorThrown) ->
|
|
||||||
if jqxhr.status is 402
|
|
||||||
@state = 'error'
|
|
||||||
@stateMessage = arguments[2]
|
|
||||||
else
|
|
||||||
@state = 'error'
|
|
||||||
@stateMessage = "#{jqxhr.status}: #{jqxhr.responseText}"
|
|
||||||
@renderSelectors '#fixed-area'
|
|
||||||
})
|
|
||||||
|
|
||||||
addMemberships: ->
|
|
||||||
if not @membershipAdditions.size()
|
|
||||||
@renderSelectors '#fixed-area'
|
|
||||||
document.location.reload()
|
|
||||||
return
|
|
||||||
|
|
||||||
membershipAddition = @membershipAdditions.first()
|
|
||||||
courseInstance = membershipAddition.get('courseInstance')
|
|
||||||
userID = membershipAddition.get('userID')
|
|
||||||
$.ajax({
|
|
||||||
method: 'POST'
|
|
||||||
url: _.result(courseInstance, 'url') + '/members'
|
|
||||||
data: { userID: userID }
|
|
||||||
context: @
|
|
||||||
success: ->
|
|
||||||
@membershipAdditions.remove(membershipAddition)
|
|
||||||
@renderSelectors '#fixed-area'
|
|
||||||
@addMemberships()
|
|
||||||
error: (jqxhr, textStatus, errorThrown) ->
|
|
||||||
if jqxhr.status is 402
|
|
||||||
@state = 'error'
|
|
||||||
@stateMessage = arguments[2]
|
|
||||||
else
|
|
||||||
@state = 'error'
|
|
||||||
@stateMessage = "#{jqxhr.status}: #{jqxhr.responseText}"
|
|
||||||
@renderSelectors '#fixed-area'
|
|
||||||
})
|
|
||||||
|
|
||||||
onClickManageTabLink: ->
|
|
||||||
@$('.nav-tabs a[href="#manage-tab-pane"]').tab('show')
|
|
||||||
|
|
|
@ -63,9 +63,10 @@ module.exports = class HeroVictoryModal extends ModalView
|
||||||
else
|
else
|
||||||
@readyToContinue = true
|
@readyToContinue = true
|
||||||
@playSound 'victory'
|
@playSound 'victory'
|
||||||
if @level.get('type', true) is 'course' and nextLevel = @level.get('nextLevel')
|
if @level.get('type', true) is 'course'
|
||||||
@nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}"
|
if nextLevel = @level.get('nextLevel')
|
||||||
@nextLevel = @supermodel.loadModel(@nextLevel, 'level').model
|
@nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}"
|
||||||
|
@nextLevel = @supermodel.loadModel(@nextLevel, 'level').model
|
||||||
if @courseID
|
if @courseID
|
||||||
@course = new Course().setURL "/db/course/#{@courseID}"
|
@course = new Course().setURL "/db/course/#{@courseID}"
|
||||||
@course = @supermodel.loadModel(@course, 'course').model
|
@course = @supermodel.loadModel(@course, 'course').model
|
||||||
|
|
|
@ -46,6 +46,8 @@ CampaignHandler = class CampaignHandler extends Handler
|
||||||
query = i18nCoverage: {$exists: true}
|
query = i18nCoverage: {$exists: true}
|
||||||
if req.query.project
|
if req.query.project
|
||||||
projection[field] = 1 for field in req.query.project.split(',')
|
projection[field] = 1 for field in req.query.project.split(',')
|
||||||
|
if req.query.type
|
||||||
|
query.type = req.query.type
|
||||||
q = @modelClass.find query, projection
|
q = @modelClass.find query, projection
|
||||||
q.exec (err, documents) =>
|
q.exec (err, documents) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
|
|
|
@ -36,6 +36,7 @@ ClassroomHandler = class ClassroomHandler extends Handler
|
||||||
method = req.method.toLowerCase()
|
method = req.method.toLowerCase()
|
||||||
return @inviteStudents(req, res, args[0]) if args[1] is 'invite-members'
|
return @inviteStudents(req, res, args[0]) if args[1] is 'invite-members'
|
||||||
return @joinClassroomAPI(req, res, args[0]) if method is 'post' and args[1] is 'members'
|
return @joinClassroomAPI(req, res, args[0]) if method is 'post' and args[1] is 'members'
|
||||||
|
return @removeMember(req, res, args[0]) if req.method is 'DELETE' and args[1] is 'members'
|
||||||
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
|
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
|
||||||
super(arguments...)
|
super(arguments...)
|
||||||
|
|
||||||
|
@ -65,6 +66,25 @@ ClassroomHandler = class ClassroomHandler extends Handler
|
||||||
classroom.set('members', members)
|
classroom.set('members', members)
|
||||||
return @sendSuccess(res, @formatEntity(req, classroom))
|
return @sendSuccess(res, @formatEntity(req, classroom))
|
||||||
|
|
||||||
|
removeMember: (req, res, classroomID) ->
|
||||||
|
userID = req.body.userID
|
||||||
|
return @sendBadInputError(res, 'Input must be a MongoDB ID') unless utils.isID(userID)
|
||||||
|
Classroom.findById classroomID, (err, classroom) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendNotFoundError(res, 'Classroom referenced by course instance not found') unless classroom
|
||||||
|
return @sendForbiddenError(res) unless _.any(classroom.get('members'), (memberID) -> memberID.toString() is userID)
|
||||||
|
ownsClassroom = classroom.get('ownerID').equals(req.user.get('_id'))
|
||||||
|
removingSelf = userID is req.user.id
|
||||||
|
return @sendForbiddenError(res) unless ownsClassroom or removingSelf
|
||||||
|
alreadyNotInClassroom = not _.any classroom.get('members') or [], (memberID) -> memberID.toString() is userID
|
||||||
|
return @sendSuccess(res, @formatEntity(req, classroom)) if alreadyNotInClassroom
|
||||||
|
members = _.clone(classroom.get('members'))
|
||||||
|
members.splice(members.indexOf(userID), 1)
|
||||||
|
classroom.set('members', members)
|
||||||
|
classroom.save (err, classroom) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
@sendSuccess(res, @formatEntity(req, classroom))
|
||||||
|
|
||||||
formatEntity: (req, doc) ->
|
formatEntity: (req, doc) ->
|
||||||
if req.user?.isAdmin() or req.user?.get('_id').equals(doc.get('ownerID'))
|
if req.user?.isAdmin() or req.user?.get('_id').equals(doc.get('ownerID'))
|
||||||
return doc.toObject()
|
return doc.toObject()
|
||||||
|
@ -104,6 +124,11 @@ ClassroomHandler = class ClassroomHandler extends Handler
|
||||||
Classroom.find {members: mongoose.Types.ObjectId(memberID)}, (err, classrooms) =>
|
Classroom.find {members: mongoose.Types.ObjectId(memberID)}, (err, classrooms) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
return @sendSuccess(res, (@formatEntity(req, classroom) for classroom in classrooms))
|
return @sendSuccess(res, (@formatEntity(req, classroom) for classroom in classrooms))
|
||||||
|
else if code = req.query.code.toLowerCase()
|
||||||
|
Classroom.findOne {code: code}, (err, classroom) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendNotFoundError(res) unless classroom
|
||||||
|
return @sendSuccess(res, @formatEntity(req, classroom))
|
||||||
else
|
else
|
||||||
super(arguments...)
|
super(arguments...)
|
||||||
|
|
||||||
|
|
|
@ -35,9 +35,11 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||||
return @createHOCAPI(req, res) if relationship is 'create-for-hoc'
|
return @createHOCAPI(req, res) if relationship is 'create-for-hoc'
|
||||||
return @getLevelSessionsAPI(req, res, args[0]) if args[1] is 'level_sessions'
|
return @getLevelSessionsAPI(req, res, args[0]) if args[1] is 'level_sessions'
|
||||||
return @addMember(req, res, args[0]) if req.method is 'POST' and args[1] is 'members'
|
return @addMember(req, res, args[0]) if req.method is 'POST' and args[1] is 'members'
|
||||||
|
return @removeMember(req, res, args[0]) if req.method is 'DELETE' and args[1] is 'members'
|
||||||
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
|
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
|
||||||
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
|
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
|
||||||
return @redeemPrepaidCodeAPI(req, res) if args[1] is 'redeem_prepaid'
|
return @redeemPrepaidCodeAPI(req, res) if args[1] is 'redeem_prepaid'
|
||||||
|
return @getMyCourseLevelSessionsAPI(req, res, args[0]) if args[1] is 'my-course-level-sessions'
|
||||||
return @findByLevel(req, res, args[2]) if args[1] is 'find_by_level'
|
return @findByLevel(req, res, args[2]) if args[1] is 'find_by_level'
|
||||||
super arguments...
|
super arguments...
|
||||||
|
|
||||||
|
@ -93,6 +95,30 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
@sendSuccess(res, @formatEntity(req, courseInstance))
|
@sendSuccess(res, @formatEntity(req, courseInstance))
|
||||||
|
|
||||||
|
removeMember: (req, res, courseInstanceID) ->
|
||||||
|
userID = req.body.userID
|
||||||
|
return @sendBadInputError(res, 'Input must be a MongoDB ID') unless utils.isID(userID)
|
||||||
|
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendNotFoundError(res, 'Course instance not found') unless courseInstance
|
||||||
|
Classroom.findById courseInstance.get('classroomID'), (err, classroom) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendNotFoundError(res, 'Classroom referenced by course instance not found') unless classroom
|
||||||
|
return @sendForbiddenError(res) unless _.any(classroom.get('members'), (memberID) -> memberID.toString() is userID)
|
||||||
|
ownsCourseInstance = courseInstance.get('ownerID').equals(req.user.get('_id'))
|
||||||
|
removingSelf = userID is req.user.id
|
||||||
|
return @sendForbiddenError(res) unless ownsCourseInstance or removingSelf
|
||||||
|
alreadyNotInCourseInstance = not _.any courseInstance.get('members') or [], (memberID) -> memberID.toString() is userID
|
||||||
|
return @sendSuccess(res, @formatEntity(req, courseInstance)) if alreadyNotInCourseInstance
|
||||||
|
members = _.clone(courseInstance.get('members'))
|
||||||
|
members.splice(members.indexOf(userID), 1)
|
||||||
|
courseInstance.set('members', members)
|
||||||
|
courseInstance.save (err, courseInstance) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
User.update {_id: mongoose.Types.ObjectId(userID)}, {$pull: {courseInstances: courseInstance.get('_id')}}, (err) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
@sendSuccess(res, @formatEntity(req, courseInstance))
|
||||||
|
|
||||||
post: (req, res) ->
|
post: (req, res) ->
|
||||||
return @sendBadInputError(res, 'No classroomID') unless req.body.classroomID
|
return @sendBadInputError(res, 'No classroomID') unless req.body.classroomID
|
||||||
return @sendBadInputError(res, 'No courseID') unless req.body.courseID
|
return @sendBadInputError(res, 'No courseID') unless req.body.courseID
|
||||||
|
@ -126,7 +152,28 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||||
levelIDs = (levelID for levelID of campaign.get('levels'))
|
levelIDs = (levelID for levelID of campaign.get('levels'))
|
||||||
memberIDs = _.map courseInstance.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID
|
memberIDs = _.map courseInstance.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID
|
||||||
query = {$and: [{creator: {$in: memberIDs}}, {'level.original': {$in: levelIDs}}]}
|
query = {$and: [{creator: {$in: memberIDs}}, {'level.original': {$in: levelIDs}}]}
|
||||||
LevelSession.find query, (err, documents) =>
|
cursor = LevelSession.find(query)
|
||||||
|
cursor = cursor.select(req.query.project) if req.query.project
|
||||||
|
cursor.exec (err, documents) =>
|
||||||
|
return @sendDatabaseError(res, err) if err?
|
||||||
|
cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
|
||||||
|
@sendSuccess(res, cleandocs)
|
||||||
|
|
||||||
|
getMyCourseLevelSessionsAPI: (req, res, courseInstanceID) ->
|
||||||
|
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendNotFoundError(res) unless courseInstance
|
||||||
|
Course.findById courseInstance.get('courseID'), (err, course) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendNotFoundError(res) unless course
|
||||||
|
Campaign.findById course.get('campaignID'), (err, campaign) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendNotFoundError(res) unless campaign
|
||||||
|
levelIDs = (levelID for levelID, level of campaign.get('levels') when not _.contains(level.type, 'ladder'))
|
||||||
|
query = {$and: [{creator: req.user.id}, {'level.original': {$in: levelIDs}}]}
|
||||||
|
cursor = LevelSession.find(query)
|
||||||
|
cursor = cursor.select(req.query.project) if req.query.project
|
||||||
|
cursor.exec (err, documents) =>
|
||||||
return @sendDatabaseError(res, err) if err?
|
return @sendDatabaseError(res, err) if err?
|
||||||
cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
|
cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
|
||||||
@sendSuccess(res, cleandocs)
|
@sendSuccess(res, cleandocs)
|
||||||
|
|
|
@ -111,7 +111,7 @@ describe 'PUT /db/classroom', ->
|
||||||
expect(res.statusCode).toBe(403)
|
expect(res.statusCode).toBe(403)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
describe 'POST /db/classroom/:id/members', ->
|
describe 'POST /db/classroom/~/members', ->
|
||||||
|
|
||||||
it 'clears database users and classrooms', (done) ->
|
it 'clears database users and classrooms', (done) ->
|
||||||
clearModels [User, Classroom], (err) ->
|
clearModels [User, Classroom], (err) ->
|
||||||
|
@ -135,6 +135,36 @@ describe 'POST /db/classroom/:id/members', ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
||||||
|
describe 'DELETE /db/classroom/:id/members', ->
|
||||||
|
|
||||||
|
it 'clears database users and classrooms', (done) ->
|
||||||
|
clearModels [User, Classroom], (err) ->
|
||||||
|
throw err if err
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'removes the given user from the list of members in the classroom', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
data = { name: 'Classroom 6' }
|
||||||
|
request.post {uri: classroomsURL, json: data }, (err, res, body) ->
|
||||||
|
classroomCode = body.code
|
||||||
|
classroomID = body._id
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
loginNewUser (user2) ->
|
||||||
|
url = getURL("/db/classroom/~/members")
|
||||||
|
data = { code: classroomCode }
|
||||||
|
request.post { uri: url, json: data }, (err, res, body) ->
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
Classroom.findById classroomID, (err, classroom) ->
|
||||||
|
expect(classroom.get('members').length).toBe(1)
|
||||||
|
url = getURL("/db/classroom/#{classroom.id}/members")
|
||||||
|
data = { userID: user2.id }
|
||||||
|
request.del { uri: url, json: data }, (err, res, body) ->
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
Classroom.findById classroomID, (err, classroom) ->
|
||||||
|
expect(classroom.get('members').length).toBe(0)
|
||||||
|
done()
|
||||||
|
|
||||||
|
|
||||||
describe 'POST /db/classroom/:id/invite-members', ->
|
describe 'POST /db/classroom/:id/invite-members', ->
|
||||||
|
|
||||||
it 'takes a list of emails and sends invites', (done) ->
|
it 'takes a list of emails and sends invites', (done) ->
|
||||||
|
|
|
@ -85,8 +85,19 @@ describe 'POST /db/course_instance/:id/members', ->
|
||||||
cb()
|
cb()
|
||||||
|
|
||||||
], makeTestIterator(@), done)
|
], makeTestIterator(@), done)
|
||||||
|
|
||||||
|
|
||||||
|
it 'adds the CourseInstance id to the user', (done) ->
|
||||||
|
async.eachSeries([
|
||||||
|
|
||||||
|
addTestUserToClassroom,
|
||||||
|
(test, cb) ->
|
||||||
|
url = getURL("/db/course_instance/#{test.courseInstance.id}/members")
|
||||||
|
request.post {uri: url, json: {userID: test.user.id}}, (err, res, body) ->
|
||||||
|
User.findById(test.user.id).exec (err, user) ->
|
||||||
|
expect(_.size(user.get('courseInstances'))).toBe(1)
|
||||||
|
cb()
|
||||||
|
], makeTestIterator(@), done)
|
||||||
|
|
||||||
it 'return 403 if the member is not in the classroom', (done) ->
|
it 'return 403 if the member is not in the classroom', (done) ->
|
||||||
async.eachSeries([
|
async.eachSeries([
|
||||||
|
|
||||||
|
@ -155,5 +166,67 @@ describe 'POST /db/course_instance/:id/members', ->
|
||||||
test.prepaid.set('redeemers', [{userID: test.user.get('_id')}])
|
test.prepaid.set('redeemers', [{userID: test.user.get('_id')}])
|
||||||
test.prepaid.save cb
|
test.prepaid.save cb
|
||||||
|
|
||||||
|
|
||||||
|
describe 'DELETE /db/course_instance/:id/members', ->
|
||||||
|
|
||||||
|
beforeEach (done) -> clearModels([CourseInstance, Course, User, Classroom, Prepaid], done)
|
||||||
|
beforeEach (done) -> loginJoe (@joe) => done()
|
||||||
|
beforeEach init.course({free: true})
|
||||||
|
beforeEach init.classroom()
|
||||||
|
beforeEach init.courseInstance()
|
||||||
|
beforeEach init.user()
|
||||||
|
beforeEach init.prepaid()
|
||||||
|
|
||||||
|
it 'removes a member to the given CourseInstance', (done) ->
|
||||||
|
async.eachSeries([
|
||||||
|
|
||||||
|
addTestUserToClassroom,
|
||||||
|
addTestUserToCourseInstance,
|
||||||
|
(test, cb) ->
|
||||||
|
url = getURL("/db/course_instance/#{test.courseInstance.id}/members")
|
||||||
|
request.del {uri: url, json: {userID: test.user.id}}, (err, res, body) ->
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(body.members.length).toBe(0)
|
||||||
|
cb()
|
||||||
|
|
||||||
|
], makeTestIterator(@), done)
|
||||||
|
|
||||||
|
it 'removes the CourseInstance from the User.courseInstances', (done) ->
|
||||||
|
async.eachSeries([
|
||||||
|
|
||||||
|
addTestUserToClassroom,
|
||||||
|
addTestUserToCourseInstance,
|
||||||
|
(test, cb) ->
|
||||||
|
User.findById(test.user.id).exec (err, user) ->
|
||||||
|
expect(_.size(user.get('courseInstances'))).toBe(1)
|
||||||
|
cb()
|
||||||
|
removeTestUserFromCourseInstance,
|
||||||
|
(test, cb) ->
|
||||||
|
User.findById(test.user.id).exec (err, user) ->
|
||||||
|
expect(_.size(user.get('courseInstances'))).toBe(0)
|
||||||
|
cb()
|
||||||
|
|
||||||
|
], makeTestIterator(@), done)
|
||||||
|
|
||||||
|
addTestUserToClassroom = (test, cb) ->
|
||||||
|
test.classroom.set('members', [test.user.get('_id')])
|
||||||
|
test.classroom.save cb
|
||||||
|
|
||||||
|
addTestUserToCourseInstance = (test, cb) ->
|
||||||
|
url = getURL("/db/course_instance/#{test.courseInstance.id}/members")
|
||||||
|
request.post {uri: url, json: {userID: test.user.id}}, (err, res, body) ->
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(body.members.length).toBe(1)
|
||||||
|
expect(body.members[0]).toBe(test.user.id)
|
||||||
|
cb()
|
||||||
|
|
||||||
|
removeTestUserFromCourseInstance = (test, cb) ->
|
||||||
|
url = getURL("/db/course_instance/#{test.courseInstance.id}/members")
|
||||||
|
request.del {uri: url, json: {userID: test.user.id}}, (err, res, body) ->
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(body.members.length).toBe(0)
|
||||||
|
cb()
|
||||||
|
|
||||||
|
|
||||||
makeTestIterator = (testObject) -> (func, callback) -> func(testObject, callback)
|
makeTestIterator = (testObject) -> (func, callback) -> func(testObject, callback)
|
||||||
|
|
|
@ -74,15 +74,16 @@ describe 'POST /db/user', ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'serves the user through /db/user/id', (done) ->
|
it 'serves the user through /db/user/id', (done) ->
|
||||||
unittest.getNormalJoe (user) ->
|
request.post getURL('/auth/logout'), ->
|
||||||
url = getURL(urlUser+'/'+user._id)
|
unittest.getNormalJoe (user) ->
|
||||||
request.get url, (err, res, body) ->
|
url = getURL(urlUser+'/'+user._id)
|
||||||
expect(res.statusCode).toBe(200)
|
request.get url, (err, res, body) ->
|
||||||
user = JSON.parse(body)
|
expect(res.statusCode).toBe(200)
|
||||||
expect(user.name).toBe('Joe') # Anyone should be served the username.
|
user = JSON.parse(body)
|
||||||
expect(user.email).toBeUndefined() # Shouldn't be available to just anyone.
|
expect(user.name).toBe('Joe') # Anyone should be served the username.
|
||||||
expect(user.passwordHash).toBeUndefined()
|
expect(user.email).toBeUndefined() # Shouldn't be available to just anyone.
|
||||||
done()
|
expect(user.passwordHash).toBeUndefined()
|
||||||
|
done()
|
||||||
|
|
||||||
it 'creates admins based on passwords', (done) ->
|
it 'creates admins based on passwords', (done) ->
|
||||||
request.post getURL('/auth/logout'), ->
|
request.post getURL('/auth/logout'), ->
|
||||||
|
@ -353,12 +354,14 @@ describe 'Statistics', ->
|
||||||
session.save (err) ->
|
session.save (err) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
|
|
||||||
User.findById joe.get('id'), (err, guy) ->
|
f = ->
|
||||||
expect(err).toBeNull()
|
User.findById joe.get('id'), (err, guy) ->
|
||||||
expect(guy.get 'id').toBe joe.get 'id'
|
expect(err).toBeNull()
|
||||||
expect(guy.get 'stats.gamesCompleted').toBe 1
|
expect(guy.get 'id').toBe joe.get 'id'
|
||||||
|
expect(guy.get 'stats.gamesCompleted').toBe 1
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
setTimeout f, 100
|
||||||
|
|
||||||
it 'recalculates games completed', (done) ->
|
it 'recalculates games completed', (done) ->
|
||||||
unittest.getNormalJoe (joe) ->
|
unittest.getNormalJoe (joe) ->
|
||||||
|
|