Merge branch 'courses-vhoc'

This commit is contained in:
Scott Erickson 2015-12-02 13:16:24 -08:00
commit 3c1a55fc2f
64 changed files with 2231 additions and 600 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View file

@ -70,7 +70,8 @@ module.exports = class CocoRouter extends Backbone.Router
'courses/teachers': go('courses/TeacherCoursesView')
'courses/purchase': go('courses/PurchaseCoursesView')
'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'
'demo(/*subpath)': go('DemoView')

View file

@ -886,6 +886,7 @@
courses:
course: "Course"
courses: "courses"
create_new_class: "Create New Class"
not_enrolled: "You are not enrolled in this course."
visit_pref: "Please visit the"
visit_suf: "page to enroll."
@ -1363,6 +1364,15 @@
campaigns: "Campaigns"
poll: "Poll"
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:
advanced_strings: "Advanced Strings"

View file

@ -1,5 +1,7 @@
CocoModel = require './CocoModel'
schema = require 'schemas/models/campaign.schema'
Level = require 'models/Level'
CocoCollection = require 'collections/CocoCollection'
module.exports = class Campaign extends CocoModel
@className: 'Campaign'
@ -8,3 +10,29 @@ module.exports = class Campaign extends CocoModel
saveBackups: true
@denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position', 'rewards']))
@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

View file

@ -5,3 +5,21 @@ module.exports = class Classroom extends CocoModel
@className: 'Classroom'
@schema: schema
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)

View file

@ -5,3 +5,32 @@ module.exports = class CourseInstance extends CocoModel
@className: 'CourseInstance'
@schema: schema
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}"

View file

@ -16,6 +16,14 @@ module.exports = class User extends CocoModel
isInGodMode: -> 'godmode' in @get('permissions', true)
isAnonymous: -> @get('anonymous', 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) ->
photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null

View file

@ -393,3 +393,7 @@ body > iframe[src^="https://apis.google.com"]
top: 0
left: 0
pointer-events: none
// TODO: update Bootstrap, remove this
.text-uppercase
text-transform: uppercase

View file

@ -0,0 +1,7 @@
#activate-licenses-modal
h2
margin-top: -20px
.well
max-height: 200px
overflow: scroll

View file

@ -0,0 +1,3 @@
#change-course-language-modal
p
margin: 30px 0

View file

@ -0,0 +1,7 @@
#choose-language-modal
button
margin: 20px 0 10px
.progress
width: 50%
margin: 50px 25%

View file

@ -0,0 +1,7 @@
#classroom-view
#main-button-area
.btn
margin-left: 10px
.progress
margin-bottom: 5px

View file

@ -140,3 +140,25 @@
.settings-name-input
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

View file

@ -1,3 +1,50 @@
#courses-view
.row
margin-top: 40px
#site-content-area
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

View file

@ -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

View 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

View 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

View file

@ -0,0 +1,3 @@
#remove-student-modal
.glyphicon-warning-sign
font-size: 40px

View file

@ -0,0 +1,7 @@
#student-log-in-modal
#log-in-btn
min-width: 30%
margin-bottom: 10px
.form
margin: 0 25%

View 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

View file

@ -1,26 +1,70 @@
#teacher-courses-view
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
width: 300px
.edit-classroom-small
cursor: pointer
&:hover
color: grey
#fixed-area
position: fixed
bottom: 0
left: 0
right: 0
.well
margin-bottom: 0
padding: 5px
.col-sm-5
padding-top: 8px
.progress
margin-bottom: 0
.no-students
font-size: 22px
font-style: italic
margin: 10px
text-align: center
.section-header
border-bottom: 1px solid black
font-size: 20px
font-weight: bold
margin-bottom: 20px
text-transform: uppercase
.text-center
text-align: center
.uppercase
text-transform: uppercase
.welcome
font-size: 24px
font-weight: bold
margin-bottom: 20px

View file

@ -1,14 +1,18 @@
#teachers-view
.main-content-area
width: 650px
box-shadow: 0px 0px 0px
.bigger-text
font-size: 18px
table
background-color: #F9F1DD
.uppercase
text-transform: uppercase
.discount-table
width: 50%
.btn-create-account
margin: 4px
min-width: 300px
.teachers-title
color: green
.btn-login-account
margin: 4px
min-width: 300px
.text-center
text-align: center

