mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 08:50:58 -05:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
27f270856a
39 changed files with 289 additions and 60 deletions
9
Vagrantfile
vendored
9
Vagrantfile
vendored
|
@ -17,7 +17,14 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||||
config.vm.network "forwarded_port", guest: 3000, host: 13000
|
config.vm.network "forwarded_port", guest: 3000, host: 13000
|
||||||
config.vm.network "forwarded_port", guest: 9485, host: 19485
|
config.vm.network "forwarded_port", guest: 9485, host: 19485
|
||||||
|
|
||||||
config.vm.provision "shell", path: "scripts/vagrant/core/provision.sh", privileged: false
|
config.vm.define "default" do |default|
|
||||||
|
default.vm.provision "shell", path: "scripts/vagrant/core/provision.sh", privileged: false
|
||||||
|
end
|
||||||
|
|
||||||
|
config.vm.define "brunchv2", autostart: false do |brunchv2|
|
||||||
|
brunchv2.vm.provision "shell", path: "scripts/vagrant/core/provision.sh", privileged: false
|
||||||
|
brunchv2.vm.provision "shell", path: "scripts/vagrant/core/update-brunchv2.sh", privileged: false
|
||||||
|
end
|
||||||
|
|
||||||
config.vm.provider "virtualbox" do |v|
|
config.vm.provider "virtualbox" do |v|
|
||||||
v.memory = 2048
|
v.memory = 2048
|
||||||
|
|
|
@ -69,15 +69,15 @@ module.exports = class CocoRouter extends Backbone.Router
|
||||||
'contribute/diplomat': go('contribute/DiplomatView')
|
'contribute/diplomat': go('contribute/DiplomatView')
|
||||||
'contribute/scribe': go('contribute/ScribeView')
|
'contribute/scribe': go('contribute/ScribeView')
|
||||||
|
|
||||||
'courses': go('courses/CoursesView') # , { studentsOnly: true }) # TODO: Enforce after session-less play for teachers
|
'courses': go('courses/CoursesView', { studentsOnly: true })
|
||||||
'Courses': go('courses/CoursesView') # , { studentsOnly: true })
|
'Courses': go('courses/CoursesView', { studentsOnly: true })
|
||||||
'courses/students': redirect('/courses')
|
'courses/students': redirect('/courses')
|
||||||
'courses/teachers': redirect('/teachers/classes')
|
'courses/teachers': redirect('/teachers/classes')
|
||||||
'courses/purchase': redirect('/teachers/licenses')
|
'courses/purchase': redirect('/teachers/licenses')
|
||||||
'courses/enroll(/:courseID)': redirect('/teachers/licenses')
|
'courses/enroll(/:courseID)': redirect('/teachers/licenses')
|
||||||
'courses/update-account': go('courses/CoursesUpdateAccountView')
|
'courses/update-account': go('courses/CoursesUpdateAccountView')
|
||||||
'courses/:classroomID': go('courses/ClassroomView') #, { studentsOnly: true })
|
'courses/:classroomID': go('courses/ClassroomView', { studentsOnly: true })
|
||||||
'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView')
|
'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView', { studentsOnly: true })
|
||||||
|
|
||||||
'db/*path': 'routeToServer'
|
'db/*path': 'routeToServer'
|
||||||
'demo(/*subpath)': go('DemoView')
|
'demo(/*subpath)': go('DemoView')
|
||||||
|
@ -142,14 +142,14 @@ module.exports = class CocoRouter extends Backbone.Router
|
||||||
'SEEN': go('NewHomeView')
|
'SEEN': go('NewHomeView')
|
||||||
|
|
||||||
'teachers': redirect('/teachers/classes')
|
'teachers': redirect('/teachers/classes')
|
||||||
'teachers/classes': go('courses/TeacherClassesView') #, { teachersOnly: true })
|
'teachers/classes': go('courses/TeacherClassesView', { teachersOnly: true })
|
||||||
'teachers/classes/:classroomID': go('courses/TeacherClassView') #, { teachersOnly: true })
|
'teachers/classes/:classroomID': go('courses/TeacherClassView', { teachersOnly: true })
|
||||||
'teachers/courses': go('courses/TeacherCoursesView')
|
'teachers/courses': go('courses/TeacherCoursesView')
|
||||||
'teachers/demo': go('teachers/RequestQuoteView')
|
'teachers/demo': go('teachers/RequestQuoteView')
|
||||||
'teachers/enrollments': redirect('/teachers/licenses')
|
'teachers/enrollments': redirect('/teachers/licenses')
|
||||||
'teachers/licenses': go('courses/EnrollmentsView') #, { teachersOnly: true })
|
'teachers/licenses': go('courses/EnrollmentsView', { teachersOnly: true })
|
||||||
'teachers/freetrial': go('teachers/RequestQuoteView')
|
'teachers/freetrial': go('teachers/RequestQuoteView')
|
||||||
'teachers/quote': go('teachers/RequestQuoteView')
|
'teachers/quote': redirect('/teachers/demo')
|
||||||
'teachers/signup': ->
|
'teachers/signup': ->
|
||||||
return @routeDirectly('teachers/CreateTeacherAccountView', []) if me.isAnonymous()
|
return @routeDirectly('teachers/CreateTeacherAccountView', []) if me.isAnonymous()
|
||||||
@navigate('/teachers/update-account', {trigger: true, replace: true})
|
@navigate('/teachers/update-account', {trigger: true, replace: true})
|
||||||
|
@ -174,7 +174,7 @@ module.exports = class CocoRouter extends Backbone.Router
|
||||||
routeDirectly: (path, args=[], options={}) ->
|
routeDirectly: (path, args=[], options={}) ->
|
||||||
if options.teachersOnly and not me.isTeacher()
|
if options.teachersOnly and not me.isTeacher()
|
||||||
return @routeDirectly('teachers/RestrictedToTeachersView')
|
return @routeDirectly('teachers/RestrictedToTeachersView')
|
||||||
if options.studentsOnly and me.isTeacher()
|
if options.studentsOnly and not me.isStudent()
|
||||||
return @routeDirectly('courses/RestrictedToStudentsView')
|
return @routeDirectly('courses/RestrictedToStudentsView')
|
||||||
leavingMessage = _.result(window.currentView, 'onLeaveMessage')
|
leavingMessage = _.result(window.currentView, 'onLeaveMessage')
|
||||||
if leavingMessage
|
if leavingMessage
|
||||||
|
|
|
@ -49,5 +49,8 @@ class Rand
|
||||||
arr[i] = t
|
arr[i] = t
|
||||||
arr
|
arr
|
||||||
|
|
||||||
|
choice: (arr) =>
|
||||||
|
return arr[@rand arr.length]
|
||||||
|
|
||||||
|
|
||||||
module.exports = Rand
|
module.exports = Rand
|
||||||
|
|
|
@ -1279,7 +1279,8 @@
|
||||||
student_age_range_to: "to"
|
student_age_range_to: "to"
|
||||||
create_class: "Create Class"
|
create_class: "Create Class"
|
||||||
class_name: "Class Name"
|
class_name: "Class Name"
|
||||||
teacher_account_restricted: "Your account is a teacher account, and so cannot access student content."
|
teacher_account_restricted: "Your account is a teacher account and cannot access student content." # {change}
|
||||||
|
account_restricted: "A student account is required to access this page."
|
||||||
update_account_login_title: "Log in to update your account"
|
update_account_login_title: "Log in to update your account"
|
||||||
update_account_title: "Your account needs attention!"
|
update_account_title: "Your account needs attention!"
|
||||||
update_account_blurb: "Before you can access your classes, choose how you want to use this account."
|
update_account_blurb: "Before you can access your classes, choose how you want to use this account."
|
||||||
|
@ -1767,7 +1768,7 @@
|
||||||
|
|
||||||
coppa_deny:
|
coppa_deny:
|
||||||
text1: "Can’t wait to learn programming?"
|
text1: "Can’t wait to learn programming?"
|
||||||
text2: "Ask your parents to create an account for you!"
|
text2: "Your parents will need to create an account for you to use! Email team@codecombat.com if you have any questions." # {change}
|
||||||
close: "Close Window"
|
close: "Close Window"
|
||||||
|
|
||||||
loading_error:
|
loading_error:
|
||||||
|
|
|
@ -25,7 +25,7 @@ module.exports = class User extends CocoModel
|
||||||
return name if name
|
return name if name
|
||||||
[emailName, emailDomain] = @get('email')?.split('@') or []
|
[emailName, emailDomain] = @get('email')?.split('@') or []
|
||||||
return emailName if emailName
|
return emailName if emailName
|
||||||
return 'Anoner'
|
return 'Anonymous'
|
||||||
|
|
||||||
getPhotoURL: (size=80, useJobProfilePhoto=false, useEmployerPageAvatar=false) ->
|
getPhotoURL: (size=80, useJobProfilePhoto=false, useEmployerPageAvatar=false) ->
|
||||||
photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null
|
photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null
|
||||||
|
|
|
@ -276,9 +276,16 @@ c.extendNamedProperties LevelSchema # let's have the name be the first property
|
||||||
_.extend LevelSchema.properties,
|
_.extend LevelSchema.properties,
|
||||||
description: {title: 'Description', description: 'A short explanation of what this level is about.', type: 'string', maxLength: 65536, format: 'markdown'}
|
description: {title: 'Description', description: 'A short explanation of what this level is about.', type: 'string', maxLength: 65536, format: 'markdown'}
|
||||||
loadingTip: { type: 'string', title: 'Loading Tip', description: 'What to show for this level while it\'s loading.' }
|
loadingTip: { type: 'string', title: 'Loading Tip', description: 'What to show for this level while it\'s loading.' }
|
||||||
documentation: c.object {title: 'Documentation', description: 'Documentation articles relating to this level.', required: ['specificArticles', 'generalArticles'], 'default': {specificArticles: [], generalArticles: []}},
|
documentation: c.object {title: 'Documentation', description: 'Documentation articles relating to this level.', 'default': {specificArticles: [], generalArticles: []}},
|
||||||
specificArticles: c.array {title: 'Specific Articles', description: 'Specific documentation articles that live only in this level.', uniqueItems: true }, SpecificArticleSchema
|
specificArticles: c.array {title: 'Specific Articles', description: 'Specific documentation articles that live only in this level.', uniqueItems: true }, SpecificArticleSchema
|
||||||
generalArticles: c.array {title: 'General Articles', description: 'General documentation articles that can be linked from multiple levels.', uniqueItems: true}, GeneralArticleSchema
|
generalArticles: c.array {title: 'General Articles', description: 'General documentation articles that can be linked from multiple levels.', uniqueItems: true}, GeneralArticleSchema
|
||||||
|
hints: c.array {title: 'Hints', description: 'Hints that will be gradually revealed to the player.', uniqueItems: true }, {
|
||||||
|
type: 'object'
|
||||||
|
properties: {
|
||||||
|
body: {type: 'string', title: 'Content', description: 'The body content of the article, in Markdown.', format: 'markdown'}
|
||||||
|
i18n: {type: 'object', format: 'i18n', props: ['body'], description: 'Help translate this hint'}
|
||||||
|
}
|
||||||
|
}
|
||||||
background: c.objectId({format: 'hidden'})
|
background: c.objectId({format: 'hidden'})
|
||||||
nextLevel: {
|
nextLevel: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|
|
@ -6,7 +6,7 @@ UserSchema = c.object
|
||||||
default:
|
default:
|
||||||
visa: 'Authorized to work in the US'
|
visa: 'Authorized to work in the US'
|
||||||
music: true
|
music: true
|
||||||
name: 'Anoner'
|
name: 'Anonymous'
|
||||||
autocastDelay: 5000
|
autocastDelay: 5000
|
||||||
emails: {}
|
emails: {}
|
||||||
permissions: []
|
permissions: []
|
||||||
|
|
|
@ -123,7 +123,7 @@ block content
|
||||||
if memberLanguageMap && memberLanguageMap[member.id]
|
if memberLanguageMap && memberLanguageMap[member.id]
|
||||||
span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id])
|
span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id])
|
||||||
div
|
div
|
||||||
a(href="/user/#{member.id}")= member.get('name') || 'Anoner'
|
a(href="/user/#{member.id}")= member.get('name') || 'Anonymous'
|
||||||
div Level #{member.level()}
|
div Level #{member.level()}
|
||||||
if isOwner && member.id !== clan.get('ownerID')
|
if isOwner && member.id !== clan.get('ownerID')
|
||||||
button.btn.btn-xs.btn-warning.remove-member-btn(data-id="#{member.id}", data-i18n="clans.rem_hero") Remove Hero
|
button.btn.btn-xs.btn-warning.remove-member-btn(data-id="#{member.id}", data-i18n="clans.rem_hero") Remove Hero
|
||||||
|
@ -220,7 +220,7 @@ block content
|
||||||
if memberLanguageMap && memberLanguageMap[member.id]
|
if memberLanguageMap && memberLanguageMap[member.id]
|
||||||
span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id])
|
span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id])
|
||||||
td.name-cell
|
td.name-cell
|
||||||
a(href="/user/#{member.id}")= member.get('name') || 'Anoner'
|
a(href="/user/#{member.id}")= member.get('name') || 'Anonymous'
|
||||||
td.level-cell= member.level()
|
td.level-cell= member.level()
|
||||||
td.achievements-cell
|
td.achievements-cell
|
||||||
if memberAchievementsMap && memberAchievementsMap[member.id]
|
if memberAchievementsMap && memberAchievementsMap[member.id]
|
||||||
|
|
|
@ -45,7 +45,7 @@ block content
|
||||||
if view.idNameMap && view.idNameMap[clan.get('ownerID')]
|
if view.idNameMap && view.idNameMap[clan.get('ownerID')]
|
||||||
a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')]
|
a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')]
|
||||||
else
|
else
|
||||||
a(href="/user/#{clan.get('ownerID')}") Anoner
|
a(href="/user/#{clan.get('ownerID')}") Anonymous
|
||||||
td
|
td
|
||||||
if view.myClanIDs.indexOf(clan.id) < 0
|
if view.myClanIDs.indexOf(clan.id) < 0
|
||||||
button.btn.btn-success.join-clan-btn(data-id="#{clan.id}", data-i18n="clans.join_clan") Join Clan
|
button.btn.btn-success.join-clan-btn(data-id="#{clan.id}", data-i18n="clans.join_clan") Join Clan
|
||||||
|
@ -75,7 +75,7 @@ block content
|
||||||
if view.idNameMap && view.idNameMap[clan.get('ownerID')]
|
if view.idNameMap && view.idNameMap[clan.get('ownerID')]
|
||||||
a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')]
|
a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')]
|
||||||
else
|
else
|
||||||
a(href="/user/#{clan.get('ownerID')}") Anoner
|
a(href="/user/#{clan.get('ownerID')}") Anonymous
|
||||||
td= clan.get('type')
|
td= clan.get('type')
|
||||||
td
|
td
|
||||||
if clan.get('ownerID') !== me.id
|
if clan.get('ownerID') !== me.id
|
||||||
|
|
|
@ -3,10 +3,19 @@ extends /templates/base-flat
|
||||||
block content
|
block content
|
||||||
.access-restricted.container.text-center.m-y-3
|
.access-restricted.container.text-center.m-y-3
|
||||||
h5(data-i18n='teacher.access_restricted')
|
h5(data-i18n='teacher.access_restricted')
|
||||||
|
if me.isTeacher()
|
||||||
p(data-i18n='courses.teacher_account_restricted')
|
p(data-i18n='courses.teacher_account_restricted')
|
||||||
a.btn.btn-lg.btn-primary(href="/teachers/classes" data-i18n="new_home.goto_classes")
|
a.btn.btn-lg.btn-primary(href="/teachers/classes" data-i18n="new_home.goto_classes")
|
||||||
button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out")
|
button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out")
|
||||||
|
else
|
||||||
|
p(data-i18n='courses.account_restricted')
|
||||||
|
if me.isAnonymous()
|
||||||
|
.login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in')
|
||||||
|
else
|
||||||
|
a.btn.btn-lg.btn-primary(href="/courses/update-account" data-i18n="courses.update_account_update_student")
|
||||||
|
button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out")
|
||||||
|
|
||||||
|
if me.isTeacher()
|
||||||
.teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3
|
.teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3
|
||||||
h5(data-i18n='teacher.what_is_a_teacher_account')
|
h5(data-i18n='teacher.what_is_a_teacher_account')
|
||||||
p(data-i18n='teacher.teacher_account_explanation')
|
p(data-i18n='teacher.teacher_account_explanation')
|
|
@ -11,7 +11,7 @@ ul.user-feedback-list.list-group
|
||||||
em= moment(new Date(feedback.created)).fromNow()
|
em= moment(new Date(feedback.created)).fromNow()
|
||||||
span.spl.spr -
|
span.spl.spr -
|
||||||
a(href="/user/#{feedback.creator}")
|
a(href="/user/#{feedback.creator}")
|
||||||
strong= feedback.creatorName || 'Anoner'
|
strong= feedback.creatorName || 'Anonymous'
|
||||||
if feedback.review
|
if feedback.review
|
||||||
span.spr :
|
span.spr :
|
||||||
span= feedback.review
|
span= feedback.review
|
||||||
|
|
|
@ -12,7 +12,7 @@ for player in view.players
|
||||||
.player-gold
|
.player-gold
|
||||||
.gold-icon
|
.gold-icon
|
||||||
.gold-value
|
.gold-value
|
||||||
.player-name= player.name || 'Anoner'
|
.player-name= player.name || 'Anonymous'
|
||||||
.player-health
|
.player-health
|
||||||
.health-icon
|
.health-icon
|
||||||
.health-bar-container
|
.health-bar-container
|
||||||
|
|
|
@ -81,7 +81,7 @@ block append content
|
||||||
if idNameMap && idNameMap[clan.get('ownerID')]
|
if idNameMap && idNameMap[clan.get('ownerID')]
|
||||||
a(href="/user/#{clan.get('ownerID')}")= idNameMap[clan.get('ownerID')]
|
a(href="/user/#{clan.get('ownerID')}")= idNameMap[clan.get('ownerID')]
|
||||||
else
|
else
|
||||||
a(href="/user/#{clan.get('ownerID')}") Anoner
|
a(href="/user/#{clan.get('ownerID')}") Anonymous
|
||||||
td= clan.get('members').length
|
td= clan.get('members').length
|
||||||
else
|
else
|
||||||
.panel-body
|
.panel-body
|
||||||
|
|
|
@ -23,6 +23,8 @@ module.exports = class AboutView extends RootView
|
||||||
'left': 'onLeftPressed'
|
'left': 'onLeftPressed'
|
||||||
'esc': 'onEscapePressed'
|
'esc': 'onEscapePressed'
|
||||||
|
|
||||||
|
getTitle: -> return $.i18n.t('nav.about')
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
super(arguments...)
|
super(arguments...)
|
||||||
@$('#fixed-nav').affix({
|
@$('#fixed-nav').affix({
|
||||||
|
|
|
@ -20,6 +20,8 @@ module.exports = class MainAdminView extends RootView
|
||||||
'click #create-free-sub-btn': 'onClickFreeSubLink'
|
'click #create-free-sub-btn': 'onClickFreeSubLink'
|
||||||
'click #terminal-create': 'onClickTerminalSubLink'
|
'click #terminal-create': 'onClickTerminalSubLink'
|
||||||
|
|
||||||
|
getTitle: -> return $.i18n.t('account_settings.admin')
|
||||||
|
|
||||||
initialize: ->
|
initialize: ->
|
||||||
if window.amActually
|
if window.amActually
|
||||||
@amActually = new User({_id: window.amActually})
|
@amActually = new User({_id: window.amActually})
|
||||||
|
@ -65,7 +67,7 @@ module.exports = class MainAdminView extends RootView
|
||||||
forms.enableSubmit(@$('#user-search-button'))
|
forms.enableSubmit(@$('#user-search-button'))
|
||||||
result = ''
|
result = ''
|
||||||
if users.length
|
if users.length
|
||||||
result = ("<tr data-user-id='#{user._id}'><td><code>#{user._id}</code></td><td>#{_.escape(user.name or 'Anoner')}</td><td>#{_.escape(user.email)}</td></tr>" for user in users)
|
result = ("<tr data-user-id='#{user._id}'><td><code>#{user._id}</code></td><td>#{_.escape(user.name or 'Anonymous')}</td><td>#{_.escape(user.email)}</td></tr>" for user in users)
|
||||||
result = "<table class=\"table\">#{result.join('\n')}</table>"
|
result = "<table class=\"table\">#{result.join('\n')}</table>"
|
||||||
@$el.find('#user-search-result').html(result)
|
@$el.find('#user-search-result').html(result)
|
||||||
|
|
||||||
|
|
|
@ -138,7 +138,7 @@ module.exports = class ClanDetailsView extends RootView
|
||||||
return unless @members? and @memberSort?
|
return unless @members? and @memberSort?
|
||||||
switch @memberSort
|
switch @memberSort
|
||||||
when "nameDesc"
|
when "nameDesc"
|
||||||
@members.comparator = (a, b) -> return (b.get('name') or 'Anoner').localeCompare(a.get('name') or 'Anoner')
|
@members.comparator = (a, b) -> return (b.get('name') or 'Anonymous').localeCompare(a.get('name') or 'Anonymous')
|
||||||
when "progressAsc"
|
when "progressAsc"
|
||||||
@members.comparator = (a, b) ->
|
@members.comparator = (a, b) ->
|
||||||
aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete')
|
aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete')
|
||||||
|
@ -151,7 +151,7 @@ module.exports = class ClanDetailsView extends RootView
|
||||||
else if aStarted > bStarted then return 1
|
else if aStarted > bStarted then return 1
|
||||||
if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return -1
|
if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return -1
|
||||||
else if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return 1
|
else if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return 1
|
||||||
(a.get('name') or 'Anoner').localeCompare(b.get('name') or 'Anoner')
|
(a.get('name') or 'Anonymous').localeCompare(b.get('name') or 'Anonymous')
|
||||||
when "progressDesc"
|
when "progressDesc"
|
||||||
@members.comparator = (a, b) ->
|
@members.comparator = (a, b) ->
|
||||||
aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete')
|
aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete')
|
||||||
|
@ -164,9 +164,9 @@ module.exports = class ClanDetailsView extends RootView
|
||||||
else if aStarted < bStarted then return 1
|
else if aStarted < bStarted then return 1
|
||||||
if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return -1
|
if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return -1
|
||||||
else if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return 1
|
else if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return 1
|
||||||
(b.get('name') or 'Anoner').localeCompare(a.get('name') or 'Anoner')
|
(b.get('name') or 'Anonymous').localeCompare(a.get('name') or 'Anonymous')
|
||||||
else
|
else
|
||||||
@members.comparator = (a, b) -> return (a.get('name') or 'Anoner').localeCompare(b.get('name') or 'Anoner')
|
@members.comparator = (a, b) -> return (a.get('name') or 'Anonymous').localeCompare(b.get('name') or 'Anonymous')
|
||||||
@members.sort()
|
@members.sort()
|
||||||
|
|
||||||
updateHeroIcons: ->
|
updateHeroIcons: ->
|
||||||
|
|
|
@ -110,10 +110,8 @@ module.exports = class RootView extends CocoView
|
||||||
@buildLanguages()
|
@buildLanguages()
|
||||||
$('body').removeClass('is-playing')
|
$('body').removeClass('is-playing')
|
||||||
|
|
||||||
if application.isProduction()
|
if title = @getTitle() then title += ' | CodeCombat'
|
||||||
title = 'CodeCombat - ' + (@getTitle() or 'Learn how to code by playing a game')
|
else title = 'CodeCombat - Learn how to code by playing a game'
|
||||||
else
|
|
||||||
title = @getTitle() or @constructor.name
|
|
||||||
|
|
||||||
$('title').text(title)
|
$('title').text(title)
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,8 @@ module.exports = class CoursesView extends RootView
|
||||||
'submit #join-class-form': 'onSubmitJoinClassForm'
|
'submit #join-class-form': 'onSubmitJoinClassForm'
|
||||||
'click #change-language-link': 'onClickChangeLanguageLink'
|
'click #change-language-link': 'onClickChangeLanguageLink'
|
||||||
|
|
||||||
|
getTitle: -> return $.i18n.t('teacher.students')
|
||||||
|
|
||||||
initialize: ->
|
initialize: ->
|
||||||
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
||||||
@courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID')
|
@courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID')
|
||||||
|
|
|
@ -19,6 +19,8 @@ module.exports = class EnrollmentsView extends RootView
|
||||||
'click #how-to-enroll-link': 'onClickHowToEnrollLink'
|
'click #how-to-enroll-link': 'onClickHowToEnrollLink'
|
||||||
'click #contact-us-btn': 'onClickContactUsButton'
|
'click #contact-us-btn': 'onClickContactUsButton'
|
||||||
|
|
||||||
|
getTitle: -> return $.i18n.t('teacher.enrollments')
|
||||||
|
|
||||||
initialize: ->
|
initialize: ->
|
||||||
@state = new State({
|
@state = new State({
|
||||||
totalEnrolled: 0
|
totalEnrolled: 0
|
||||||
|
|
|
@ -41,6 +41,7 @@ module.exports = class TeacherClassView extends RootView
|
||||||
'click .select-all': 'onClickSelectAll'
|
'click .select-all': 'onClickSelectAll'
|
||||||
'click .student-checkbox': 'onClickStudentCheckbox'
|
'click .student-checkbox': 'onClickStudentCheckbox'
|
||||||
'keyup #student-search': 'onKeyPressStudentSearch'
|
'keyup #student-search': 'onKeyPressStudentSearch'
|
||||||
|
'change .course-select, .bulk-course-select': 'onChangeCourseSelect'
|
||||||
|
|
||||||
getInitialState: ->
|
getInitialState: ->
|
||||||
{
|
{
|
||||||
|
@ -62,6 +63,8 @@ module.exports = class TeacherClassView extends RootView
|
||||||
enrolledUsers: ""
|
enrolledUsers: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTitle: -> return @classroom?.get('name')
|
||||||
|
|
||||||
initialize: (options, classroomID) ->
|
initialize: (options, classroomID) ->
|
||||||
super(options)
|
super(options)
|
||||||
@singleStudentCourseProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-course'
|
@singleStudentCourseProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-course'
|
||||||
|
@ -149,6 +152,8 @@ module.exports = class TeacherClassView extends RootView
|
||||||
@listenTo @students, 'sort', ->
|
@listenTo @students, 'sort', ->
|
||||||
@state.set students: @students
|
@state.set students: @students
|
||||||
@render()
|
@render()
|
||||||
|
@listenTo @, 'course-select:change', ({ selectedCourse }) ->
|
||||||
|
@state.set selectedCourse: selectedCourse
|
||||||
|
|
||||||
setCourseMembers: =>
|
setCourseMembers: =>
|
||||||
for course in @courses.models
|
for course in @courses.models
|
||||||
|
@ -273,6 +278,9 @@ module.exports = class TeacherClassView extends RootView
|
||||||
onKeyPressStudentSearch: (e) ->
|
onKeyPressStudentSearch: (e) ->
|
||||||
@state.set('searchTerm', $(e.target).val())
|
@state.set('searchTerm', $(e.target).val())
|
||||||
|
|
||||||
|
onChangeCourseSelect: (e) ->
|
||||||
|
@trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) }
|
||||||
|
|
||||||
getSelectedStudentIDs: ->
|
getSelectedStudentIDs: ->
|
||||||
@$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
|
@$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
|
||||||
$(checkbox).data('student-id')
|
$(checkbox).data('student-id')
|
||||||
|
|
|
@ -25,6 +25,8 @@ module.exports = class TeacherClassesView extends RootView
|
||||||
'click .add-students-btn': 'onClickAddStudentsButton'
|
'click .add-students-btn': 'onClickAddStudentsButton'
|
||||||
'click .create-classroom-btn': 'onClickCreateClassroomButton'
|
'click .create-classroom-btn': 'onClickCreateClassroomButton'
|
||||||
|
|
||||||
|
getTitle: -> return $.i18n.t('teacher.my_classes')
|
||||||
|
|
||||||
initialize: (options) ->
|
initialize: (options) ->
|
||||||
super(options)
|
super(options)
|
||||||
@classrooms = new Classrooms()
|
@classrooms = new Classrooms()
|
||||||
|
|
|
@ -39,6 +39,8 @@ module.exports = class TeacherCoursesView extends RootView
|
||||||
"569ed916efa72b0ced971447": null
|
"569ed916efa72b0ced971447": null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTitle: -> return $.i18n.t('teacher.courses')
|
||||||
|
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
super(options)
|
super(options)
|
||||||
@ownedClassrooms = new Classrooms()
|
@ownedClassrooms = new Classrooms()
|
||||||
|
|
|
@ -29,6 +29,12 @@ module.exports = class I18NEditLevelView extends I18NEditModelView
|
||||||
@wrapRow 'Guide article name', ['name'], doc.name, i18n[lang]?.name, ['documentation', 'specificArticles', index]
|
@wrapRow 'Guide article name', ['name'], doc.name, i18n[lang]?.name, ['documentation', 'specificArticles', index]
|
||||||
@wrapRow "'#{doc.name}' body", ['body'], doc.body, i18n[lang]?.body, ['documentation', 'specificArticles', index], 'markdown'
|
@wrapRow "'#{doc.name}' body", ['body'], doc.body, i18n[lang]?.body, ['documentation', 'specificArticles', index], 'markdown'
|
||||||
|
|
||||||
|
# hints
|
||||||
|
for hint, index in @model.get('documentation')?.hints ? []
|
||||||
|
if i18n = hint.i18n
|
||||||
|
name = "Hint #{index+1}"
|
||||||
|
@wrapRow "'#{name}' body", ['body'], hint.body, i18n[lang]?.body, ['documentation', 'hints', index], 'markdown'
|
||||||
|
|
||||||
# sprite dialogues
|
# sprite dialogues
|
||||||
for script, scriptIndex in @model.get('scripts') ? []
|
for script, scriptIndex in @model.get('scripts') ? []
|
||||||
for noteGroup, noteGroupIndex in script.noteChain ? []
|
for noteGroup, noteGroupIndex in script.noteChain ? []
|
||||||
|
|
|
@ -61,7 +61,7 @@ module.exports = class LadderPlayModal extends ModalView
|
||||||
|
|
||||||
success = (@nameMap) =>
|
success = (@nameMap) =>
|
||||||
for challenger in _.values(@challengers)
|
for challenger in _.values(@challengers)
|
||||||
challenger.opponentName = @nameMap[challenger.opponentID]?.name or 'Anoner'
|
challenger.opponentName = @nameMap[challenger.opponentID]?.name or 'Anonymous'
|
||||||
challenger.opponentWizard = @nameMap[challenger.opponentID]?.wizard or {}
|
challenger.opponentWizard = @nameMap[challenger.opponentID]?.wizard or {}
|
||||||
@checkWizardLoaded()
|
@checkWizardLoaded()
|
||||||
|
|
||||||
|
|
|
@ -207,7 +207,7 @@ module.exports = class SpectateLevelView extends RootView
|
||||||
findPlayerNames: ->
|
findPlayerNames: ->
|
||||||
playerNames = {}
|
playerNames = {}
|
||||||
for session in [@session, @otherSession] when session?.get('team')
|
for session in [@session, @otherSession] when session?.get('team')
|
||||||
playerNames[session.get('team')] = session.get('creatorName') or 'Anoner'
|
playerNames[session.get('team')] = session.get('creatorName') or 'Anonymous'
|
||||||
playerNames
|
playerNames
|
||||||
|
|
||||||
initGoalManager: ->
|
initGoalManager: ->
|
||||||
|
|
|
@ -353,7 +353,7 @@ module.exports = class PlayLevelView extends RootView
|
||||||
return {} unless @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
|
return {} unless @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
|
||||||
playerNames = {}
|
playerNames = {}
|
||||||
for session in [@session, @otherSession] when session?.get('team')
|
for session in [@session, @otherSession] when session?.get('team')
|
||||||
playerNames[session.get('team')] = session.get('creatorName') or 'Anoner'
|
playerNames[session.get('team')] = session.get('creatorName') or 'Anonymous'
|
||||||
playerNames
|
playerNames
|
||||||
|
|
||||||
# Once Surface is Loaded ####################################################
|
# Once Surface is Loaded ####################################################
|
||||||
|
|
|
@ -87,7 +87,7 @@ emailUserInitialRecruiting = (user, callback) ->
|
||||||
return callback null, false if DEBUGGING and (totalEmailsSent > 1 or Math.random() > 0.05)
|
return callback null, false if DEBUGGING and (totalEmailsSent > 1 or Math.random() > 0.05)
|
||||||
++totalEmailsSent
|
++totalEmailsSent
|
||||||
name = if user.firstName and user.lastName then "#{user.firstName}" else user.name
|
name = if user.firstName and user.lastName then "#{user.firstName}" else user.name
|
||||||
name = 'Wizard' if not name or name is 'Anoner'
|
name = 'Wizard' if not name or name in ['Anoner', 'Anonymous']
|
||||||
team = user.session.levelInfo.team
|
team = user.session.levelInfo.team
|
||||||
team = team.substr(0, team.length - 1)
|
team = team.substr(0, team.length - 1)
|
||||||
context =
|
context =
|
||||||
|
|
|
@ -28,11 +28,11 @@ var deteacher = function deteacher(email) {
|
||||||
else {
|
else {
|
||||||
print('Unset role', db.users.update({_id: user._id}, {$unset: {role: ''}}));
|
print('Unset role', db.users.update({_id: user._id}, {$unset: {role: ''}}));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
db.system.js.save(
|
db.system.js.save(
|
||||||
{
|
{
|
||||||
_id: 'deteacher',
|
_id: 'deteacher',
|
||||||
value: deteacher
|
value: deteacher
|
||||||
}
|
}
|
||||||
)
|
);
|
57
scripts/mongodb/stored/getPrepaidsFor.js
Normal file
57
scripts/mongodb/stored/getPrepaidsFor.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Script for changing prepaid start/end dates and propagating them to users.
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Usage
|
||||||
|
* ---------------
|
||||||
|
* In mongo shell
|
||||||
|
*
|
||||||
|
* > db.loadServerScripts();
|
||||||
|
* > var prepaids = getPrepaidsFor('some@email.com'); // prints basic stats for prepaids found
|
||||||
|
* > prepaids.models // Raw prepaid data
|
||||||
|
* > prepaids.setStart(2001,1,1) // Set start date
|
||||||
|
* > prepaids.setEnd(2100,1,1) // Set end date
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
function getPrepaidsFor(email) {
|
||||||
|
var user = db.users.findOne({emailLower: email.toLowerCase()});
|
||||||
|
if (!user) {
|
||||||
|
print('User not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = {};
|
||||||
|
result.models = db.prepaids.find({creator: user._id}).toArray();
|
||||||
|
result.setStart = function(year, month, day) {
|
||||||
|
var startDate = new Date(Date.UTC(year, month-1, day)).toISOString();
|
||||||
|
print('setting to', startDate);
|
||||||
|
for (var i in this.models) {
|
||||||
|
var prepaid = this.models[i];
|
||||||
|
print('Prepaid update', db.prepaids.update({_id: prepaid._id}, {$set: {startDate: startDate}}));
|
||||||
|
print('User update', db.users.update({'coursePrepaid._id': prepaid._id}, {$set: {'coursePrepaid.startDate': startDate}}, {multi: true}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
result.setEnd = function(year, month, day) {
|
||||||
|
var endDate = new Date(Date.UTC(year, month-1, day)).toISOString();
|
||||||
|
print('setting to', endDate);
|
||||||
|
for (var i in this.models) {
|
||||||
|
var prepaid = this.models[i];
|
||||||
|
print('Prepaid update', db.prepaids.update({_id: prepaid._id}, {$set: {endDate: endDate}}));
|
||||||
|
print('User update', db.users.update({'coursePrepaid._id': prepaid._id}, {$set: {'coursePrepaid.endDate': endDate}}, {multi: true}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i in result.models) {
|
||||||
|
var prepaid = result.models[i];
|
||||||
|
print('Prepaid:', prepaid.startDate, 'to', prepaid.endDate, 'with', prepaid.redeemers.length, '/', prepaid.maxRedeemers, 'uses');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
db.system.js.save(
|
||||||
|
{
|
||||||
|
_id: 'getPrepaidsFor',
|
||||||
|
value: getPrepaidsFor
|
||||||
|
}
|
||||||
|
);
|
13
scripts/vagrant/core/update-brunchv2.sh
Normal file
13
scripts/vagrant/core/update-brunchv2.sh
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#!/bin/bash -e
|
||||||
|
# Original content copyright (c) 2014 dpen2000 licensed under the MIT license
|
||||||
|
|
||||||
|
echo "updating brunch to v2..."
|
||||||
|
cd /vagrant
|
||||||
|
npm install \
|
||||||
|
brunch@">=2.0.0" \
|
||||||
|
auto-reload-brunch@">=2.0.0" \
|
||||||
|
coffee-script-brunch@">=2.0.0" \
|
||||||
|
coffeelint-brunch@">=2.0.0" \
|
||||||
|
css-brunch@">=2.0.0" \
|
||||||
|
javascript-brunch@">=2.0.0" \
|
||||||
|
sass-brunch@">=2.0.0" --no-bin-links
|
|
@ -5,6 +5,7 @@ Classroom = require './../models/Classroom'
|
||||||
User = require '../models/User'
|
User = require '../models/User'
|
||||||
sendwithus = require '../sendwithus'
|
sendwithus = require '../sendwithus'
|
||||||
utils = require '../lib/utils'
|
utils = require '../lib/utils'
|
||||||
|
log = require 'winston'
|
||||||
UserHandler = require './user_handler'
|
UserHandler = require './user_handler'
|
||||||
|
|
||||||
ClassroomHandler = class ClassroomHandler extends Handler
|
ClassroomHandler = class ClassroomHandler extends Handler
|
||||||
|
@ -68,13 +69,16 @@ ClassroomHandler = class ClassroomHandler extends Handler
|
||||||
return _.omit(doc.toObject(), 'code', 'codeCamel')
|
return _.omit(doc.toObject(), 'code', 'codeCamel')
|
||||||
|
|
||||||
inviteStudents: (req, res, classroomID) ->
|
inviteStudents: (req, res, classroomID) ->
|
||||||
|
return @sendUnauthorizedError(res) if not req.user?
|
||||||
if not req.body.emails
|
if not req.body.emails
|
||||||
return @sendBadInputError(res, 'Emails not included')
|
return @sendBadInputError(res, 'Emails not included')
|
||||||
|
|
||||||
Classroom.findById classroomID, (err, classroom) =>
|
Classroom.findById classroomID, (err, classroom) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
return @sendNotFoundError(res) unless classroom
|
return @sendNotFoundError(res) unless classroom
|
||||||
return @sendForbiddenError(res) unless classroom.get('ownerID').equals(req.user.get('_id'))
|
unless classroom.get('ownerID').equals(req.user.get('_id'))
|
||||||
|
log.debug "classroom_handler.inviteStudents: Can't invite to classroom (#{classroom.id}) you (#{req.user.get('_id')}) don't own"
|
||||||
|
return @sendForbiddenError(res)
|
||||||
|
|
||||||
for email in req.body.emails
|
for email in req.body.emails
|
||||||
joinCode = (classroom.get('codeCamel') or classroom.get('code'))
|
joinCode = (classroom.get('codeCamel') or classroom.get('code'))
|
||||||
|
@ -83,6 +87,7 @@ ClassroomHandler = class ClassroomHandler extends Handler
|
||||||
recipient:
|
recipient:
|
||||||
address: email
|
address: email
|
||||||
email_data:
|
email_data:
|
||||||
|
teacher_name: req.user.broadName()
|
||||||
class_name: classroom.get('name')
|
class_name: classroom.get('name')
|
||||||
join_link: "https://codecombat.com/courses?_cc=" + joinCode
|
join_link: "https://codecombat.com/courses?_cc=" + joinCode
|
||||||
join_code: joinCode
|
join_code: joinCode
|
||||||
|
@ -91,13 +96,17 @@ ClassroomHandler = class ClassroomHandler extends Handler
|
||||||
|
|
||||||
get: (req, res) ->
|
get: (req, res) ->
|
||||||
if ownerID = req.query.ownerID
|
if ownerID = req.query.ownerID
|
||||||
return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or ownerID is req.user.id)
|
unless req.user and (req.user.isAdmin() or ownerID is req.user.id)
|
||||||
|
log.debug "classroom_handler.get: ownerID (#{ownerID}) must be yourself (#{req.user?.id})"
|
||||||
|
return @sendForbiddenError(res)
|
||||||
return @sendBadInputError(res, 'Bad ownerID') unless utils.isID ownerID
|
return @sendBadInputError(res, 'Bad ownerID') unless utils.isID ownerID
|
||||||
Classroom.find {ownerID: mongoose.Types.ObjectId(ownerID)}, (err, classrooms) =>
|
Classroom.find {ownerID: mongoose.Types.ObjectId(ownerID)}, (err, classrooms) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
return @sendSuccess(res, (@formatEntity(req, classroom) for classroom in classrooms))
|
return @sendSuccess(res, (@formatEntity(req, classroom) for classroom in classrooms))
|
||||||
else if memberID = req.query.memberID
|
else if memberID = req.query.memberID
|
||||||
return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or memberID is req.user.id)
|
unless req.user and (req.user.isAdmin() or memberID is req.user.id)
|
||||||
|
log.debug "classroom_handler.get: memberID (#{memberID}) must be yourself (#{req.user?.id})"
|
||||||
|
return @sendForbiddenError(res)
|
||||||
return @sendBadInputError(res, 'Bad memberID') unless utils.isID memberID
|
return @sendBadInputError(res, 'Bad memberID') unless utils.isID memberID
|
||||||
Classroom.find {members: mongoose.Types.ObjectId(memberID)}, (err, classrooms) =>
|
Classroom.find {members: mongoose.Types.ObjectId(memberID)}, (err, classrooms) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
|
|
|
@ -192,6 +192,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||||
address: email
|
address: email
|
||||||
subject: course.get('name')
|
subject: course.get('name')
|
||||||
email_data:
|
email_data:
|
||||||
|
teacher_name: req.user.broadName()
|
||||||
class_name: course.get('name')
|
class_name: course.get('name')
|
||||||
join_link: "https://codecombat.com/courses/students?_ppc=" + prepaid.get('code')
|
join_link: "https://codecombat.com/courses/students?_ppc=" + prepaid.get('code')
|
||||||
sendwithus.api.send context, _.noop
|
sendwithus.api.send context, _.noop
|
||||||
|
|
|
@ -68,4 +68,5 @@ module.exports =
|
||||||
return done(null, activity.sender) if /@codecombat\.com/ig.test(activity.sender)
|
return done(null, activity.sender) if /@codecombat\.com/ig.test(activity.sender)
|
||||||
return done(null, config.mail.supportSchools)
|
return done(null, config.mail.supportSchools)
|
||||||
catch error
|
catch error
|
||||||
|
log.error("closeIO.getSalesContactEmail Error for #{email}: #{JSON.stringify(error)}")
|
||||||
return done(error, config.mail.supportSchools)
|
return done(error, config.mail.supportSchools)
|
||||||
|
|
|
@ -3,6 +3,7 @@ utils = require '../lib/utils'
|
||||||
errors = require '../commons/errors'
|
errors = require '../commons/errors'
|
||||||
schemas = require '../../app/schemas/schemas'
|
schemas = require '../../app/schemas/schemas'
|
||||||
wrap = require 'co-express'
|
wrap = require 'co-express'
|
||||||
|
log = require 'winston'
|
||||||
Promise = require 'bluebird'
|
Promise = require 'bluebird'
|
||||||
database = require '../commons/database'
|
database = require '../commons/database'
|
||||||
mongoose = require 'mongoose'
|
mongoose = require 'mongoose'
|
||||||
|
@ -21,6 +22,7 @@ module.exports =
|
||||||
return next() unless code
|
return next() unless code
|
||||||
classroom = yield Classroom.findOne({ code: code.toLowerCase() }).select('name ownerID aceConfig')
|
classroom = yield Classroom.findOne({ code: code.toLowerCase() }).select('name ownerID aceConfig')
|
||||||
if not classroom
|
if not classroom
|
||||||
|
log.debug("classrooms.fetchByCode: Couldn't find Classroom with code: #{code}")
|
||||||
throw new errors.NotFound('Classroom not found.')
|
throw new errors.NotFound('Classroom not found.')
|
||||||
classroom = classroom.toObject()
|
classroom = classroom.toObject()
|
||||||
# Tack on the teacher's name for display to the user
|
# Tack on the teacher's name for display to the user
|
||||||
|
@ -33,7 +35,9 @@ module.exports =
|
||||||
return next() unless ownerID
|
return next() unless ownerID
|
||||||
throw new errors.UnprocessableEntity('Bad ownerID') unless utils.isID ownerID
|
throw new errors.UnprocessableEntity('Bad ownerID') unless utils.isID ownerID
|
||||||
throw new errors.Unauthorized() unless req.user
|
throw new errors.Unauthorized() unless req.user
|
||||||
throw new errors.Forbidden('"ownerID" must be yourself') unless req.user.isAdmin() or ownerID is req.user.id
|
unless req.user.isAdmin() or ownerID is req.user.id
|
||||||
|
log.debug("classrooms.getByOwner: Can't fetch classroom you don't own. User: #{req.user.id} Owner: #{ownerID}")
|
||||||
|
throw new errors.Forbidden('"ownerID" must be yourself')
|
||||||
sanitizedOptions = {}
|
sanitizedOptions = {}
|
||||||
unless _.isUndefined(options.archived)
|
unless _.isUndefined(options.archived)
|
||||||
# Handles when .archived is true, vs false-or-null
|
# Handles when .archived is true, vs false-or-null
|
||||||
|
@ -114,6 +118,7 @@ module.exports =
|
||||||
isOwner = classroom.get('ownerID').equals(req.user._id)
|
isOwner = classroom.get('ownerID').equals(req.user._id)
|
||||||
isMember = req.user.id in (m.toString() for m in classroom.get('members'))
|
isMember = req.user.id in (m.toString() for m in classroom.get('members'))
|
||||||
unless req.user.isAdmin() or isOwner or isMember
|
unless req.user.isAdmin() or isOwner or isMember
|
||||||
|
log.debug "classrooms.fetchMembers: Can't fetch members for class (#{classroom.id}) you (#{req.user.id}) don't own and aren't a member of."
|
||||||
throw new errors.Forbidden('You do not own this classroom.')
|
throw new errors.Forbidden('You do not own this classroom.')
|
||||||
memberIDs = classroom.get('members') or []
|
memberIDs = classroom.get('members') or []
|
||||||
memberIDs = memberIDs.slice(memberSkip, memberSkip + memberLimit)
|
memberIDs = memberIDs.slice(memberSkip, memberSkip + memberLimit)
|
||||||
|
@ -126,7 +131,9 @@ module.exports =
|
||||||
|
|
||||||
post: wrap (req, res) ->
|
post: wrap (req, res) ->
|
||||||
throw new errors.Unauthorized() unless req.user and not req.user.isAnonymous()
|
throw new errors.Unauthorized() unless req.user and not req.user.isAnonymous()
|
||||||
throw new errors.Forbidden() unless req.user?.isTeacher()
|
unless req.user?.isTeacher()
|
||||||
|
log.debug "classrooms.post: Can't create classroom if you (#{req.user?.id}) aren't a teacher."
|
||||||
|
throw new errors.Forbidden()
|
||||||
classroom = database.initDoc(req, Classroom)
|
classroom = database.initDoc(req, Classroom)
|
||||||
classroom.set 'ownerID', req.user._id
|
classroom.set 'ownerID', req.user._id
|
||||||
classroom.set 'members', []
|
classroom.set 'members', []
|
||||||
|
@ -159,11 +166,13 @@ module.exports =
|
||||||
unless req.body?.code
|
unless req.body?.code
|
||||||
throw new errors.UnprocessableEntity('Need a code')
|
throw new errors.UnprocessableEntity('Need a code')
|
||||||
if req.user.isTeacher()
|
if req.user.isTeacher()
|
||||||
|
log.debug("classrooms.join: Cannot join a classroom as a teacher: #{req.user.id}")
|
||||||
throw new errors.Forbidden('Cannot join a classroom as a teacher')
|
throw new errors.Forbidden('Cannot join a classroom as a teacher')
|
||||||
code = req.body.code.toLowerCase()
|
code = req.body.code.toLowerCase()
|
||||||
classroom = yield Classroom.findOne({code: code})
|
classroom = yield Classroom.findOne({code: code})
|
||||||
if not classroom
|
if not classroom
|
||||||
throw new errors.NotFound('Classroom not found.')
|
log.debug("classrooms.join: Classroom not found with code #{code}")
|
||||||
|
throw new errors.NotFound("Classroom not found with code #{code}")
|
||||||
members = _.clone(classroom.get('members'))
|
members = _.clone(classroom.get('members'))
|
||||||
if _.any(members, (memberID) -> memberID.equals(req.user._id))
|
if _.any(members, (memberID) -> memberID.equals(req.user._id))
|
||||||
return res.send(classroom.toObject({req: req}))
|
return res.send(classroom.toObject({req: req}))
|
||||||
|
@ -196,10 +205,12 @@ module.exports =
|
||||||
ownedStudentIDs = _.flatten ownedClassrooms.map (c) ->
|
ownedStudentIDs = _.flatten ownedClassrooms.map (c) ->
|
||||||
c.get('members').map (id) ->
|
c.get('members').map (id) ->
|
||||||
id.toString()
|
id.toString()
|
||||||
return next() unless memberID in ownedStudentIDs
|
unless memberID in ownedStudentIDs
|
||||||
|
throw new errors.Forbidden("Can't reset the password of a student that's not in one of your classrooms.")
|
||||||
student = yield User.findById(memberID)
|
student = yield User.findById(memberID)
|
||||||
if student.get('emailVerified')
|
if student.get('emailVerified')
|
||||||
return next new errors.Forbidden("Can't reset password for a student that has verified their email address.")
|
log.debug "classrooms.setStudentPassword: Can't reset password for a student (#{memberID}) that has verified their email address."
|
||||||
|
throw new errors.Forbidden("Can't reset password for a student that has verified their email address.")
|
||||||
{ valid, error } = tv4.validateResult(newPassword, schemas.passwordString)
|
{ valid, error } = tv4.validateResult(newPassword, schemas.passwordString)
|
||||||
unless valid
|
unless valid
|
||||||
throw new errors.UnprocessableEntity(error.message)
|
throw new errors.UnprocessableEntity(error.message)
|
||||||
|
|
|
@ -49,7 +49,7 @@ UserSchema.methods.broadName = ->
|
||||||
return name if name
|
return name if name
|
||||||
[emailName, emailDomain] = @get('email').split('@')
|
[emailName, emailDomain] = @get('email').split('@')
|
||||||
return emailName if emailName
|
return emailName if emailName
|
||||||
return 'Anoner'
|
return 'Anonymous'
|
||||||
|
|
||||||
UserSchema.methods.isInGodMode = ->
|
UserSchema.methods.isInGodMode = ->
|
||||||
p = @get('permissions')
|
p = @get('permissions')
|
||||||
|
|
|
@ -55,6 +55,7 @@ createMailContext = (req, done) ->
|
||||||
email_data:
|
email_data:
|
||||||
subject: "[CodeCombat] #{subject ? ('Feedback - ' + fromAddress)}"
|
subject: "[CodeCombat] #{subject ? ('Feedback - ' + fromAddress)}"
|
||||||
content: content
|
content: content
|
||||||
|
contentHTML: content.replace /\n/g, '\n<br>'
|
||||||
if recipientID is 'schools@codecombat.com' or teacher
|
if recipientID is 'schools@codecombat.com' or teacher
|
||||||
req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if recipientID is 'schools@codecombat.com'
|
req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if recipientID is 'schools@codecombat.com'
|
||||||
closeIO.getSalesContactEmail fromAddress, (err, salesContactEmail) ->
|
closeIO.getSalesContactEmail fromAddress, (err, salesContactEmail) ->
|
||||||
|
@ -78,7 +79,7 @@ createMailContext = (req, done) ->
|
||||||
], (err, results) ->
|
], (err, results) ->
|
||||||
console.error "Error getting contact message context for #{sender}: #{err}" if err
|
console.error "Error getting contact message context for #{sender}: #{err}" if err
|
||||||
if req.body.screenshotURL
|
if req.body.screenshotURL
|
||||||
context.email_data.content += "\n<img src='#{req.body.screenshotURL}' />"
|
context.email_data.contentHTML += "\n<br><img src='#{req.body.screenshotURL}' />"
|
||||||
done context
|
done context
|
||||||
|
|
||||||
fetchRecentSessions = (user, context, sentFromLevel, callback) ->
|
fetchRecentSessions = (user, context, sentFromLevel, callback) ->
|
||||||
|
@ -98,5 +99,5 @@ fetchRecentSessions = (user, context, sentFromLevel, callback) ->
|
||||||
if sentFromLevel?.levelID is s.levelID and sentFromLevel?.courseID
|
if sentFromLevel?.levelID is s.levelID and sentFromLevel?.courseID
|
||||||
url += "&course=#{sentFromLevel.courseID}&course-instance=#{sentFromLevel.courseInstanceID}"
|
url += "&course=#{sentFromLevel.courseID}&course-instance=#{sentFromLevel.courseInstanceID}"
|
||||||
urlName += ' (course)'
|
urlName += ' (course)'
|
||||||
context.email_data.content += "\n<a href='#{url}'>#{urlName}</a>#{sessionStatus}"
|
context.email_data.contentHTML += "\n<br><a href='#{url}'>#{urlName}</a>#{sessionStatus}"
|
||||||
callback null
|
callback null
|
||||||
|
|
|
@ -578,7 +578,7 @@ sendLadderUpdateEmail = (session, now, daysAgo) ->
|
||||||
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had levelName #{session.levelName} or team #{session.team} in it."
|
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had levelName #{session.levelName} or team #{session.team} in it."
|
||||||
return
|
return
|
||||||
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
|
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
|
||||||
name = 'Wizard' if not name or name is 'Anoner'
|
name = 'Wizard' if not name or name is 'Anonymous'
|
||||||
|
|
||||||
# Fetch the most recent defeat and victory, if there are any.
|
# Fetch the most recent defeat and victory, if there are any.
|
||||||
# (We could look at strongest/weakest, but we'd have to fetch everyone, or denormalize more.)
|
# (We could look at strongest/weakest, but we'd have to fetch everyone, or denormalize more.)
|
||||||
|
@ -622,13 +622,13 @@ sendLadderUpdateEmail = (session, now, daysAgo) ->
|
||||||
if err
|
if err
|
||||||
log.error "Couldn't find defeateded opponent: #{err}"
|
log.error "Couldn't find defeateded opponent: #{err}"
|
||||||
defeatedOpponent = null
|
defeatedOpponent = null
|
||||||
victoryContext = {opponent_name: defeatedOpponent?.name ? 'Anoner', url: urlForMatch(victory)} if victory
|
victoryContext = {opponent_name: defeatedOpponent?.name ? 'Anonymous', url: urlForMatch(victory)} if victory
|
||||||
|
|
||||||
onFetchedVictoriousOpponent = (err, victoriousOpponent) ->
|
onFetchedVictoriousOpponent = (err, victoriousOpponent) ->
|
||||||
if err
|
if err
|
||||||
log.error "Couldn't find victorious opponent: #{err}"
|
log.error "Couldn't find victorious opponent: #{err}"
|
||||||
victoriousOpponent = null
|
victoriousOpponent = null
|
||||||
defeatContext = {opponent_name: victoriousOpponent?.name ? 'Anoner', url: urlForMatch(defeat)} if defeat
|
defeatContext = {opponent_name: victoriousOpponent?.name ? 'Anonymous', url: urlForMatch(defeat)} if defeat
|
||||||
|
|
||||||
Level.find({original: session.level.original, created: {$gt: session.submitDate}}).select('created commitMessage version').sort('-created').lean().exec (err, levelVersions) ->
|
Level.find({original: session.level.original, created: {$gt: session.submitDate}}).select('created commitMessage version').sort('-created').lean().exec (err, levelVersions) ->
|
||||||
sendEmail defeatContext, victoryContext, (if levelVersions.length then levelVersions else null)
|
sendEmail defeatContext, victoryContext, (if levelVersions.length then levelVersions else null)
|
||||||
|
@ -706,7 +706,7 @@ sendNextStepsEmail = (user, now, daysAgo) ->
|
||||||
do (err, nextLevel) ->
|
do (err, nextLevel) ->
|
||||||
return log.error "Couldn't find next level for #{user.get('email')}: #{err}" if err
|
return log.error "Couldn't find next level for #{user.get('email')}: #{err}" if err
|
||||||
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
|
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
|
||||||
name = 'hero' if not name or name is 'Anoner'
|
name = 'Hero' if not name or name in ['Anoner', 'Anonymous']
|
||||||
#secretLevel = switch user.get('testGroupNumber') % 8
|
#secretLevel = switch user.get('testGroupNumber') % 8
|
||||||
# when 0, 1, 2, 3 then name: 'Forgetful Gemsmith', slug: 'forgetful-gemsmith'
|
# when 0, 1, 2, 3 then name: 'Forgetful Gemsmith', slug: 'forgetful-gemsmith'
|
||||||
# when 4, 5, 6, 7 then name: 'Signs and Portents', slug: 'signs-and-portents'
|
# when 4, 5, 6, 7 then name: 'Signs and Portents', slug: 'signs-and-portents'
|
||||||
|
|
|
@ -447,3 +447,73 @@ describe 'GET /db/classroom/:handle/members', ->
|
||||||
expect(user.email).toBeDefined()
|
expect(user.email).toBeDefined()
|
||||||
expect(user.passwordHash).toBeUndefined()
|
expect(user.passwordHash).toBeUndefined()
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
describe 'POST /db/classroom/:classroomID/members/:memberID/reset-password', ->
|
||||||
|
it 'changes the password', utils.wrap (done) ->
|
||||||
|
yield utils.clearModels([User, Classroom])
|
||||||
|
teacher = yield utils.initUser()
|
||||||
|
yield utils.loginUser(teacher)
|
||||||
|
student = yield utils.initUser({ name: "Firstname Lastname" })
|
||||||
|
newPassword = "this is a new password"
|
||||||
|
classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student._id] }).save()
|
||||||
|
expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword))
|
||||||
|
[res, body] = yield request.postAsync({
|
||||||
|
uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password")
|
||||||
|
json: { password: newPassword }
|
||||||
|
})
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
changedStudent = yield User.findById(student.id)
|
||||||
|
expect(changedStudent.get('passwordHash')).toEqual(User.hashPassword(newPassword))
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "doesn't change the password if you're not their teacher", utils.wrap (done) ->
|
||||||
|
yield utils.clearModels([User, Classroom])
|
||||||
|
teacher = yield utils.initUser()
|
||||||
|
yield utils.loginUser(teacher)
|
||||||
|
student = yield utils.initUser({ name: "Firstname Lastname" })
|
||||||
|
student2 = yield utils.initUser({ name: "Firstname Lastname 2" })
|
||||||
|
newPassword = "this is a new password"
|
||||||
|
classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student2._id] }).save()
|
||||||
|
expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword))
|
||||||
|
[res, body] = yield request.postAsync({
|
||||||
|
uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password")
|
||||||
|
json: { password: newPassword }
|
||||||
|
})
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
changedStudent = yield User.findById(student.id)
|
||||||
|
expect(changedStudent.get('passwordHash')).toEqual(student.get('passwordHash'))
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "doesn't change the password if their email is verified", utils.wrap (done) ->
|
||||||
|
yield utils.clearModels([User, Classroom])
|
||||||
|
teacher = yield utils.initUser()
|
||||||
|
yield utils.loginUser(teacher)
|
||||||
|
student = yield utils.initUser({ name: "Firstname Lastname", emailVerified: true })
|
||||||
|
newPassword = "this is a new password"
|
||||||
|
classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student._id] }).save()
|
||||||
|
expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword))
|
||||||
|
[res, body] = yield request.postAsync({
|
||||||
|
uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password")
|
||||||
|
json: { password: newPassword }
|
||||||
|
})
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
changedStudent = yield User.findById(student.id)
|
||||||
|
expect(changedStudent.get('passwordHash')).toEqual(student.get('passwordHash'))
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "doesn't let you set a 1-character password", utils.wrap (done) ->
|
||||||
|
yield utils.clearModels([User, Classroom])
|
||||||
|
teacher = yield utils.initUser()
|
||||||
|
yield utils.loginUser(teacher)
|
||||||
|
student = yield utils.initUser({ name: "Firstname Lastname" })
|
||||||
|
newPassword = "e"
|
||||||
|
classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student._id] }).save()
|
||||||
|
expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword))
|
||||||
|
[res, body] = yield request.postAsync({
|
||||||
|
uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password")
|
||||||
|
json: { password: newPassword }
|
||||||
|
})
|
||||||
|
expect(res.statusCode).toBe(422)
|
||||||
|
changedStudent = yield User.findById(student.id)
|
||||||
|
expect(changedStudent.get('passwordHash')).toEqual(student.get('passwordHash'))
|
||||||
|
done()
|
||||||
|
|
|
@ -41,6 +41,21 @@ describe 'Level', ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
||||||
|
describe 'POST /db/level/:handle', ->
|
||||||
|
it 'creates a new version', utils.wrap (done) ->
|
||||||
|
yield utils.clearModels([Campaign, Course, CourseInstance, Level, User])
|
||||||
|
admin = yield utils.initAdmin()
|
||||||
|
yield utils.loginUser(admin)
|
||||||
|
@level = yield utils.makeLevel()
|
||||||
|
levelJSON = @level.toObject()
|
||||||
|
levelJSON.name = 'New name'
|
||||||
|
|
||||||
|
url = getURL("/db/level/#{@level.id}")
|
||||||
|
[res, body] = yield request.postAsync({url: url, json: levelJSON})
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
done()
|
||||||
|
|
||||||
|
|
||||||
describe 'GET /db/level/:handle/session', ->
|
describe 'GET /db/level/:handle/session', ->
|
||||||
|
|
||||||
describe 'when level IS a course level', ->
|
describe 'when level IS a course level', ->
|
||||||
|
|
Loading…
Reference in a new issue