Merge branch 'master' into production
Before Width: | Height: | Size: 710 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 349 KiB After Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 509 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 95 KiB |
|
@ -70,7 +70,8 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'courses/teachers': go('courses/TeacherCoursesView')
|
||||
'courses/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')
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
7
app/styles/courses/activate-licenses-modal.sass
Normal file
|
@ -0,0 +1,7 @@
|
|||
#activate-licenses-modal
|
||||
h2
|
||||
margin-top: -20px
|
||||
|
||||
.well
|
||||
max-height: 200px
|
||||
overflow: scroll
|
3
app/styles/courses/change-course-language-modal.sass
Normal file
|
@ -0,0 +1,3 @@
|
|||
#change-course-language-modal
|
||||
p
|
||||
margin: 30px 0
|
7
app/styles/courses/choose-language-modal.sass
Normal file
|
@ -0,0 +1,7 @@
|
|||
#choose-language-modal
|
||||
button
|
||||
margin: 20px 0 10px
|
||||
|
||||
.progress
|
||||
width: 50%
|
||||
margin: 50px 25%
|
7
app/styles/courses/classroom-view.sass
Normal file
|
@ -0,0 +1,7 @@
|
|||
#classroom-view
|
||||
#main-button-area
|
||||
.btn
|
||||
margin-left: 10px
|
||||
|
||||
.progress
|
||||
margin-bottom: 5px
|
|
@ -140,3 +140,25 @@
|
|||
|
||||
.settings-name-input
|
||||
width: 50%
|
||||
|
||||
.jumbotron
|
||||
form
|
||||
margin-top: -40px
|
||||
margin-bottom: 20px
|
||||
|
||||
.btn
|
||||
margin-top: -15px
|
||||
|
||||
.no-school
|
||||
width: 30%
|
||||
|
||||
.save-school
|
||||
margin-left: 10%
|
||||
width: 60%
|
||||
|
||||
.btn:not(.btn-submit)
|
||||
white-space: normal
|
||||
min-height: 200px
|
||||
|
||||
h1
|
||||
font-size: 48px
|
||||
|
|
|
@ -1,3 +1,50 @@
|
|||
#courses-view
|
||||
.row
|
||||
#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
|
|
@ -1 +1,19 @@
|
|||
//#hour-of-code-view
|
||||
#hour-of-code-view
|
||||
|
||||
hr
|
||||
border-top: 1px solid grey
|
||||
margin: 30px 20px
|
||||
|
||||
#site-content-area
|
||||
padding: 20px 300px
|
||||
|
||||
h1
|
||||
margin-bottom: 40px
|
||||
p
|
||||
margin: 20px
|
||||
|
||||
h3
|
||||
margin-top: 50px
|
||||
|
||||
ul
|
||||
margin-bottom: 50px
|
15
app/styles/courses/invite-to-classroom-modal.sass
Normal file
|
@ -0,0 +1,15 @@
|
|||
#invite-to-classroom-modal
|
||||
|
||||
.modal-dialog
|
||||
width: 700px
|
||||
|
||||
#class-code-well
|
||||
margin: 0 auto
|
||||
text-align: center
|
||||
font-weight: bold
|
||||
display: inline-block
|
||||
|
||||
#copied-alert, #copy-failed-alert
|
||||
margin-top: 10px
|
||||
padding: 5px
|
||||
display: inline-block
|
14
app/styles/courses/mock1/purchase-courses-view.sass
Normal file
|
@ -0,0 +1,14 @@
|
|||
#purchase-courses-view
|
||||
|
||||
font-size: 18px
|
||||
|
||||
.enrollments-info
|
||||
width: 300px
|
||||
|
||||
#students-input
|
||||
width: 100px
|
||||
font-size: 30px
|
||||
text-align: center
|
||||
|
||||
.uppercase
|
||||
text-transform: uppercase
|
3
app/styles/courses/remove-student-modal.sass
Normal file
|
@ -0,0 +1,3 @@
|
|||
#remove-student-modal
|
||||
.glyphicon-warning-sign
|
||||
font-size: 40px
|
7
app/styles/courses/student-log-in-modal.sass
Normal file
|
@ -0,0 +1,7 @@
|
|||
#student-log-in-modal
|
||||
#log-in-btn
|
||||
min-width: 30%
|
||||
margin-bottom: 10px
|
||||
|
||||
.form
|
||||
margin: 0 25%
|
10
app/styles/courses/student-sign-up-modal.sass
Normal file
|
@ -0,0 +1,10 @@
|
|||
#student-sign-up-modal
|
||||
#sign-up-btn
|
||||
min-width: 30%
|
||||
margin-bottom: 10px
|
||||
|
||||
.form
|
||||
margin: 0 25%
|
||||
|
||||
.modal-dialog
|
||||
margin-top: 0
|
|
@ -1,6 +1,41 @@
|
|||
#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
|
||||
|
||||
|
@ -9,18 +44,27 @@
|
|||
&:hover
|
||||
color: grey
|
||||
|
||||
#fixed-area
|
||||
position: fixed
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
.no-students
|
||||
font-size: 22px
|
||||
font-style: italic
|
||||
margin: 10px
|
||||
text-align: center
|
||||
|
||||
.well
|
||||
margin-bottom: 0
|
||||
padding: 5px
|
||||
.section-header
|
||||
border-bottom: 1px solid black
|
||||
font-size: 20px
|
||||
font-weight: bold
|
||||
margin-bottom: 20px
|
||||
text-transform: uppercase
|
||||
|
||||
.col-sm-5
|
||||
padding-top: 8px
|
||||
|
||||
.progress
|
||||
margin-bottom: 0
|
||||
.text-center
|
||||
text-align: center
|
||||
|
||||
.uppercase
|
||||
text-transform: uppercase
|
||||
|
||||
.welcome
|
||||
font-size: 24px
|
||||
font-weight: bold
|
||||
margin-bottom: 20px
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
#teachers-view
|
||||
|
||||
.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
|
||||
|
|
69
app/templates/courses/activate-licenses-modal.jade
Normal file
|
@ -0,0 +1,69 @@
|
|||
extends /templates/core/modal-base
|
||||
|
||||
block modal-header-content
|
||||
.clearfix
|
||||
.text-center
|
||||
h2 Activate Licenses
|
||||
p= view.classroom.get('name')
|
||||
|
||||
block modal-body-content
|
||||
- var usedLic = view.prepaids.totalRedeemers();
|
||||
- var totalLic = view.prepaids.totalMaxRedeemers();
|
||||
- var remainingLic = totalLic - usedLic;
|
||||
.text-center
|
||||
p
|
||||
strong.spr Licenses remaining:
|
||||
strong= remainingLic
|
||||
|
||||
.row
|
||||
.col-sm-10.col-sm-offset-1
|
||||
form.form
|
||||
if view.user
|
||||
.form-group
|
||||
.radio
|
||||
label
|
||||
input(type="radio", name="targets" value="given" checked=true)
|
||||
span.spr Activate license for student:
|
||||
span= view.user.get('name')
|
||||
.radio
|
||||
label
|
||||
input(type="radio", name="targets" value="selection")
|
||||
span.spr Activate licenses for the following students:
|
||||
else
|
||||
p Activate licenses for the following students:
|
||||
|
||||
.well.form-group
|
||||
for user in view.users.models
|
||||
.checkbox
|
||||
label
|
||||
- var paid = user.get('coursePrepaidID')
|
||||
input(type="checkbox", disabled=paid, checked=true, data-user-id=user.id, name='user')
|
||||
span.spr= user.broadName()
|
||||
|
||||
#error-alert.alert.alert-danger.hide
|
||||
|
||||
#progress-area.hide
|
||||
.progress
|
||||
.progress-bar
|
||||
|
||||
#submit-form-area.text-center
|
||||
p
|
||||
span.spr Total students:
|
||||
span#total-selected-span.spr
|
||||
span#not-depleted-span
|
||||
| (
|
||||
span.spr licenses remaining:
|
||||
span#licenses-remaining-span
|
||||
| )
|
||||
span#depleted-span
|
||||
| (
|
||||
span insufficient licenses
|
||||
| )
|
||||
|
||||
p
|
||||
button#activate-licenses-btn.btn.btn-success.text-uppercase(type="submit") Activate Licenses
|
||||
|
||||
p
|
||||
a#get-more-licenses-btn.btn.btn-info.text-uppercase(href="/courses/purchase") Get More Licenses
|
||||
|
||||
block modal-footer-content
|
28
app/templates/courses/change-course-language-modal.jade
Normal file
|
@ -0,0 +1,28 @@
|
|||
extends /templates/core/modal-base
|
||||
|
||||
block modal-header-content
|
||||
h3 Change Course Language
|
||||
.clearfix
|
||||
|
||||
block modal-body-content
|
||||
#choice-area.text-center
|
||||
- var currentLanguage = (me.get('aceConfig') || {}).language || 'python';
|
||||
button.lang-choice-btn.btn.btn-success.btn-lg(data-language='python')
|
||||
if currentLanguage === 'python'
|
||||
| Keep Using Python
|
||||
else
|
||||
| Switch To Python
|
||||
|
||||
p - OR -
|
||||
|
||||
button.lang-choice-btn.btn.btn-default(data-language='javascript')
|
||||
if currentLanguage === 'javascript'
|
||||
| Keep Using JavaScript
|
||||
else
|
||||
| Switch to JavaScript
|
||||
|
||||
#saving-progress.progress.progress-striped.active.hide
|
||||
.progress-bar(style="width: 100%")
|
||||
|
||||
block modal-footer-content
|
||||
|
21
app/templates/courses/choose-language-modal.jade
Normal file
|
@ -0,0 +1,21 @@
|
|||
extends /templates/core/modal-base
|
||||
|
||||
block modal-header-content
|
||||
.clearfix
|
||||
.text-center
|
||||
h2.modal-title Greetings!
|
||||
h3(data-i18n="choose_hero.programming_language_description")
|
||||
|
||||
block modal-body-content
|
||||
#choice-area.text-center
|
||||
button.lang-choice-btn.btn.btn-success.btn-lg(data-language='python') Python
|
||||
p(data-i18n="choose_hero.python_blurb")
|
||||
|
||||
button.lang-choice-btn.btn.btn-default(data-language='javascript') JavaScript
|
||||
p(data-i18n="choose_hero.javascript_blurb")
|
||||
|
||||
#saving-progress.progress.progress-striped.active.hide
|
||||
.progress-bar(style="width: 100%")
|
||||
|
||||
block modal-footer-content
|
||||
|
|
@ -1,24 +1,30 @@
|
|||
extends /templates/core/modal-base
|
||||
|
||||
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")
|
||||
|
|
65
app/templates/courses/classroom-view.jade
Normal file
|
@ -0,0 +1,65 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
|
||||
- var isOwner = view.classroom.get('ownerID') === me.id;
|
||||
if isOwner
|
||||
a(href="/courses/teachers") Back to my classrooms
|
||||
else
|
||||
a(href="/courses") Back to my courses
|
||||
|
||||
h1
|
||||
span.spr= view.classroom.get('name')
|
||||
if isOwner
|
||||
a#edit-class-details-link
|
||||
small Edit class details
|
||||
|
||||
if view.classroom.get('description')
|
||||
p= view.classroom.get('description')
|
||||
|
||||
// TODO: Add classroom statistics (grab from CourseDetailsView)
|
||||
|
||||
h1
|
||||
| Students
|
||||
.pull-right#main-button-area
|
||||
button#add-students-btn.btn.btn-success Add Students
|
||||
button#activate-licenses-btn.btn.btn-warning Activate Licenses
|
||||
a.btn.btn-warning(href="/courses/purchase?from-classroom="+view.classroom.id) Purchase Licenses
|
||||
|
||||
hr
|
||||
|
||||
for user in view.users.models
|
||||
a.remove-student-link.pull-right.text-uppercase(data-user-id=user.id)
|
||||
span.glyphicon.glyphicon-remove
|
||||
span.spl remove student
|
||||
|
||||
h2= user.broadName()
|
||||
- var lastPlayedString = view.makeLastPlayedString(user);
|
||||
if lastPlayedString
|
||||
p Last Played: #{lastPlayedString}
|
||||
|
||||
- var paidFor = user.get('coursePrepaidID');
|
||||
for courseInstance in view.courseInstances.models
|
||||
- var inCourse = _.contains(courseInstance.get('members'), user.id);
|
||||
- var course = view.courses.get(courseInstance.get('courseID'));
|
||||
- var campaign = view.campaigns.get(course.get('campaignID'));
|
||||
- var stats = campaign.statsForSessions(courseInstance.sessionsByUser[user.id] || []);
|
||||
if !(course.get('free') || paidFor)
|
||||
- continue;
|
||||
.row
|
||||
.col-sm-3.col-sm-offset-1= campaign.get('fullName')
|
||||
.col-sm-7
|
||||
if inCourse
|
||||
.progress
|
||||
.progress-bar(style='width:'+stats.levels.pctDone)
|
||||
else if paidFor
|
||||
button.enable-btn.btn.btn-info.btn-sm(data-user-id=user.id, data-course-instance-id=courseInstance.id) Enable
|
||||
|
||||
if !paidFor
|
||||
.text-center
|
||||
p
|
||||
em Activate a license to enable more courses for this student.
|
||||
p
|
||||
button.activate-single-license-btn.btn.btn-info.btn-sm(data-user-id=user.id) Activate
|
||||
|
||||
hr
|
|
@ -2,11 +2,6 @@ extends /templates/base
|
|||
|
||||
block content
|
||||
|
||||
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
|
||||
p
|
||||
// TODO: format this text all good and stuff
|
||||
strong
|
||||
if courseInstance.get('name')
|
||||
span= courseInstance.get('name')
|
||||
else if view.classroom.get('name')
|
||||
span= view.classroom.get('name')
|
||||
else
|
||||
span(data-i18n='courses.unnamed_class')
|
||||
|
||||
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')}
|
||||
small.spl
|
||||
if courseInstance.get('name')
|
||||
| (#{courseInstance.get('name')})
|
||||
else if view.classroom.get('name')
|
||||
| (#{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}")
|
||||
strong= view.getOwnerName()
|
||||
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 You’re ready to take the next step! Explore hundreds of challenging levels, learn advanced programming skills, and compete in multiplayer arenas!
|
||||
else if view.singlePlayerMode && me.isAnonymous()
|
||||
.col-md-6
|
||||
a.btn.btn-lg.btn-success.signup-button
|
||||
h1 Create an Account
|
||||
p Sign up for a FREE CodeCombat account and gain access to more levels, more programming skills, and more fun!
|
||||
.col-md-6
|
||||
a.btn.btn-lg.btn-success(href="/play")
|
||||
h1 Preview Campaign
|
||||
p Take a sneak peek at all that CodeCombat has to offer before signing up for your FREE account.
|
||||
else if !view.singlePlayerMode
|
||||
.col-md-6
|
||||
if view.arenaLevel
|
||||
a.btn.btn-lg.btn-success.btn-play-level(data-level-slug=view.arenaLevel.slug, data-level-id=view.arenaLevel.original)
|
||||
h1
|
||||
span Arena
|
||||
| :
|
||||
span.spl= view.arenaLevel.name
|
||||
p= view.arenaLevel.description.replace(/!\[.*?\)/, '')
|
||||
else
|
||||
a.btn.btn-lg.btn-success.disabled
|
||||
h1 Arena Coming Soon
|
||||
p We are working on a multiplayer arena for classrooms at the end of #{course.get('name')}.
|
||||
.col-md-6
|
||||
if view.nextCourseInstance
|
||||
a.btn.btn-lg.btn-success(href="/courses/#{view.nextCourse.id}/#{view.nextCourseInstance.id}")
|
||||
h1= view.nextCourse.get('name')
|
||||
p= view.nextCourse.get('description')
|
||||
else if view.nextCourse
|
||||
a.btn.btn-lg.btn-success.disabled
|
||||
h1= view.nextCourse.get('name')
|
||||
p
|
||||
em NOT ENROLLED
|
||||
p Ask your teacher to enroll you in the next course.
|
||||
else
|
||||
a.btn.btn-lg.btn-success(disabled=!view.nextCourse ? "disabled" : "")
|
||||
h1 Next Course
|
||||
p
|
||||
em COMING SOON
|
||||
p We are hard at work making more courses for you!
|
||||
|
||||
if !me.isAnonymous()
|
||||
div.well.well-sm(role='tabpanel')
|
||||
ul.nav.nav-pills(role='tablist')
|
||||
if view.teacherMode
|
||||
li.active(role='presentation')
|
||||
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
|
||||
li(role='presentation')
|
||||
a(href='#levels', aria-controls='levels', role='tab', data-toggle='tab', data-i18n="nav.play")
|
||||
else
|
||||
li.active(role='presentation')
|
||||
a(href='#levels', aria-controls='levels', role='tab', data-toggle='tab', data-i18n="nav.play")
|
||||
li(role='presentation')
|
||||
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
|
||||
.tab-content
|
||||
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)
|
||||
|
||||
|
|
|
@ -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
|
||||
#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!)
|
||||
|
||||
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
|
||||
|
||||
.row
|
||||
.col-sm-6.text-center
|
||||
a(href="/courses/students").btn.btn-default Students Click Here
|
||||
|
||||
.col-sm-6.text-center
|
||||
a(href="/courses/teachers").btn.btn-default Teachers Click Here
|
|
@ -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!)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,34 +8,41 @@ block content
|
|||
.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.
|
||||
p.text-center
|
||||
strong How many enrollments do you need?
|
||||
br
|
||||
|
||||
#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)
|
||||
p.text-center
|
||||
input#students-input(
|
||||
placeholder='<number of enrollments>'
|
||||
value=view.numberOfStudents
|
||||
type='number'
|
||||
)
|
||||
br
|
||||
|
||||
.form-group
|
||||
.col-sm-offset-3.col-sm-10
|
||||
button#purchase-btn.btn.btn-primary Purchase
|
||||
.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
|
||||
|
|
27
app/templates/courses/remove-student-modal.jade
Normal file
|
@ -0,0 +1,27 @@
|
|||
extends /templates/core/modal-base
|
||||
|
||||
block modal-header-content
|
||||
.text-center
|
||||
h3.modal-title Remove Student
|
||||
span.glyphicon.glyphicon-warning-sign.text-danger
|
||||
h3 Are you sure you want to remove this student from this class?
|
||||
|
||||
block modal-body-content
|
||||
p.text-center
|
||||
| Student will lose access to this classroom and assigned classes.
|
||||
| Progress and gameplay is NOT lost, and the student can be added back to the classroom at any time.
|
||||
if view.user.get('coursePrepaidID')
|
||||
| The activated paid license will not be returned.
|
||||
|
||||
block modal-footer-content
|
||||
#remove-student-buttons.text-center
|
||||
p
|
||||
button.btn.btn-lg.btn-success.text-uppercase(data-dismiss="modal") Keep Student
|
||||
p - OR -
|
||||
p
|
||||
button#remove-student-btn.btn.btn-lg.btn-default.text-uppercase Remove Student
|
||||
|
||||
#remove-student-progress.text-center.hide
|
||||
.progress
|
||||
.progress-bar
|
||||
p.text-info Removing user
|
28
app/templates/courses/student-log-in-modal.jade
Normal file
|
@ -0,0 +1,28 @@
|
|||
extends /templates/core/modal-base
|
||||
|
||||
block modal-header-content
|
||||
.clearfix
|
||||
|
||||
|
||||
block modal-body-content
|
||||
.text-center
|
||||
h2.modal-title(data-i18n="login.log_in")
|
||||
|
||||
form.form
|
||||
.form-group
|
||||
label.control-label(for="email")
|
||||
span(data-i18n="general.email")
|
||||
input#email.input-large.form-control(name="email", type="email")
|
||||
.form-group
|
||||
label.control-label(for="password")
|
||||
span(data-i18n="general.password") Password
|
||||
input#password.input-large.form-control(name="password", type="password")
|
||||
|
||||
#errors-alert.alert.alert-danger.hide
|
||||
|
||||
.text-center
|
||||
input#log-in-btn.btn.btn-default(data-i18n="[value]login.log_in", type="submit")
|
||||
p
|
||||
a#create-new-account-link(data-i18n="login.signup_switch")
|
||||
|
||||
block modal-footer-content
|
51
app/templates/courses/student-sign-up-modal.jade
Normal file
|
@ -0,0 +1,51 @@
|
|||
extends /templates/core/modal-base
|
||||
|
||||
block modal-header-content
|
||||
.clearfix
|
||||
|
||||
|
||||
block modal-body-content
|
||||
.text-center
|
||||
h2.modal-title(data-i18n="signup.sign_up")
|
||||
|
||||
form.form
|
||||
.form-group
|
||||
label.control-label(for="email")
|
||||
span(data-i18n="general.email")
|
||||
input#email.input-large.form-control(name="email", type="email")
|
||||
.help-block use your school email if you have one
|
||||
.form-group
|
||||
label.control-label(for="name")
|
||||
span(data-i18n="general.name") Name
|
||||
if me.get('name')
|
||||
input#name.input-large.form-control(name="name", type="text", value="#{me.get('name')}")
|
||||
else
|
||||
input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater")
|
||||
.help-block a unique name no one has chosen
|
||||
.form-group
|
||||
label.control-label(for="password")
|
||||
span(data-i18n="general.password") Password
|
||||
input#password.input-large.form-control(name="password", type="password")
|
||||
.help-block pick something you can remember
|
||||
.form-group
|
||||
label.control-label(for="class-code-input")
|
||||
span Class Code
|
||||
input#class-code-input.input-large.form-control(name="classCode", value=view.classCode)
|
||||
.help-block optional - ask your teacher to give you one!
|
||||
.form-group
|
||||
label.control-label(for="school-input")
|
||||
span(data-i18n="signup.school_name") School Name and City
|
||||
input#school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder")
|
||||
.help-block optional - what school do you go to?
|
||||
|
||||
#errors-alert.alert.alert-danger.hide
|
||||
|
||||
.text-center
|
||||
if view.willPlay
|
||||
input#sign-up-btn.btn.btn-default(type="submit", value="Start Playing")
|
||||
p
|
||||
a#skip-link Skip this, I'll create an account later!
|
||||
else
|
||||
input#sign-up-btn.btn.btn-default(data-i18n="[value]signup.sign_up", type="submit")
|
||||
|
||||
block modal-footer-content
|
|
@ -2,188 +2,104 @@ extends /templates/base
|
|||
|
||||
block content
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
.text-center
|
||||
if me.isAnonymous() || !me.get('name')
|
||||
.welcome Welcome!
|
||||
else
|
||||
.welcome Welcome, #{me.get('name')}!
|
||||
|
||||
|
||||
.section-header Your Classes
|
||||
|
||||
#manage-tab-pane.tab-pane.well
|
||||
if view.classrooms.models.length > 0
|
||||
.container-fluid
|
||||
each classroom in view.classrooms.models
|
||||
+classroom(classroom)
|
||||
else
|
||||
.no-students No classes yet!
|
||||
|
||||
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
|
||||
.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)
|
||||
.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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,34 +2,42 @@ 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
|
||||
p.bigger-text
|
||||
strong Teachers
|
||||
span - share CodeCombat's Hour of Code with your class!
|
||||
|
||||
h2(data-i18n="teachers.more_info")
|
||||
p(data-i18n="teachers.intro_1")
|
||||
p(data-i18n="teachers.intro_2")
|
||||
.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.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
h3.teachers-title(data-i18n="teachers.teacher_subs_title")
|
||||
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
|
||||
| .
|
||||
|
|
|
@ -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')
|
||||
|
|
97
app/views/courses/ActivateLicensesModal.coffee
Normal file
|
@ -0,0 +1,97 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/courses/activate-licenses-modal'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Prepaid = require 'models/Prepaid'
|
||||
User = require 'models/User'
|
||||
|
||||
module.exports = class ActivateLicensesModal extends ModalView
|
||||
id: 'activate-licenses-modal'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'change input': 'updateSelectionSpans'
|
||||
'submit form': 'onSubmitForm'
|
||||
|
||||
initialize: (options) ->
|
||||
@classroom = options.classroom
|
||||
@users = options.users
|
||||
@user = options.user
|
||||
@prepaids = new CocoCollection([], { url: "/db/prepaid", model: Prepaid })
|
||||
sum = (numbers) -> _.reduce(numbers, (a, b) -> a + b)
|
||||
@prepaids.totalMaxRedeemers = -> sum((prepaid.get('maxRedeemers') for prepaid in @models)) or 0
|
||||
@prepaids.totalRedeemers = -> sum((_.size(prepaid.get('redeemers')) for prepaid in @models)) or 0
|
||||
@prepaids.comparator = '_id'
|
||||
@supermodel.loadCollection(@prepaids, 'prepaids', {data: {creator: me.id}})
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@updateSelectionSpans()
|
||||
|
||||
updateSelectionSpans: ->
|
||||
targets = @$('input[name="targets"]:checked').val()
|
||||
if targets is 'given'
|
||||
numToActivate = 1
|
||||
else
|
||||
numToActivate = @$('input[name="user"]:checked:not(:disabled)').length
|
||||
@$('#total-selected-span').text(numToActivate)
|
||||
remaining = @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() - numToActivate
|
||||
@$('#licenses-remaining-span').text(remaining)
|
||||
depleted = remaining < 0
|
||||
@$('#not-depleted-span').toggleClass('hide', depleted)
|
||||
@$('#depleted-span').toggleClass('hide', !depleted)
|
||||
@$('#activate-licenses-btn').toggleClass('disabled', depleted)
|
||||
|
||||
showProgress: ->
|
||||
@$('#submit-form-area').addClass('hide')
|
||||
@$('#progress-area').removeClass('hide')
|
||||
|
||||
hideProgress: ->
|
||||
@$('#submit-form-area').removeClass('hide')
|
||||
@$('#progress-area').addClass('hide')
|
||||
|
||||
onSubmitForm: (e) ->
|
||||
e.preventDefault()
|
||||
@$('#error-alert').addClass('hide')
|
||||
@usersToRedeem = new CocoCollection([], {model: User})
|
||||
targets = @$('input[name="targets"]:checked').val()
|
||||
if targets is 'given'
|
||||
@usersToRedeem.add(@user)
|
||||
else
|
||||
checkedBoxes = @$('input[name="user"]:checked:not(:disabled)')
|
||||
_.each checkedBoxes, (el) =>
|
||||
$el = $(el)
|
||||
userID = $el.data('user-id')
|
||||
@usersToRedeem.add @users.get(userID)
|
||||
return unless @usersToRedeem.size()
|
||||
@usersToRedeem.originalSize = @usersToRedeem.size()
|
||||
@showProgress()
|
||||
@redeemUsers()
|
||||
|
||||
redeemUsers: ->
|
||||
if not @usersToRedeem.size()
|
||||
@finishRedeemUsers()
|
||||
return
|
||||
|
||||
user = @usersToRedeem.first()
|
||||
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots())
|
||||
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots()) unless prepaid
|
||||
$.ajax({
|
||||
method: 'POST'
|
||||
url: _.result(prepaid, 'url') + '/redeemers'
|
||||
data: { userID: user.id }
|
||||
context: @
|
||||
success: ->
|
||||
@usersToRedeem.remove(user)
|
||||
pct = 100 * (@usersToRedeem.originalSize - @usersToRedeem.size() / @usersToRedeem.originalSize)
|
||||
@$('#progress-area .progress-bar').css('width', "#{pct.toFixed(1)}%")
|
||||
@redeemUsers()
|
||||
error: (jqxhr, textStatus, errorThrown) ->
|
||||
if jqxhr.status is 402
|
||||
message = arguments[2]
|
||||
else
|
||||
message = "#{jqxhr.status}: #{jqxhr.responseText}"
|
||||
@$('#error-alert').text(message).removeClass('hide')
|
||||
})
|
||||
|
||||
finishRedeemUsers: ->
|
||||
@trigger 'redeem-users'
|
26
app/views/courses/ChangeCourseLanguageModal.coffee
Normal file
|
@ -0,0 +1,26 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/courses/change-course-language-modal'
|
||||
|
||||
module.exports = class ChangeCourseLanguageModal extends ModalView
|
||||
id: 'change-course-language-modal'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click .lang-choice-btn': 'onClickLanguageChoiceButton'
|
||||
|
||||
onClickLanguageChoiceButton: (e) ->
|
||||
@chosenLanguage = $(e.target).data('language')
|
||||
aceConfig = _.clone(me.get('aceConfig') or {})
|
||||
aceConfig.language = @chosenLanguage
|
||||
me.set('aceConfig', aceConfig)
|
||||
res = me.patch()
|
||||
if res
|
||||
@$('#choice-area').hide()
|
||||
@$('#saving-progress').removeClass('hide')
|
||||
@listenToOnce me, 'sync', @onLanguageSettingSaved
|
||||
else
|
||||
@onLanguageSettingSaved()
|
||||
|
||||
onLanguageSettingSaved: ->
|
||||
@trigger('set-language')
|
||||
@hide()
|
51
app/views/courses/ChooseLanguageModal.coffee
Normal file
|
@ -0,0 +1,51 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/courses/choose-language-modal'
|
||||
|
||||
module.exports = class ChooseLanguageModal extends ModalView
|
||||
id: 'choose-language-modal'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click .lang-choice-btn': 'onClickLanguageChoiceButton'
|
||||
|
||||
initialize: (options) ->
|
||||
options ?= {}
|
||||
@logoutFirst = options.logoutFirst
|
||||
|
||||
onClickLanguageChoiceButton: (e) ->
|
||||
@chosenLanguage = $(e.target).data('language')
|
||||
if @logoutFirst
|
||||
@logoutUser()
|
||||
else
|
||||
@saveLanguageSetting()
|
||||
|
||||
logoutUser: ->
|
||||
$.ajax({
|
||||
method: 'POST'
|
||||
url: '/auth/logout'
|
||||
context: @
|
||||
success: @onUserLoggedOut
|
||||
})
|
||||
|
||||
onUserLoggedOut: ->
|
||||
me.clear()
|
||||
me.fetch({
|
||||
url: '/auth/whoami'
|
||||
})
|
||||
@listenToOnce me, 'sync', @saveLanguageSetting
|
||||
|
||||
saveLanguageSetting: ->
|
||||
aceConfig = _.clone(me.get('aceConfig') or {})
|
||||
aceConfig.language = @chosenLanguage
|
||||
me.set('aceConfig', aceConfig)
|
||||
res = me.patch()
|
||||
if res
|
||||
@$('#choice-area').hide()
|
||||
@$('#saving-progress').removeClass('hide')
|
||||
@listenToOnce me, 'sync', @onLanguageSettingSaved
|
||||
else
|
||||
@onLanguageSettingSaved()
|
||||
|
||||
onLanguageSettingSaved: ->
|
||||
@trigger('set-language')
|
||||
@hide()
|
|
@ -1,3 +1,4 @@
|
|||
Classroom = require 'models/Classroom'
|
||||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/courses/classroom-settings-modal'
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
126
app/views/courses/ClassroomView.coffee
Normal file
|
@ -0,0 +1,126 @@
|
|||
Campaign = require 'models/Campaign'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Course = require 'models/Course'
|
||||
CourseInstance = require 'models/CourseInstance'
|
||||
Classroom = require 'models/Classroom'
|
||||
LevelSession = require 'models/LevelSession'
|
||||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/courses/classroom-view'
|
||||
User = require 'models/User'
|
||||
utils = require 'core/utils'
|
||||
Prepaid = require 'models/Prepaid'
|
||||
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
||||
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
||||
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
||||
RemoveStudentModal = require 'views/courses/RemoveStudentModal'
|
||||
|
||||
module.exports = class ClassroomView extends RootView
|
||||
id: 'classroom-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click #edit-class-details-link': 'onClickEditClassDetailsLink'
|
||||
'click #activate-licenses-btn': 'onClickActivateLicensesButton'
|
||||
'click .activate-single-license-btn': 'onClickActivateSingleLicenseButton'
|
||||
'click #add-students-btn': 'onClickAddStudentsButton'
|
||||
'click .enable-btn': 'onClickEnableButton'
|
||||
'click .remove-student-link': 'onClickRemoveStudentLink'
|
||||
|
||||
initialize: (options, classroomID) ->
|
||||
@classroom = new Classroom({_id: classroomID})
|
||||
@supermodel.loadModel @classroom, 'classroom'
|
||||
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
||||
@courses.comparator = '_id'
|
||||
@supermodel.loadCollection(@courses, 'courses')
|
||||
@campaigns = new CocoCollection([], { url: "/db/campaign", model: Campaign })
|
||||
@courses.comparator = '_id'
|
||||
@supermodel.loadCollection(@campaigns, 'campaigns', { data: { type: 'course' }})
|
||||
@courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance})
|
||||
@courseInstances.comparator = 'courseID'
|
||||
@supermodel.loadCollection(@courseInstances, 'course_instances', { data: { classroomID: classroomID } })
|
||||
@users = new CocoCollection([], { url: "/db/classroom/#{classroomID}/members", model: User })
|
||||
@users.comparator = (user) => user.broadName().toLowerCase()
|
||||
@supermodel.loadCollection(@users, 'users')
|
||||
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
|
||||
|
||||
onCourseInstancesSync: ->
|
||||
@sessions = new CocoCollection([], { model: LevelSession })
|
||||
for courseInstance in @courseInstances.models
|
||||
sessions = new CocoCollection([], { url: "/db/course_instance/#{courseInstance.id}/level_sessions", model: LevelSession })
|
||||
@supermodel.loadCollection(sessions, 'sessions')
|
||||
courseInstance.sessions = sessions
|
||||
sessions.courseInstance = courseInstance
|
||||
@listenToOnce sessions, 'sync', (sessions) ->
|
||||
@sessions.add(sessions.slice())
|
||||
sessions.courseInstance.sessionsByUser = sessions.groupBy('creator')
|
||||
|
||||
onLoaded: ->
|
||||
userSessions = @sessions.groupBy('creator')
|
||||
for user in @users.models
|
||||
user.sessions = new CocoCollection(userSessions[user.id], { model: LevelSession })
|
||||
user.sessions.comparator = 'changed'
|
||||
user.sessions.sort()
|
||||
for courseInstance in @courseInstances.models
|
||||
courseID = courseInstance.get('courseID')
|
||||
course = @courses.get(courseID)
|
||||
campaignID = course.get('campaignID')
|
||||
campaign = @campaigns.get(campaignID)
|
||||
courseInstance.sessions.campaign = campaign
|
||||
super()
|
||||
|
||||
onClickActivateLicensesButton: ->
|
||||
modal = new ActivateLicensesModal({
|
||||
classroom: @classroom
|
||||
users: @users
|
||||
})
|
||||
@openModalView(modal)
|
||||
modal.once 'redeem-users', -> document.location.reload()
|
||||
|
||||
onClickActivateSingleLicenseButton: (e) ->
|
||||
userID = $(e.target).data('user-id')
|
||||
user = @users.get(userID)
|
||||
modal = new ActivateLicensesModal({
|
||||
classroom: @classroom
|
||||
users: @users
|
||||
user: user
|
||||
})
|
||||
@openModalView(modal)
|
||||
modal.once 'redeem-users', -> document.location.reload()
|
||||
|
||||
onClickEditClassDetailsLink: ->
|
||||
modal = new ClassroomSettingsModal({classroom: @classroom})
|
||||
@openModalView(modal)
|
||||
@listenToOnce modal, 'hidden', @render
|
||||
|
||||
makeLastPlayedString: (user) ->
|
||||
session = user.sessions.last()
|
||||
return '' if not session
|
||||
campaign = session.collection.campaign
|
||||
levelOriginal = session.get('level').original
|
||||
campaignLevel = campaign.get('levels')[levelOriginal]
|
||||
return "#{campaign.get('fullName')}, #{campaignLevel.name}"
|
||||
|
||||
onClickAddStudentsButton: (e) ->
|
||||
modal = new InviteToClassroomModal({classroom: @classroom})
|
||||
@openModalView(modal)
|
||||
|
||||
onClickEnableButton: (e) ->
|
||||
courseInstance = @courseInstances.get($(e.target).data('course-instance-id'))
|
||||
userID = $(e.target).data('user-id')
|
||||
courseInstance.addMember(userID)
|
||||
$(e.target).attr('disabled', true)
|
||||
@listenToOnce courseInstance, 'sync', @render
|
||||
|
||||
onClickRemoveStudentLink: (e) ->
|
||||
user = @users.get($(e.target).closest('a').data('user-id'))
|
||||
modal = new RemoveStudentModal({
|
||||
classroom: @classroom
|
||||
user: user
|
||||
courseInstances: @courseInstances
|
||||
})
|
||||
@openModalView(modal)
|
||||
modal.once 'remove-student', @onStudentRemoved, @
|
||||
|
||||
onStudentRemoved: (e) ->
|
||||
@users.remove(e.user)
|
||||
@render()
|
|
@ -9,12 +9,16 @@ template = require 'templates/courses/course-details'
|
|||
User = require 'models/User'
|
||||
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')
|
||||
|
|
|
@ -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, @
|
|
@ -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,10 +19,39 @@ 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.
|
||||
|
@ -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()
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
37
app/views/courses/RemoveStudentModal.coffee
Normal file
|
@ -0,0 +1,37 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/courses/remove-student-modal'
|
||||
|
||||
module.exports = class RemoveStudentModal extends ModalView
|
||||
id: 'remove-student-modal'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click #remove-student-btn': 'onClickRemoveStudentButton'
|
||||
|
||||
initialize: (options) ->
|
||||
@classroom = options.classroom
|
||||
@user = options.user
|
||||
@courseInstances = options.courseInstances
|
||||
|
||||
onClickRemoveStudentButton: ->
|
||||
@$('#remove-student-buttons').addClass('hide')
|
||||
@$('#remove-student-progress').removeClass('hide')
|
||||
userID = @user.id
|
||||
@toRemove = @courseInstances.filter (courseInstance) -> _.contains(courseInstance.get('members'), userID)
|
||||
@toRemove.push @classroom
|
||||
@totalJobs = _.size(@toRemove)
|
||||
@removeStudent()
|
||||
|
||||
removeStudent: ->
|
||||
model = @toRemove.shift()
|
||||
if not model
|
||||
@trigger 'remove-student', { user: @user }
|
||||
@hide()
|
||||
return
|
||||
|
||||
model.removeMember(@user.id)
|
||||
pct = (100 * (@totalJobs - @toRemove.length) / @totalJobs).toFixed(1) + '%'
|
||||
@$('#remove-student-progress .progress-bar').css('width', pct)
|
||||
@listenToOnce model, 'sync', ->
|
||||
@removeStudent()
|
||||
|
34
app/views/courses/StudentLogInModal.coffee
Normal file
|
@ -0,0 +1,34 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/courses/student-log-in-modal'
|
||||
auth = require 'core/auth'
|
||||
forms = require 'core/forms'
|
||||
User = require 'models/User'
|
||||
|
||||
module.exports = class StudentSignInModal extends ModalView
|
||||
id: 'student-log-in-modal'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click #log-in-btn': 'onClickLogInButton'
|
||||
'submit form': 'onSubmitForm'
|
||||
'click #create-new-account-link': 'onClickCreateNewAccountLink'
|
||||
|
||||
onSubmitForm: (e) ->
|
||||
e.preventDefault()
|
||||
@login()
|
||||
|
||||
onClickLogInButton: ->
|
||||
@login()
|
||||
|
||||
login: ->
|
||||
data = forms.formToObject @$el
|
||||
@enableModalInProgress(@$el)
|
||||
auth.loginUser data, (jqxhr) =>
|
||||
error = jqxhr.responseJSON[0]
|
||||
message = _.filter([error.property, error.message]).join(' ')
|
||||
@disableModalInProgress(@$el)
|
||||
@$('#errors-alert').text(message).removeClass('hide')
|
||||
|
||||
onClickCreateNewAccountLink: ->
|
||||
@trigger 'want-to-create-account'
|
||||
@hide?()
|
96
app/views/courses/StudentSignUpModal.coffee
Normal file
|
@ -0,0 +1,96 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/courses/student-sign-up-modal'
|
||||
auth = require 'core/auth'
|
||||
forms = require 'core/forms'
|
||||
User = require 'models/User'
|
||||
Classroom = require 'models/Classroom'
|
||||
utils = require 'core/utils'
|
||||
|
||||
module.exports = class StudentSignUpModal extends ModalView
|
||||
id: 'student-sign-up-modal'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click #sign-up-btn': 'onClickSignUpButton'
|
||||
'submit form': 'onSubmitForm'
|
||||
'click #skip-link': 'onClickSkipLink'
|
||||
|
||||
initialize: (options) ->
|
||||
options ?= {}
|
||||
@willPlay = options.willPlay
|
||||
@classCode = utils.getQueryVariable('_cc') or ''
|
||||
|
||||
onClickSkipLink: ->
|
||||
@trigger 'click-skip-link' # defer to view that opened this modal
|
||||
@hide?()
|
||||
|
||||
onSubmitForm: (e) ->
|
||||
e.preventDefault()
|
||||
@signupClassroomPrecheck()
|
||||
|
||||
onClickSignUpButton: ->
|
||||
@signupClassroomPrecheck()
|
||||
|
||||
emailCheck: ->
|
||||
email = @$('#email').val()
|
||||
filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i # https://news.ycombinator.com/item?id=5763990
|
||||
unless filter.test(email)
|
||||
@$('#errors-alert').text($.i18n.t('share_progress_modal.email_invalid')).removeClass('hide')
|
||||
return false
|
||||
return true
|
||||
|
||||
signupClassroomPrecheck: ->
|
||||
if not _.all([@$('#email').val(), @$('#password').val(), @$('#name').val()])
|
||||
@$('#errors-alert').text('Enter email, username and password').removeClass('hide')
|
||||
return
|
||||
classCode = @$('#class-code-input').val()
|
||||
if not classCode
|
||||
return @signup()
|
||||
classroom = new Classroom()
|
||||
classroom.fetch({ url: '/db/classroom?code='+classCode })
|
||||
classroom.once 'sync', @signup, @
|
||||
classroom.once 'error', @onClassroomFetchError, @
|
||||
@enableModalInProgress(@$el)
|
||||
|
||||
onClassroomFetchError: ->
|
||||
@disableModalInProgress(@$el)
|
||||
@$('#errors-alert').text('Classroom code could not be found').removeClass('hide')
|
||||
|
||||
signup: ->
|
||||
return unless @emailCheck()
|
||||
# TODO: consolidate with AuthModal logic, or make user creation process less magical, more RESTful
|
||||
data = forms.formToObject @$el
|
||||
delete data.classCode
|
||||
for key, val of me.attributes when key in ['preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1', 'name', 'music', 'volume', 'emails', 'schoolName']
|
||||
data[key] ?= val
|
||||
Backbone.Mediator.publish "auth:signed-up", {}
|
||||
data.emails ?= {}
|
||||
data.emails.generalNews ?= {}
|
||||
data.emails.generalNews.enabled = false
|
||||
window.tracker?.trackEvent 'Finished Student Signup', label: 'CodeCombat'
|
||||
@enableModalInProgress(@$el)
|
||||
user = new User(data)
|
||||
user.notyErrors = false
|
||||
user.save({}, {
|
||||
validate: false # make server deal with everything
|
||||
error: @onCreateUserError
|
||||
success: @onCreateUserSuccess
|
||||
})
|
||||
|
||||
onCreateUserError: (model, jqxhr) =>
|
||||
# really need to make our server errors uniform
|
||||
if jqxhr.responseJSON
|
||||
error = jqxhr.responseJSON
|
||||
error = error[0] if _.isArray(error)
|
||||
message = _.filter([error.property, error.message]).join(' ')
|
||||
else
|
||||
message = jqxhr.responseText
|
||||
@disableModalInProgress(@$el)
|
||||
@$('#errors-alert').text(message).removeClass('hide')
|
||||
|
||||
onCreateUserSuccess: =>
|
||||
classCode = @$('#class-code-input').val()
|
||||
if classCode
|
||||
url = "/courses?_cc="+classCode
|
||||
application.router.navigate(url)
|
||||
window.location.reload()
|
|
@ -4,13 +4,11 @@ CocoCollection = require 'collections/CocoCollection'
|
|||
CocoModel = require 'models/CocoModel'
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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...)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -86,6 +86,17 @@ describe 'POST /db/course_instance/:id/members', ->
|
|||
|
||||
], 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)
|
||||
|
|
@ -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
|
||||
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()
|
||||
|
||||
done()
|
||||
setTimeout f, 100
|
||||
|
||||
it 'recalculates games completed', (done) ->
|
||||
unittest.getNormalJoe (joe) ->
|
||||
|
|