View 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

View 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

View 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

View file

@ -1,24 +1,30 @@
extends /templates/core/modal-base
block modal-header-content
button.close(data-dismiss='modal')
span ×
h3.modal-title(data-i18n="courses.edit_settings1")
if view.classroom
h3.modal-title(data-i18n="courses.edit_settings1")
else
h3.modal-title(data-i18n="courses.create_new_class")
block modal-body-content
.form
.form-group
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
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
label(data-i18n="choose_hero.programming_language")
select.form-control#programming-language-select
- var aceConfig = view.classroom.get('aceConfig') || {};
option(value="python", selected=aceConfig.language==='python') Python
option(value="javascript", selected=aceConfig.language==='javascript') JavaScript
- var aceConfig = view.classroom ? view.classroom.get('aceConfig') || {} : {};
option(value="python", selected=aceConfig.language==='python') Learn Python
option(value="javascript", selected=aceConfig.language==='javascript') Learn JavaScript
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")

View 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

View file

@ -2,11 +2,6 @@ extends /templates/base
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
h1= course.get('name')
if noCourseInstance
@ -31,43 +26,110 @@ block content
else if !course || !courseInstance
h1(data-i18n="common.loading") Loading...
else
h1
| #{course.get('name')}
small.spl
p
// TODO: format this text all good and stuff
strong
if courseInstance.get('name')
| (#{courseInstance.get('name')})
span= courseInstance.get('name')
else if view.classroom.get('name')
| (#{view.classroom.get('name')})
span= view.classroom.get('name')
else
| (
span(data-i18n='courses.unnamed_class')
| )
if !view.owner.isNew()
p
span.spr Creator:
a(href="/user/#{view.owner.id}")
if !view.owner.isNew() && view.getOwnerName()
span.spl.spr - Teacher:
//a(href="/user/#{view.owner.id}") // Don't link to profiles until we improve them
span
strong= view.getOwnerName()
h1
| #{course.get('name')}
if view.courseComplete
span.spl - Complete!
p
if courseInstance.get('description')
each line in courseInstance.get('description').split('\n')
div= line
div.well.well-sm(role='tabpanel')
ul.nav.nav-pills(role='tablist')
if adminMode
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")
if view.courseComplete && !view.teacherMode
.jumbotron
if promptForSchool
.row
.col-md-6.col-md-offset-3
form.form#school-form
.form-group
label.control-label(for="course-complete-school-input")
span.spr(data-i18n="signup.school_name") School Name and City
em.optional-note
| (
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 Youre 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
if adminMode
if view.teacherMode
.tab-pane.active#progress(role='tabpanel')
+progress-tab
.tab-pane#levels(role='tabpanel')
@ -243,7 +305,7 @@ mixin progress-members-popup-completed(i, level, session)
p
span.spr(data-i18n="courses.completed")
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")
mixin progress-members-popup-started(i, level, session)
@ -255,7 +317,7 @@ mixin progress-members-popup-started(i, level, session)
p
span.spr(data-i18n="clans.last_played")
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")
mixin levels-tab
@ -273,7 +335,7 @@ mixin levels-tab
each level, levelID in campaign.get('levels')
tr
td
if lastLevelCompleted || adminMode
if lastLevelCompleted || view.teacherMode
- 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)
td
@ -288,4 +350,3 @@ mixin levels-tab
each concept in course.get('concepts')
if levelConceptMap[levelID][concept]
span.spr.progress-level-cell.progress-level-cell-not-started(data-i18n="concepts." + concept)

View file

@ -1,12 +1,165 @@
extends /templates/base
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
.row
.col-sm-6.text-center
a(href="/courses/students").btn.btn-default Students Click Here
#main-content
if me.isAnonymous()
h1.text-center Adventurers, welcome to Courses!
.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
a(href="/courses/teachers").btn.btn-default Teachers Click Here
else
- 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

View file

@ -1,11 +1,66 @@
extends /templates/base
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
.container-fluid
.row
.col-md-6.text-center
button#student-btn.btn.btn-lg.btn-success(data-i18n="courses.students_click")
.col-md-6.text-center
a.btn.btn-lg.btn-default(data-i18n="courses.teachers_click", href="/courses/teachers?hoc=true")
h1.text-center Adventurers, welcome to our Hour of Code!
#main-content
.well.text-center
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!)

View file

@ -1,17 +1,13 @@
extends /templates/core/modal-base
block modal-header-content
h2 Invite Students to Classroom
h2 Add Students
h3= view.classroom.get('name')
block modal-body-content
p(data-i18n="courses.invite_students")
h3(data-i18n="courses.invite_link_header")
p(data-i18n="courses.invite_link_p_1")
.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")
h3 Option 1: Invite students via email
p Students will automatically be sent an invitation to join this class, and will
| need to create an account with a username and password.
.form
.form-group
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")
#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")
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

View file

@ -6,36 +6,43 @@ block content
p.text-center Purchasing...
.progress.progress-striped.active
.progress-bar(style="width: 100%")
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
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
h3.text-center Purchase Courses for Students
h2.text-center Purchase Student Enrollments
br
if view.state === 'error'
.alert.alert-danger= view.stateMessage
.form-horizontal
.form-group
label.col-sm-3.control-label Students
.col-sm-6
input#students-input.form-control(
placeholder='<number of seats>'
value=view.numberOfStudents
type='number'
)
.help-block Each student will have access to all courses.
#price-form-group.form-group
label.col-sm-3.control-label Price
.col-sm-6
.form-control-static
| #{view.getPriceString()} ($#{view.pricePerStudent.toFixed(2)} per student)
.form-group
.col-sm-offset-3.col-sm-10
button#purchase-btn.btn.btn-primary Purchase
p.text-center
strong How many enrollments do you need?
br
p.text-center
input#students-input(
placeholder='<number of enrollments>'
value=view.numberOfStudents
type='number'
)
br
.container-fluid
.row
.col-md-offset-3.col-md-6 One enrollment per student is required to assign them to paid CodeCombat courses. A single student does not need multiple enrollments to access all paid courses.
br
p.text-center#price-form-group
strong Total: #{view.numberOfStudents} enrollments x $#{view.pricePerStudent.toFixed(2)} = #{view.getPriceString()}
p.text-center
button#purchase-btn.btn.btn-lg.btn-success.uppercase Purchase Now

