Merge remote-tracking branch 'upstream/master'

This commit is contained in:
duybkict 2016-05-28 12:44:51 +07:00
commit 27f270856a
39 changed files with 289 additions and 60 deletions

9
Vagrantfile vendored
View file

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

View file

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

View file

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

View file

@ -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: "Cant wait to learn programming?" text1: "Cant 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:

View file

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

View file

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

View file

@ -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: []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 ? []

View file

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

View file

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

View file

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

View file

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

View file

@ -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
} }
) );

View 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
}
);

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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