View 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

View 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

View 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

View file

@ -2,188 +2,104 @@ extends /templates/base
block content
if view.hoc
h1 Welcome to Hour of Code!
p
| Thank you for choosing CodeCombat for your students.
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.
.text-center
if me.isAnonymous() || !me.get('name')
.welcome Welcome!
else
.welcome Welcome, #{me.get('name')}!
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.
- var totalRedeemers = view.prepaids.totalRedeemers();
- var totalMaxRedeemers = view.prepaids.totalMaxRedeemers();
.text-right
span.spr Used paid seats: #{totalRedeemers}/#{totalMaxRedeemers}
a.btn.btn-default.btn-xs(href="/courses/purchase") Add
for classroom in view.classrooms.models
h2
span.spr= classroom.get('name')
- var language = (classroom.get('aceConfig') || {}).language || 'python';
if language === 'python'
img(src="/images/common/code_languages/python_icon.png")
if language === 'javascript'
img(src="/images/common/code_languages/javascript_icon.png")
small.spl.edit-classroom-small(data-classroom-id=classroom.id)
span.glyphicon.glyphicon-pencil
- 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
.section-header Your Classes
if view.classrooms.models.length > 0
.container-fluid
each classroom in view.classrooms.models
+classroom(classroom)
else
.no-students No classes yet!
.text-center
button.btn.btn-lg.btn-success.uppercase.create-new-class create new class
br
.section-header Available Courses
.container-fluid
- var courses = view.courses.models;
- var i = 0;
while i < courses.length
- var course = courses[i];
- i++;
.row
.col-sm-3.col-sm-offset-3
button#create-new-class-btn.btn.btn-default.btn-block Create New Class
.col-sm-3
input#new-classroom-name-input.form-control(placeholder='new class name')
#fixed-area
.container
.row.well
if view.state === 'saving-changes'
p Saving changes
- var total = view.membershipAdditions.originalSize + view.usersToRedeem.originalSize;
- var left = view.membershipAdditions.size() + view.usersToRedeem.size();
- var pct = Math.max(10, (100 * (total - left) / total)).toFixed(1) + '%';
.progress.progress-striped.active
.progress-bar(style="width: #{pct}")
else
- var seatsLeft = totalMaxRedeemers - totalRedeemers - view.usersToRedeem.size();
if seatsLeft < 0
.alert.alert-danger
span.spr You do not have enough seats to accommodate all students you have selected.
a(href="/courses/purchase") Buy more seats.
else
.col-sm-2
button#save-changes-btn.btn.btn-primary.btn-block(disabled=!view.numCourseInstancesToAddTo) Save Changes
.col-sm-5
| Students to add to courses: #{view.numCourseInstancesToAddTo || 0}
.col-sm-5
| Seats to expend: #{view.usersToRedeem.size()} (will have #{seatsLeft} seats left)
block footer
.col-md-6
+course-info(course)
if i < courses.length
- course = courses[i];
- i++;
.col-md-6
+course-info(course)
block footer
mixin classroom(classroom)
.row
- var classMemberCount = classroom.get('members') ? classroom.get('members').length : 0;
if classMemberCount > 0
.col-md-8
p
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}")
.active-courses active courses
- var courseInstances = view.courseInstances.where({classroomID: classroom.id});
each courseInstance in courseInstances
+course(courseInstance, classMemberCount)
else
.col-md-12
p
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

View file

@ -28,7 +28,7 @@ block outer_content
#hour-of-code
h1(data-i18n="home.hoc_title")
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")
//- #slogan(data-i18n="home.slogan")

View file

@ -12,10 +12,11 @@ block modal-body-content
#victory-text= victoryText
if isCourseLevel
if currentCourseName
p
span.spr.level-title(data-i18n="play_level.course")
span.level-name= currentCourseName
.course-name-container
if currentCourseName
p
span.spr.level-title(data-i18n="play_level.course")
span.level-name= currentCourseName
.container-fluid
.row
.col-md-6
@ -26,6 +27,10 @@ block modal-body-content
if nextLevelName
.level-title(data-i18n="play_level.next_level")
.level-name= nextLevelName.replace('Course: ', '')
else
.level-title(data-i18n="play_level.course")
.level-name(data-i18n="play_level.victory_title_suffix")
br
#level-feedback

View file

@ -1,35 +1,43 @@
extends /templates/base
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
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.teachers-title(data-i18n="teachers.teacher_subs_title")
p.bigger-text
strong Teachers
span - share CodeCombat's Hour of Code with your class!
.container-fluid
.row
.col-md-6
.bigger-text
div In just one hour, students will learn:
ul
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
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.
@ -39,23 +47,28 @@ block content
a(href='/teachers/freetrial', data-i18n="teachers.teacher_subs_2")
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")
p
span.spr(data-i18n="teachers.more_info_1")
a(href='http://discourse.codecombat.com/c/teachers', data-i18n="teachers.more_info_2")
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
| .

View file

@ -1,6 +1,18 @@
AuthModal = require 'views/core/AuthModal'
RootView = require 'views/core/RootView'
template = require 'templates/teachers'
module.exports = class TeachersView extends RootView
id: 'teachers-view'
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')

View 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'

View 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()

View 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()

View file

@ -1,10 +1,11 @@
Classroom = require 'models/Classroom'
ModalView = require 'views/core/ModalView'
template = require 'templates/courses/classroom-settings-modal'
module.exports = class AddLevelSystemModal extends ModalView
id: 'classroom-settings-modal'
template: template
events:
'click #save-settings-btn': 'onClickSaveSettingsButton'
@ -12,15 +13,16 @@ module.exports = class AddLevelSystemModal extends ModalView
@classroom = options.classroom
onClickSaveSettingsButton: ->
return unless @classroom
if name = $('.settings-name-input').val()
name = $('.settings-name-input').val()
unless @classroom
return unless name
@classroom = new Classroom({ name: name })
if name
@classroom.set('name', name)
description = $('.settings-description-input').val()
@classroom.set('description', description)
@classroom.set('aceConfig', {
language: @$('#programming-language-select').val()
})
@classroom.patch()
@classroom.save()
@hide()

View 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()

View file

@ -9,12 +9,16 @@ template = require 'templates/courses/course-details'
User = require 'models/User'
utils = require 'core/utils'
Prepaid = require 'models/Prepaid'
storage = require 'core/storage'
autoplayedOnce = false
module.exports = class CourseDetailsView extends RootView
id: 'course-details-view'
template: template
teacherMode: false
singlePlayerMode: false
memberSort: 'nameAsc'
events:
'change .progress-expand-checkbox': 'onCheckExpandedProgress'
@ -25,14 +29,13 @@ module.exports = class CourseDetailsView extends RootView
'click .progress-level-cell': 'onClickProgressLevelCell'
'mouseenter .progress-level-cell': 'onMouseEnterPoint'
'mouseleave .progress-level-cell': 'onMouseLeavePoint'
'submit #school-form': 'onSubmitSchoolForm'
constructor: (options, @courseID, @courseInstanceID) ->
super options
@courseID ?= options.courseID
@courseInstanceID ?= options.courseInstanceID
@classroom = new Classroom()
@adminMode = me.isAdmin()
@memberSort = 'nameAsc'
@course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID
@listenTo @course, 'sync', @onCourseSync
@prepaid = new Prepaid()
@ -43,7 +46,6 @@ module.exports = class CourseDetailsView extends RootView
getRenderData: ->
context = super()
context.adminMode = @adminMode ? false
context.campaign = @campaign
context.conceptsCompleted = @conceptsCompleted ? {}
context.course = @course if @course?.loaded
@ -62,13 +64,22 @@ module.exports = class CourseDetailsView extends RootView
context.userConceptStateMap = @userConceptStateMap ? {}
context.userLevelStateMap = @userLevelStateMap ? {}
context.document = document
context.promptForSchool = @courseComplete and not me.isAnonymous() and not me.get('schoolName') and not storage.load('no-school')
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: ->
return if @destroyed
# console.log 'onCourseSync'
if me.isAnonymous() and (not me.get('hourOfCode') and not @course.get('hourOfCode'))
@noCourseInstance = true
@render?()
@render()
return
return if @campaign?
campaignID = @course.get('campaignID')
@ -78,24 +89,36 @@ module.exports = class CourseDetailsView extends RootView
@onCampaignSync()
else
@supermodel.loadModel @campaign, 'campaign'
@render?()
@render()
onCampaignSync: ->
return if @destroyed
# console.log 'onCampaignSync'
if @courseInstanceID
@loadCourseInstance(@courseInstanceID)
else unless me.isAnonymous()
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
@supermodel.loadCollection(@courseInstances, 'course_instances')
@loadCourseInstances()
@levelConceptMap = {}
for levelID, level of @campaign.get('levels')
@levelConceptMap[levelID] ?= {}
for concept in level.concepts
@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) ->
return if @destroyed
# console.log 'loadCourseInstance'
return if @courseInstance?
@courseInstanceID = courseInstanceID
@ -107,23 +130,29 @@ module.exports = class CourseDetailsView extends RootView
@courseInstance = @supermodel.loadModel(@courseInstance, 'course_instance').model
onCourseInstancesSync: ->
return if @destroyed
# console.log 'onCourseInstancesSync'
if @courseInstances.models.length is 1
@loadCourseInstance(@courseInstances.models[0].id)
else
if @courseInstances.models.length is 0
@noCourseInstance = true
@findNextCourseInstance()
if not @courseInstance
# We are loading these to find the one we want to display.
if @courseInstances.models.length is 1
@loadCourseInstance(@courseInstances.models[0].id)
else
@noCourseInstanceSelected = true
@render?()
if @courseInstances.models.length is 0
@noCourseInstance = true
else
@noCourseInstanceSelected = true
@render()
onCourseInstanceSync: ->
return if @destroyed
# console.log 'onCourseInstanceSync'
if @courseInstance.get('classroomID')
@classroom = new Classroom({_id: @courseInstance.get('classroomID')})
@supermodel.loadModel @classroom, 'classroom'
@adminMode = true if @courseInstance.get('ownerID') is me.id and @courseInstance.get('name') isnt 'Single Player'
@levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator:'_id' })
@singlePlayerMode = @courseInstance.get('name') is 'Single Player'
@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
@supermodel.loadCollection @levelSessions, 'level_sessions', cache: false
@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
@owner = new User({_id: @courseInstance.get('ownerID')})
@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
@listenTo @prepaid, 'sync', @onPrepaidSync
if @prepaid.loaded
@onPrepaidSync()
else
@supermodel.loadModel @prepaid, 'prepaid'
@render?()
@render()
onPrepaidSync: ->
@render?()
return if @destroyed
# TODO: why do we rerender here? Template doesn't use prepaid.
@render()
onLevelSessionsSync: ->
return if @destroyed
# console.log 'onLevelSessionsSync'
@instanceStats = averageLevelsCompleted: 0, furthestLevelCompleted: '', totalLevelsCompleted: 0, totalPlayTime: 0
@memberStats = {}
@ -152,17 +184,28 @@ module.exports = class CourseDetailsView extends RootView
@userLevelStateMap = {}
levelStateMap = {}
for levelSession in @levelSessions.models
continue if levelSession.skipMe # Don't track second arena session as another completed level
userID = levelSession.get('creator')
levelID = levelSession.get('level').original
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
@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++ if state is 'complete'
@memberStats[userID].totalPlayTime += parseInt(levelSession.get('playtime') ? 0)
@memberStats[userID].totalPlayTime += playtime
@userConceptStateMap[userID] ?= {}
for concept of @levelConceptMap[levelID]
@ -185,36 +228,58 @@ module.exports = class CourseDetailsView extends RootView
for concept, state of conceptStateMap
@conceptsCompleted[concept] ?= 0
@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 @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
@$el.find('button.btn-play-level').click()
onMembersSync: ->
return if @destroyed
# console.log 'onMembersSync'
@memberUserMap = {}
for user in @members.models
@memberUserMap[user.id] = user
@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) ->
@showExpandedProgress = $('.progress-expand-checkbox').prop('checked')
# TODO: why does render reset the checkbox to be unchecked?
@render?()
@render()
$('.progress-expand-checkbox').attr('checked', @showExpandedProgress)
onClickMemberHeader: (e) ->
@memberSort = if @memberSort is 'nameAsc' then 'nameDesc' else 'nameAsc'
@sortMembers()
@render?()
@render()
onClickProgressHeader: (e) ->
@memberSort = if @memberSort is 'progressAsc' then 'progressDesc' else 'progressAsc'
@sortMembers()
@render?()
@render()
onClickPlayLevel: (e) ->
levelSlug = $(e.target).data('level-slug')
@ -224,7 +289,7 @@ module.exports = class CourseDetailsView extends RootView
viewClass = 'views/ladder/LadderView'
viewArgs = [{supermodel: @supermodel}, 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
viewArgs = viewArgs.concat ['course', @courseInstance.id]
else
@ -242,7 +307,7 @@ module.exports = class CourseDetailsView extends RootView
@loadCourseInstance(courseInstanceID)
onClickProgressLevelCell: (e) ->
return unless @adminMode
return unless @teacherMode or me.isAdmin()
levelID = $(e.currentTarget).data('level-id')
levelSlug = $(e.currentTarget).data('level-slug')
userID = $(e.currentTarget).data('user-id')
@ -302,8 +367,17 @@ module.exports = class CourseDetailsView extends RootView
aName.localeCompare(bName)
getOwnerName: ->
if @owner.isNew()
return '?'
return if @owner.isNew()
if @owner.get('firstName') and @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')

View file

@ -2,7 +2,157 @@ app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
RootView = require 'views/core/RootView'
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
id: 'courses-view'
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, @

View file

@ -6,7 +6,12 @@ CourseInstance = require 'models/CourseInstance'
RootView = require 'views/core/RootView'
template = require 'templates/courses/hour-of-code-view'
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
id: 'hour-of-code-view'
@ -14,11 +19,40 @@ module.exports = class HourOfCodeView extends RootView
events:
'click #student-btn': 'onClickStudentButton'
'click #start-new-game-btn': 'onClickStartNewGameButton'
'click #log-in-btn': 'onClickLogInButton'
'click #log-out-link': 'onClickLogOutLink'
constructor: (options) ->
super(options)
initialize: ->
@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: ->
# 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'))
@ -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;">'))
application.tracker?.trackEvent 'Hour of Code Begin'
onClickStudentButton: ->
@state = 'enrolling'
@stateMessage = undefined
@render?()
$.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
})
onClickStartNewGameButton: ->
# user without hour of code course instance, creates one, starts playing
modal = new ChooseLanguageModal({
logoutFirst: @hourOfCodeCourseInstance?
})
@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()

View file

@ -7,9 +7,12 @@ module.exports = class InviteToClassroomModal extends ModalView
events:
'click #send-invites-btn': 'onClickSendInvitesButton'
'click #copy-url-btn': 'onClickCopyURLButton'
initialize: (options) ->
@classroom = options.classroom
@classCode = @classroom.get('codeCamel') || @classroom.get('code')
@joinURL = document.location.origin + "/courses?_cc=" + @classCode
onClickSendInvitesButton: ->
emails = @$('#invite-emails-textarea').val()
@ -30,3 +33,12 @@ module.exports = class InviteToClassroomModal extends ModalView
@$('#invite-emails-sending-alert').addClass('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')

View file

@ -15,6 +15,7 @@ module.exports = class PurchaseCoursesView extends RootView
initialize: (options) ->
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
@fromClassroom = utils.getQueryVariable('from-classroom')
super(options)
events:

View 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()

View 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?()

View 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()

View file

@ -4,13 +4,11 @@ CocoCollection = require 'collections/CocoCollection'
CocoModel = require 'models/CocoModel'
Course = require 'models/Course'
Classroom = require 'models/Classroom'
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
User = require 'models/User'
Prepaid = require 'models/Prepaid'
CourseInstance = require 'models/CourseInstance'
RootView = require 'views/core/RootView'
template = require 'templates/courses/teacher-courses-view'
utils = require 'core/utils'
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
module.exports = class TeacherCoursesView extends RootView
@ -18,11 +16,8 @@ module.exports = class TeacherCoursesView extends RootView
template: template
events:
'click #create-new-class-btn': 'onClickCreateNewclassButton'
'click .add-students-btn': 'onClickAddStudentsButton'
'click .course-instance-membership-checkbox': 'onClickCourseInstanceMembershipCheckbox'
'click #save-changes-btn': 'onClickSaveChangesButton'
'click #manage-tab-link': 'onClickManageTabLink'
'click .btn-add-students': 'onClickAddStudents'
'click .create-new-class': 'onClickCreateNewClassButton'
'click .edit-classroom-small': 'onClickEditClassroomSmall'
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')
@supermodel.loadCollection(@courseInstances, 'course_instances', {data: {ownerID: me.id}})
@members = new CocoCollection([], { model: 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}})
@listenTo @members, 'sync', @renderManageTab
@usersToRedeem = new CocoCollection([], { model: User })
@hoc = utils.getQueryVariable('hoc')
@listenTo @members, 'sync', @render
@
onceClassroomsSync: ->
@ -56,160 +43,28 @@ module.exports = class TeacherCoursesView extends RootView
url: "/db/classroom/#{classroom.id}/members"
})
onClickCreateNewclassButton: ->
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) ->
onClickAddStudents: (e) ->
classroomID = $(e.target).data('classroom-id')
classroom = @classrooms.get(classroomID)
unless classroom
console.error 'No classroom ID found.'
return
modal = new InviteToClassroomModal({classroom: classroom})
@openModalView(modal)
onLoaded: ->
super()
@linkCourseIntancesToCourses()
@fillMissingCourseInstances()
onClickCreateNewClassButton: ->
return @openModalView new AuthModal() if me.get('anonymous')
modal = new ClassroomSettingsModal({})
@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: ->
for courseInstance in @courseInstances.models
courseInstance.course = @courses.get(courseInstance.get('courseID'))
fillMissingCourseInstances: ->
# TODO: Give teachers control over which courses are enabled for a given class.
# 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')
onClickEditClassroomSmall: (e) ->
classroomID = $(e.target).data('classroom-id')
classroom = @classrooms.get(classroomID)
modal = new ClassroomSettingsModal({classroom: classroom})
@openModalView(modal)
@listenToOnce modal, 'hide', @render

View file

@ -63,9 +63,10 @@ module.exports = class HeroVictoryModal extends ModalView
else
@readyToContinue = true
@playSound 'victory'
if @level.get('type', true) is 'course' and nextLevel = @level.get('nextLevel')
@nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}"
@nextLevel = @supermodel.loadModel(@nextLevel, 'level').model
if @level.get('type', true) is 'course'
if nextLevel = @level.get('nextLevel')
@nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}"
@nextLevel = @supermodel.loadModel(@nextLevel, 'level').model
if @courseID
@course = new Course().setURL "/db/course/#{@courseID}"
@course = @supermodel.loadModel(@course, 'course').model

View file

@ -46,6 +46,8 @@ CampaignHandler = class CampaignHandler extends Handler
query = i18nCoverage: {$exists: true}
if req.query.project
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.exec (err, documents) =>
return @sendDatabaseError(res, err) if err

View file

@ -36,6 +36,7 @@ ClassroomHandler = class ClassroomHandler extends Handler
method = req.method.toLowerCase()
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 @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'
super(arguments...)
@ -65,6 +66,25 @@ ClassroomHandler = class ClassroomHandler extends Handler
classroom.set('members', members)
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) ->
if req.user?.isAdmin() or req.user?.get('_id').equals(doc.get('ownerID'))
return doc.toObject()
@ -104,6 +124,11 @@ ClassroomHandler = class ClassroomHandler extends Handler
Classroom.find {members: mongoose.Types.ObjectId(memberID)}, (err, classrooms) =>
return @sendDatabaseError(res, err) if err
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
super(arguments...)

View file

@ -35,9 +35,11 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
return @createHOCAPI(req, res) if relationship is 'create-for-hoc'
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 @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 @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
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'
super arguments...
@ -93,6 +95,30 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
return @sendDatabaseError(res, err) if err
@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) ->
return @sendBadInputError(res, 'No classroomID') unless req.body.classroomID
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'))
memberIDs = _.map courseInstance.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID
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?
cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
@sendSuccess(res, cleandocs)

View file

@ -111,7 +111,7 @@ describe 'PUT /db/classroom', ->
expect(res.statusCode).toBe(403)
done()
describe 'POST /db/classroom/:id/members', ->
describe 'POST /db/classroom/~/members', ->
it 'clears database users and classrooms', (done) ->
clearModels [User, Classroom], (err) ->
@ -135,6 +135,36 @@ describe 'POST /db/classroom/:id/members', ->
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', ->
it 'takes a list of emails and sends invites', (done) ->

View file

@ -85,8 +85,19 @@ describe 'POST /db/course_instance/:id/members', ->
cb()
], 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) ->
async.eachSeries([
@ -155,5 +166,67 @@ describe 'POST /db/course_instance/:id/members', ->
test.prepaid.set('redeemers', [{userID: test.user.get('_id')}])
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)

View file

@ -74,15 +74,16 @@ describe 'POST /db/user', ->
done()
it 'serves the user through /db/user/id', (done) ->
unittest.getNormalJoe (user) ->
url = getURL(urlUser+'/'+user._id)
request.get url, (err, res, body) ->
expect(res.statusCode).toBe(200)
user = JSON.parse(body)
expect(user.name).toBe('Joe') # Anyone should be served the username.
expect(user.email).toBeUndefined() # Shouldn't be available to just anyone.
expect(user.passwordHash).toBeUndefined()
done()
request.post getURL('/auth/logout'), ->
unittest.getNormalJoe (user) ->
url = getURL(urlUser+'/'+user._id)
request.get url, (err, res, body) ->
expect(res.statusCode).toBe(200)
user = JSON.parse(body)
expect(user.name).toBe('Joe') # Anyone should be served the username.
expect(user.email).toBeUndefined() # Shouldn't be available to just anyone.
expect(user.passwordHash).toBeUndefined()
done()
it 'creates admins based on passwords', (done) ->
request.post getURL('/auth/logout'), ->
@ -353,12 +354,14 @@ describe 'Statistics', ->
session.save (err) ->
expect(err).toBeNull()
User.findById joe.get('id'), (err, guy) ->
expect(err).toBeNull()
expect(guy.get 'id').toBe joe.get 'id'
expect(guy.get 'stats.gamesCompleted').toBe 1
done()
f = ->
User.findById joe.get('id'), (err, guy) ->
expect(err).toBeNull()
expect(guy.get 'id').toBe joe.get 'id'
expect(guy.get 'stats.gamesCompleted').toBe 1
done()
setTimeout f, 100
it 'recalculates games completed', (done) ->
unittest.getNormalJoe (joe) ->