Merge all the bugquest fixes!

This commit is contained in:
phoenixeliot 2016-06-02 10:56:06 -07:00
commit 4359eb2caa
68 changed files with 619 additions and 432 deletions

View file

@ -172,9 +172,9 @@ module.exports = class CocoRouter extends Backbone.Router
@navigate e, {trigger: true}
routeDirectly: (path, args=[], options={}) ->
if options.teachersOnly and not me.isTeacher()
if options.teachersOnly and not (me.isTeacher() or me.isAdmin())
return @routeDirectly('teachers/RestrictedToTeachersView')
if options.studentsOnly and not me.isStudent()
if options.studentsOnly and not (me.isStudent() or me.isAdmin())
return @routeDirectly('courses/RestrictedToStudentsView')
leavingMessage = _.result(window.currentView, 'onLeaveMessage')
if leavingMessage

View file

@ -169,7 +169,7 @@ module.exports = class Tracker extends CocoClass
eventObject["user"] = me.id
dataToSend = JSON.stringify eventObject
# console.log dataToSend if debugAnalytics
$.post("#{window.location.protocol or 'http:'}//analytics.codecombat.com/analytics", dataToSend).fail ->
$.post("#{window.location.protocol or 'http:'}//analytics-cf.codecombat.com/analytics", dataToSend).fail ->
console.error "Analytics post failed!"
else
request = @supermodel.addRequestResource {

View file

@ -259,8 +259,11 @@ module.exports.filterMarkdownCodeLanguages = (text, language) ->
return '' unless text
currentLanguage = language or me.get('aceConfig')?.language or 'python'
excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'java', 'io'], currentLanguage
exclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
text.replace exclusionRegex, ''
# Exclude language-specific code blocks like ```python (... code ...)``` for each non-target language.
codeBlockExclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
# Exclude language-specific images like ![python - image description](image url) for each non-target language.
imageExclusionRegex = new RegExp "!\\[(#{excludedLanguages.join('|')}) - .+?\\]\\(.+?\\)\n?", 'gm'
return text.replace(codeBlockExclusionRegex, '').replace(imageExclusionRegex, '')
module.exports.aceEditModes = aceEditModes =
'javascript': 'ace/mode/javascript'

View file

@ -57,11 +57,11 @@ module.exports = class CountdownScreen extends CocoClass
else
@endCountdown()
hide: ->
hide: (duration=500) ->
return unless @showing
@showing = false
createjs.Tween.removeTweens @dimLayer
createjs.Tween.get(@dimLayer).to({alpha: 0}, 500).call => @layer.removeChild @dimLayer unless @destroyed
createjs.Tween.get(@dimLayer).to({alpha: 0}, duration).call => @layer.removeChild @dimLayer unless @destroyed
decrementCountdown: =>
return if @destroyed
@ -85,4 +85,4 @@ module.exports = class CountdownScreen extends CocoClass
onRealTimePlaybackEnded: (e) ->
clearInterval @countdownInterval if @countdownInterval
@countdownInterval = null
@hide()
@hide Math.max(500, 1000 * (@secondsRemaining or 0))

View file

@ -74,16 +74,16 @@ module.exports = class Label extends CocoClass
o.marginY = {D: 6, S: 4, N: 3}[st]
o.fontWeight = {D: 'bold', S: 'bold', N: 'bold'}[st]
o.shadow = {D: false, S: true, N: true}[st]
o.shadowColor = {D: '#FFF', S: '#000', N: '#FFF'}[st]
o.shadowColor = {D: '#FFF', S: '#000', N: '#000'}[st]
o.fontSize = {D: 25, S: 12, N: 24}[st]
fontFamily = {D: 'Arial', S: 'Arial', N: 'Arial'}[st]
o.fontDescriptor = "#{o.fontWeight} #{o.fontSize}px #{fontFamily}"
o.fontColor = {D: '#000', S: '#FFF', N: '#0a0'}[st]
o.fontColor = {D: '#000', S: '#FFF', N: '#6c6'}[st]
if @style is 'name' and @sprite?.thang?.team is 'humans'
o.fontColor = '#a00'
o.fontColor = '#c66'
else if @style is 'name' and @sprite?.thang?.team is 'ogres'
o.fontColor = '#00a'
o.backgroundFillColor = {D: 'white', S: 'rgba(0,0,0,0.4)', N: 'rgba(255,255,255,0.5)'}[st]
o.fontColor = '#66c'
o.backgroundFillColor = {D: 'white', S: 'rgba(0,0,0,0.4)', N: 'rgba(0,0,0,0.7)'}[st]
o.backgroundStrokeColor = {D: 'black', S: 'rgba(0,0,0,0.6)', N: 'rgba(0,0,0,0)'}[st]
o.backgroundStrokeStyle = {D: 2, S: 1, N: 1}[st]
o.backgroundBorderRadius = {D: 10, S: 3, N: 3}[st]

View file

@ -108,7 +108,7 @@ module.exports = class ThangState
storage = @trackedPropertyValues[propIndex]
value = @getStoredProp propIndex, type, storage
if prop is 'pos'
if @thang.teleport and @thang.pos.distanceSquared(value) > 900
if (@thang.teleport and @thang.pos.distanceSquared(value) > 900) or (@thang.pos.x is 0 and @thang.pos.y is 0)
# Don't interpolate; it was probably a teleport. https://github.com/codecombat/codecombat/issues/738
@thang.pos = value
else

View file

@ -5,14 +5,14 @@ class Vector
for name in ['add', 'subtract', 'multiply', 'divide', 'limit', 'normalize', 'rotate']
do (name) ->
Vector[name] = (a, b, useZ) ->
a.copy()["#{name}Self"](b, useZ)
a.copy()[name](b, useZ)
for name in ['magnitude', 'heading', 'distance', 'dot', 'equals', 'copy', 'distanceSquared']
do (name) ->
Vector[name] = (a, b, useZ) ->
a[name](b, useZ)
isVector: true
apiProperties: ['x', 'y', 'z', 'magnitude', 'heading', 'distance', 'dot', 'equals', 'copy', 'distanceSquared', 'rotate', 'add', 'subtract', 'multiply', 'divide', 'limit', 'normalize', 'rotate']
apiProperties: ['x', 'y', 'z', 'magnitude', 'heading', 'distance', 'dot', 'equals', 'copy', 'distanceSquared', 'add', 'subtract', 'multiply', 'divide', 'limit', 'normalize', 'rotate']
constructor: (x=0, y=0, z=0) ->
return new Vector x, y, z unless @ instanceof Vector
@ -24,68 +24,67 @@ class Vector
# Mutating methods:
normalizeSelf: (useZ) ->
normalize: (useZ) ->
m = @magnitude useZ
@divideSelf m, useZ if m > 0
@divide m, useZ if m > 0
@
normalize: (useZ) ->
# Hack to detect when we are in player code so we can avoid mutation
(if @__aetherAPIValue? then @copy() else @).normalizeSelf(useZ)
esper_normalize: (useZ) ->
@copy().normalize(useZ)
limitSelf: (max) ->
limit: (max) ->
if @magnitude() > max
@normalizeSelf()
@multiplySelf(max)
@normalize()
@multiply(max)
else
@
limit: (useZ) ->
(if @__aetherAPIValue? then @copy() else @).limitSelf(useZ)
esper_limit: (max) ->
@copy().limit(max)
subtractSelf: (other, useZ) ->
subtract: (other, useZ) ->
@x -= other.x
@y -= other.y
@z -= other.z if useZ
@
subtract: (other, useZ) ->
(if @__aetherAPIValue? then @copy() else @).subtractSelf(other, useZ)
esper_subtract: (other, useZ) ->
@copy().subtract(other, useZ)
addSelf: (other, useZ) ->
add: (other, useZ) ->
@x += other.x
@y += other.y
@z += other.z if useZ
@
add: (other, useZ) ->
(if @__aetherAPIValue? then @copy() else @).addSelf(other, useZ)
esper_add: (other, useZ) ->
@copy().add(other, useZ)
divideSelf: (n, useZ) ->
divide: (n, useZ) ->
[@x, @y] = [@x / n, @y / n]
@z = @z / n if useZ
@
divide: (n, useZ) ->
(if @__aetherAPIValue? then @copy() else @).divideSelf(n, useZ)
esper_divide: (n, useZ) ->
@copy().divide(n, useZ)
multiplySelf: (n, useZ) ->
multiply: (n, useZ) ->
[@x, @y] = [@x * n, @y * n]
@z = @z * n if useZ
@
multiply: (n, useZ) ->
(if @__aetherAPIValue? then @copy() else @).multiplySelf(n, useZ)
esper_multiply: (n, useZ) ->
@copy().multiply(n, useZ)
# Rotate it around the origin
# If we ever want to make this also use z: https://en.wikipedia.org/wiki/Axes_conventions
rotateSelf: (theta) ->
rotate: (theta) ->
return @ unless theta
[@x, @y] = [Math.cos(theta) * @x - Math.sin(theta) * @y, Math.sin(theta) * @x + Math.cos(theta) * @y]
@
rotate: (theta) ->
(if @__aetherAPIValue? then @copy() else @).rotateSelf(theta)
esper_rotate: (theta) ->
@copy().rotate(theta)
# Non-mutating methods:
@ -127,7 +126,7 @@ class Vector
# Not the strict projection, the other isn't converted to a unit vector first.
projectOnto: (other, useZ) ->
other.copy().multiplySelf(@dot(other, useZ), useZ)
other.copy().multiply(@dot(other, useZ), useZ)
isZero: (useZ) ->
result = @x is 0 and @y is 0

View file

@ -711,9 +711,6 @@
music_label: "Music"
music_description: "Turn background music on/off."
editor_config_title: "Editor Configuration"
editor_config_keybindings_label: "Key Bindings"
editor_config_keybindings_default: "Default (Ace)"
editor_config_keybindings_description: "Adds additional shortcuts known from the common editors."
editor_config_livecompletion_label: "Live Autocompletion"
editor_config_livecompletion_description: "Displays autocomplete suggestions while typing."
editor_config_invisibles_label: "Show Invisibles"
@ -1342,6 +1339,7 @@
unarchive_class: "unarchive class"
unarchive_this_class: "Unarchive this class"
no_students_yet: "This class has no students yet."
try_refreshing: "(You may need to refresh the page)"
add_students: "Add Students"
create_new_class: "Create a New Class"
class_overview: "Class Overview" # View Class page
@ -1424,6 +1422,7 @@
status_expired: "Expired on {{date}}"
status_not_enrolled: "Not Enrolled"
status_enrolled: "Expires on {{date}}"
select_all: "Select All"
classes:
archmage_title: "Archmage"

View file

@ -8,7 +8,6 @@ module.exports = class Campaign extends CocoModel
@className: 'Campaign'
@schema: schema
urlRoot: '/db/campaign'
saveBackups: true
@denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position', 'rewards']))
@denormalizedCampaignProperties: ['name', 'i18n', 'slug']

View file

@ -121,3 +121,10 @@ module.exports = class Classroom extends CocoModel
url: _.result(courseInstance, 'url') + '/classroom'
})
@fetch(options)
inviteMembers: (emails, options={}) ->
options.data ?= {}
options.data.email = emails
options.url = @url() + '/invite-members'
options.type = 'POST'
@fetch(options)

View file

@ -59,11 +59,13 @@ module.exports = class Level extends CocoModel
denormalize: (supermodel, session, otherSession) ->
o = $.extend true, {}, @attributes
if o.thangs and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
thangTypesWithComponents = (tt for tt in supermodel.getModels(ThangType) when tt.get('components')?)
thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original') # Optimization
for levelThang in o.thangs
@denormalizeThang(levelThang, supermodel, session, otherSession)
@denormalizeThang(levelThang, supermodel, session, otherSession, thangTypesByOriginal)
o
denormalizeThang: (levelThang, supermodel, session, otherSession) ->
denormalizeThang: (levelThang, supermodel, session, otherSession, thangTypesByOriginal) ->
levelThang.components ?= []
isHero = /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
if isHero and otherSession
@ -79,7 +81,7 @@ module.exports = class Level extends CocoModel
if isHero
placeholders = {}
placeholdersUsed = {}
placeholderThangType = supermodel.getModelByOriginal(ThangType, levelThang.thangType)
placeholderThangType = thangTypesByOriginal[levelThang.thangType]
unless placeholderThangType
console.error "Couldn't find placeholder ThangType for the hero!"
isHero = false
@ -92,7 +94,7 @@ module.exports = class Level extends CocoModel
heroThangType = session?.get('heroConfig')?.thangType
levelThang.thangType = heroThangType if heroThangType
thangType = supermodel.getModelByOriginal(ThangType, levelThang.thangType, (m) -> m.get('components')?)
thangType = thangTypesByOriginal[levelThang.thangType]
configs = {}
for thangComponent in levelThang.components
@ -168,11 +170,16 @@ module.exports = class Level extends CocoModel
# Decision? Just special case the sort logic in here until we have more examples than these two and decide how best to handle most of the cases then, since we don't really know the whole of the problem yet.
# TODO: anything that depends on Programmable will break right now.
originalsToComponents = _.indexBy levelComponents, 'original' # Optimization for speed
alliedComponent = _.find levelComponents, name: 'Allied'
actsComponent = _.find levelComponents, name: 'Acts'
for thang in thangs ? []
originalsToThangComponents = _.indexBy thang.components, 'original'
sorted = []
visit = (c, namesToIgnore) ->
return if c in sorted
lc = _.find levelComponents, {original: c.original}
lc = originalsToComponents[c.original]
console.error thang.id or thang.name, 'couldn\'t find lc for', c, 'of', levelComponents unless lc
return unless lc
return if namesToIgnore and lc.name in namesToIgnore
@ -184,20 +191,18 @@ module.exports = class Level extends CocoModel
visit c2, [lc.name] for c2 in thang.components
else
for d in lc.dependencies or []
c2 = _.find thang.components, {original: d.original}
c2 = originalsToThangComponents[d.original]
unless c2
dependent = _.find levelComponents, {original: d.original}
dependent = originalsToComponents[d.original]
dependent = dependent?.name or d.original
console.error parentType, thang.id or thang.name, 'does not have dependent Component', dependent, 'from', lc.name
visit c2 if c2
if lc.name is 'Collides'
if allied = _.find levelComponents, {name: 'Allied'}
allied = _.find(thang.components, {original: allied.original})
visit allied if allied
if lc.name is 'Moves'
if acts = _.find levelComponents, {name: 'Acts'}
acts = _.find(thang.components, {original: acts.original})
visit acts if acts
if lc.name is 'Collides' and alliedComponent
if allied = originalsToThangComponents[alliedComponent.original]
visit allied
if lc.name is 'Moves' and actsComponent
if acts = originalsToThangComponents[actsComponent.original]
visit acts
#console.log thang.id, 'sorted comps adding', lc.name
sorted.push c
for comp in thang.components
@ -258,3 +263,14 @@ module.exports = class Level extends CocoModel
else
options.url = "/db/course/#{courseID}/levels/#{levelOriginalID}/next"
@fetch(options)
getSolutions: ->
return [] unless hero = _.find (@get("thangs") ? []), id: 'Hero Placeholder'
return [] unless config = _.find(hero.components ? [], (x) -> x.config?.programmableMethods?.plan)?.config
solutions = _.cloneDeep config.programmableMethods.plan.solutions ? []
for solution in solutions
try
solution.source = _.template(solution.source)(config?.programmableMethods?.plan.context)
catch e
console.error "Problem with template and solution comments for", @get('slug'), e
solutions

View file

@ -16,12 +16,12 @@ module.exports = class ThangType extends CocoModel
samurai: '53e12be0d042f23505c3023b'
raider: '55527eb0b8abf4ba1fe9a107'
goliath: '55e1a6e876cb0948c96af9f8'
guardian: ''
guardian: '566a058620de41290036a745'
ninja: '52fc0ed77e01835453bd8f6c'
'forest-archer': '5466d4f2417c8b48a9811e87'
trapper: '5466d449417c8b48a9811e83'
pixie: ''
assassin: ''
assassin: '566a2202e132c81f00f38c81'
librarian: '52fbf74b7e01835453bd8d8e'
'potion-master': '52e9adf7427172ae56002172'
sorcerer: '52fd1524c7e6cf99160e7bc9'

View file

@ -121,7 +121,7 @@ _.extend UserSchema.properties,
aceConfig: c.object { default: { language: 'python', keyBindings: 'default', invisibles: false, indentGuides: false, behaviors: false, liveCompletion: true }},
language: {type: 'string', 'enum': ['python', 'javascript', 'coffeescript', 'clojure', 'lua', 'java', 'io']}
keyBindings: {type: 'string', 'enum': ['default', 'vim', 'emacs']}
keyBindings: {type: 'string', 'enum': ['default', 'vim', 'emacs']} # Deprecated 2016-05-30; now we just always give them 'default'.
invisibles: {type: 'boolean' }
indentGuides: {type: 'boolean' }
behaviors: {type: 'boolean' }
@ -337,7 +337,7 @@ _.extend UserSchema.properties,
}
}
enrollmentRequestSent: { type: 'boolean' }
schoolName: {type: 'string'}
role: {type: 'string', enum: ["God", "advisor", "parent", "principal", "student", "superintendent", "teacher", "technology coordinator"]}
birthday: c.stringDate({title: "Birthday"})

View file

@ -104,6 +104,12 @@
vertical-align: bottom
td
height: 66px
.select-all
padding-top: 5px
.checkbox-flat
margin-top: 3px
.enroll-student-button
margin-left: 33%

View file

@ -0,0 +1,21 @@
#verifier-view
.verifier-row
margin-top: 20px
.alert
padding: 5px
.campaign-mix, .code-language-mix
padding: 5px 20px 5px 5px
input
margin-right: 8px
.test-running
color: orange
.test-success
color: green
.test-failed
color: red

View file

@ -29,7 +29,7 @@
span.code-background
border-width: 124px 76px 64px 40px
border-image: url(/images/level/code_editor_background_border.png) 124 76 64 40 fill round
border-image: url(/images/level/code_editor_background_border.png) 124 76 64 40 fill stretch
img.code-background
display: none

View file

@ -18,7 +18,7 @@ mixin accountLinks
.style-flat
block header
nav#main-nav.navbar.navbar-default
.container-fluid
.container-fluid.container
.row
.col-lg-12
.navbar-header

View file

@ -39,10 +39,10 @@
// btn.btn.btn-sm.github-login-button#github-login-button
// img(src="/images/pages/modal/auth/github_icon.png")
// | GitHub
#facebook-login-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login
button#facebook-login-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login(disabled=true)
img.network-logo(src="/images/pages/community/logo_facebook.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_facebook")
#gplus-login-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login
button#gplus-login-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(disabled=true)
img.network-logo(src="/images/pages/community/logo_g+.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_gplus")
.gplus-login-wrapper

View file

@ -33,10 +33,10 @@
.col-md-6
.auth-network-logins.text-center
strong(data-i18n="signup.or_sign_up_with")
#facebook-signup-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login
button#facebook-signup-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login(disabled=true)
img.network-logo(src="/images/pages/community/logo_facebook.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_facebook")
#gplus-signup-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login
button#gplus-signup-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(disabled=true)
img.network-logo(src="/images/pages/community/logo_g+.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_gplus")
.gplus-login-wrapper

View file

@ -2,7 +2,7 @@ extends /templates/base
block content
if !me.isAnonymous() && (me.isTeacher() || view.ownedClassrooms.size())
if !me.isAnonymous() && me.isTeacher()
.alert.alert-danger.text-center
// DNT: Temporary
h3 ATTENTION TEACHERS:

View file

@ -2,7 +2,7 @@ extends /templates/base
block content
if me.isTeacher() || view.ownedClassrooms.size()
if me.isTeacher()
.alert.alert-danger.text-center
// DNT: Temporary
h3 ATTENTION TEACHERS:

View file

@ -4,7 +4,7 @@ block content
.container
.row.m-y-3
.col-xs-12
if me.isTeacher() || view.ownedClassrooms.size()
if me.isTeacher()
.alert.alert-danger.text-center
// DNT: Temporary
h3 ATTENTION TEACHERS:

View file

@ -4,6 +4,7 @@ block modal-header-content
.text-center
h1.modal-title(data-i18n="courses.remove_student1")
span.glyphicon.glyphicon-warning-sign.text-danger
p= view.user.get('name', true) + ' - ' + view.user.get('email')
h2(data-i18n="courses.are_you_sure")
block modal-body-content

View file

@ -11,6 +11,7 @@ block content
p(data-i18n='courses.account_restricted')
if me.isAnonymous()
.login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in')
.signup-button.btn.btn-lg.btn-primary-alt(data-i18n="login.sign_up")
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")
@ -18,4 +19,4 @@ block content
if me.isTeacher()
.teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3
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

@ -108,28 +108,36 @@ block content
+copyCodes
+addStudentsButton
ul.nav.nav-tabs.m-t-5(role='tablist')
- var activeTab = state.get('activeTab');
li(class=(activeTab === "#students-tab" ? 'active' : ''))
a.students-tab-btn(href='#students-tab')
.small-details.text-center(data-i18n='teacher.students')
.tab-spacer
li(class=(activeTab === "#course-progress-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#course-progress-tab')
.small-details.text-center(data-i18n='teacher.course_progress')
.tab-spacer
li(class=(activeTab === "#enrollment-status-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#enrollment-status-tab')
.small-details.text-center(data-i18n='teacher.enrollment_status')
.tab-filler
if view.students.length > 0
ul.nav.nav-tabs.m-t-5(role='tablist')
- var activeTab = state.get('activeTab');
li(class=(activeTab === "#students-tab" ? 'active' : ''))
a.students-tab-btn(href='#students-tab')
.small-details.text-center(data-i18n='teacher.students')
.tab-spacer
li(class=(activeTab === "#course-progress-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#course-progress-tab')
.small-details.text-center(data-i18n='teacher.course_progress')
.tab-spacer
li(class=(activeTab === "#enrollment-status-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#enrollment-status-tab')
.small-details.text-center(data-i18n='teacher.enrollment_status')
.tab-filler
.tab-content
if activeTab === '#students-tab'
+studentsTab
else if activeTab === '#course-progress-tab'
+courseProgressTab
else
+enrollmentStatusTab
.tab-content
if activeTab === '#students-tab'
+studentsTab
else if activeTab === '#course-progress-tab'
+courseProgressTab
else
+enrollmentStatusTab
else
.text-center.m-t-5.m-b-5
.text-h2
span(data-i18n="teacher.no_students_yet")
.text-h4
span(data-i18n="teacher.try_refreshing")
mixin breadcrumbs
.breadcrumbs
@ -171,8 +179,8 @@ mixin studentsTab
+bulkAssignControls
table.students-table
thead
th.checkbox-col.select-all
span Select All
th.checkbox-col.select-all.small.text-center
span(data-i18n="teacher.select_all")
.checkbox-flat
input(type='checkbox' id='checkbox-all-students')
label.checkmark(for='checkbox-all-students')

View file

@ -77,7 +77,8 @@ mixin classRow(classroom)
else
each trimCourse, index in classroom.get('courses') || []
- var course = view.courses.get(trimCourse._id);
+progressDot(classroom, course, index)
if view.courseInstances.findWhere({ classroomID: classroom.id, courseID: course.id })
+progressDot(classroom, course, index)
.view-class-arrow.col-xs-1
a.view-class-arrow-inner.glyphicon.glyphicon-chevron-right(data-classroom-id=classroom.id, href=('/teachers/classes/' + classroom.id))

View file

@ -63,7 +63,7 @@ block content
span.spr
| .
span
= level.get('name')
= level.get('name').replace('Course: ', '')
a.play-level-button.btn.btn-lg.btn-primary
span(data-i18n="courses.play_level")
.clearfix

View file

@ -2,80 +2,101 @@ extends /templates/base-flat
block content
.container
div.row(style="margin-top: 20px")
div.row.verifier-row
div.col-sm-3
p.alert.alert-success(style="padding: 5px")
p.alert.alert-success
| Passed: #{view.passed}
div.col-sm-3
p.alert.alert-warning(style="padding: 5px")
p.alert.alert-warning
| Test Problem: #{view.problem}
div.col-sm-3
p.alert.alert-danger(style="padding: 5px")
p.alert.alert-danger
| Failed: #{view.failed}
div.col-sm-3
p.alert.alert-info(style="padding: 5px")
p.alert.alert-info
| To Run: #{view.testCount - view.passed - view.problem - view.failed}
if view.levelIDs
if view.levelsByCampaign
.form.form-inline
.row
each campaignInfo, campaign in view.levelsByCampaign
.form-group.campaign-mix
- var campaignID = "campaign-" + campaign + "-checkbox";
input(id=campaignID, type="checkbox", checked=campaignInfo.checked, disabled=!!view.tests)
label(for=campaignID)= campaign + ': ' + campaignInfo.levels.length
.row
each codeLanguage in view.codeLanguages
.form-group.code-language-mix
- var codeLanguageID = "code-language-" + codeLanguage.id + "-checkbox";
input(id=codeLanguageID, type="checkbox", checked=codeLanguage.checked, disabled=!!view.tests)
label(for=codeLanguageID)= codeLanguage.id
.pull-right
button.btn.btn-primary#go-button(disabled=!!view.tests) Start Tests
if view.levelsToLoad && !view.tests
.progress
.progress-bar.progress-bar-success(role="progressbar" style="width: #{100*view.passed/view.testCount}%")
.progress-bar.progress-bar-warning(role="progressbar" style="width: #{100*view.problem/view.testCount}%")
.progress-bar.progress-bar-danger(role="progressbar" style="width: #{100*view.failed/view.testCount}%")
.progress-bar.progress-bar-success(role="progressbar" style="width: #{100*(1 - view.levelsToLoad/view.initialLevelsToLoad)}%")
if view.tests
if view.levelIDs
.progress
.progress-bar.progress-bar-success(role="progressbar" style="width: #{100*view.passed/view.testCount}%")
.progress-bar.progress-bar-warning(role="progressbar" style="width: #{100*view.problem/view.testCount}%")
.progress-bar.progress-bar-danger(role="progressbar" style="width: #{100*view.failed/view.testCount}%")
each test, id in view.tests
- if (test.state == 'no-solution')
- continue;
if test.level
.pull-right
- var last = test.level.get('slug') + view.linksQueryString
a.btn.btn-primary(href="/editor/verifier/" + last) Focus
a.btn.btn-success(href="/play/level/" + last) Play
a.btn.btn-warning(href="/editor/level/" + last) Edit
a.btn.btn-default(data-target='#verifier-test-' + id, data-toggle="collapse") Toggle
each test, id in view.tests
- if (test.state == 'no-solution')
- continue;
if test.level
.pull-right
- var last = test.level.get('slug') + view.linksQueryString
a.btn.btn-primary(href="/editor/verifier/" + last) Focus
a.btn.btn-success(href="/play/level/" + last) Play
a.btn.btn-warning(href="/editor/level/" + last) Edit
a.btn.btn-default(data-target='#verifier-test-' + id, data-toggle="collapse") Toggle
if !test.goals
h2(style='color: orange')= test.level.get('name')
small= ' in ' + test.language + ''
else if test.isSuccessful()
h2(style='color: green')= test.level.get('name')
small= ' in ' + test.language + ''
else
h2(style='color: red')= test.level.get('name')
small= ' in ' + test.language + ''
if !test.goals
h2.test-running= test.level.get('name')
small= ' in ' + test.language + ''
else if test.isSuccessful()
h2.test-success= test.level.get('name')
small= ' in ' + test.language + ''
else
h2.test-failed= test.level.get('name')
small= ' in ' + test.language + ''
div.row(class=(test.isSuccessful() && id > 1 ? 'collapse' : 'collapse in'), id='verifier-test-' + id)
div.col-xs-8
if test.solution
pre #{test.solution.source}
else
h4 Error Loading Test
pre #{test.error}
div.col-xs-4.well
if test.goals
if test.frames == test.solution.frameCount
div(style='color: green') ✓ Frames: #{test.frames}
div.row(class=(test.isSuccessful() && id > 1 ? 'collapse' : 'collapse in'), id='verifier-test-' + id)
div.col-xs-8
if test.solution
pre #{test.solution.source}
else
div(style='color: red') ✘ Frames: #{test.frames} vs #{test.solution.frameCount}
each v,k in test.goals || []
if !test.solution.goals
div(style='color: orange') ? #{k} (#{v.status})
else if v.status == test.solution.goals[k]
div(style='color: green') ✓ #{k} (#{v.status})
h4 Error Loading Test
pre #{test.error}
div.col-xs-4.well
if test.goals
if test.frames == test.solution.frameCount
div.test-success ✓ Frames: #{test.frames}
else
div(style='color: red') ✘ #{k} (#{v.status} vs #{test.solution.goals[k]})
else
h3 Pending....
div.test-failed ✘ Frames: #{test.frames} vs #{test.solution.frameCount}
if test.error
pre(style="color: red") #{test.error}
if test.userCodeProblems.length
h4(style="color: red") User Code Problems
pre(style="color: red") #{JSON.stringify(test.userCodeProblems, null, 2)}
each v,k in test.goals || []
if !test.solution.goals
div.test-running ? #{k} (#{v.status})
else if v.status == test.solution.goals[k]
div.test-success ✓ #{k} (#{v.status})
else
div.test-failed ✘ #{k} (#{v.status} vs #{test.solution.goals[k]})
else
h3 Pending....
else
h1 Loading Level...
if test.error
pre.test-faile #{test.error}
// TODO: show last frame hash
if test.userCodeProblems.length
h4.test-failed User Code Problems
pre.test-failed #{JSON.stringify(test.userCodeProblems, null, 2)}
else
h1 Loading Level...
// TODO: show last frame hash

View file

@ -1,5 +1,5 @@
.modal-header
#close-modal.well.well-sm.well-parchment(data-dismiss="modal")
#close-modal.btn.well.well-sm.well-parchment(data-dismiss="modal")
span.glyphicon.glyphicon-remove
.well.well-sm.well-parchment
h1(data-i18n="play_level.victory_new_item")

View file

@ -1,5 +1,5 @@
.modal-header
#close-modal.well.well-sm.well-parchment(data-dismiss="modal")
#close-modal.btn.well.well-sm.well-parchment(data-dismiss="modal")
span.glyphicon.glyphicon-remove
.well.well-sm.well-parchment
h1(data-i18n='play_level.level_complete')
@ -36,9 +36,9 @@
h3.text-uppercase
span(data-i18n='play_level.next_level')
span :
h2.text-uppercase= i18n(view.nextLevel.attributes, 'name')
h2.text-uppercase= i18n(view.nextLevel.attributes, 'name').replace('Course: ', '')
p= i18n(view.nextLevel.attributes, 'description')
div!= view.nextLevelDescription
.row
.col-sm-5.col-sm-offset-2

View file

@ -26,14 +26,6 @@
h3(data-i18n="options.editor_config_title") Editor Configuration
.form-group.select-group
label.control-label(for="option-key-bindings", data-i18n="options.editor_config_keybindings_label") Key Bindings
select#option-key-bindings.form-control(name="keyBindings")
option(value="default", selected=(aceConfig.keyBindings === "default"), data-i18n="options.editor_config_keybindings_default") Default (Ace)
option(value="vim", selected=(aceConfig.keyBindings === "vim")) Vim
option(value="emacs", selected=(aceConfig.keyBindings === "emacs")) Emacs
span.help-block(data-i18n="options.editor_config_keybindings_description") Adds additional shortcuts known from the common editors.
.form-group.checkbox
label(for="option-live-completion")
input#option-live-completion(name="liveCompletion", type="checkbox", checked=aceConfig.liveCompletion)

View file

@ -8,12 +8,7 @@ module.exports = class AboutView extends RootView
logoutRedirectURL: false
events:
'click #mission-link': 'onClickMissionLink'
'click #team-link': 'onClickTeamLink'
'click #community-link': 'onClickCommunityLink'
'click #story-link': 'onClickStoryLink'
'click #jobs-link': 'onClickJobsLink'
'click #contact-link': 'onClickContactLink'
'click #fixed-nav a': 'onClickFixedNavLink'
'click .screen-thumbnail': 'onClickScreenThumbnail'
'click #carousel-left': 'onLeftPressed'
'click #carousel-right': 'onRightPressed'
@ -44,29 +39,21 @@ module.exports = class AboutView extends RootView
keyboard: false
})
onClickMissionLink: (event) ->
event.preventDefault()
@scrollToLink('#mission')
afterInsert: ->
# scroll to the current hash, once everything in the browser is set up
f = =>
return if @destroyed
link = $(document.location.hash)
if link.length
@scrollToLink(document.location.hash, 0)
_.delay(f, 100)
onClickTeamLink: (event) ->
event.preventDefault()
@scrollToLink('#team')
onClickCommunityLink: (event) ->
event.preventDefault()
@scrollToLink('#community')
onClickStoryLink: (event) ->
event.preventDefault()
@scrollToLink('#story')
onClickJobsLink: (event) ->
event.preventDefault()
@scrollToLink('#jobs')
onClickContactLink: (event) ->
event.preventDefault()
@scrollToLink('#contact')
onClickFixedNavLink: (event) ->
event.preventDefault() # prevent default page scroll
link = $(event.target).closest('a')
target = link.attr('href')
history.replaceState(null, null, "about#{target}") # update hash without triggering page scroll
@scrollToLink(target)
onRightPressed: (event) ->
# Special handling, otherwise after you click the control, keyboard presses move the slide twice

View file

@ -28,6 +28,10 @@ module.exports = class AuthModal extends ModalView
initialize: (options={}) ->
@previousFormInputs = options.initialValues or {}
# TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render
application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-login-btn').attr('disabled', false) })
application.facebookHandler.loadAPI({ success: => _.defer => @$('#facebook-login-btn').attr('disabled', false) })
getRenderData: ->
c = super()
c.showRequiredError = @options.showRequiredError
@ -55,7 +59,7 @@ module.exports = class AuthModal extends ModalView
e.preventDefault()
forms.clearFormAlerts(@$el)
userObject = forms.formToObject @$el
res = tv4.validateMultiple userObject, User.schema
res = tv4.validateMultiple userObject, formSchema
return forms.applyErrorsToForm(@$el, res.errors) unless res.valid
@enableModalInProgress(@$el) # TODO: part of forms
loginUser userObject, null, window.nextURL
@ -68,28 +72,22 @@ module.exports = class AuthModal extends ModalView
onClickGPlusLoginButton: ->
btn = @$('#gplus-login-btn')
btn.attr('disabled', true)
application.gplusHandler.loadAPI({
application.gplusHandler.connect({
context: @
success: ->
btn.attr('disabled', false)
application.gplusHandler.connect({
btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in'))
btn.attr('disabled', true)
application.gplusHandler.loadPerson({
context: @
success: ->
btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in'))
btn.attr('disabled', true)
application.gplusHandler.loadPerson({
context: @
success: (gplusAttrs) ->
existingUser = new User()
existingUser.fetchGPlusUser(gplusAttrs.gplusID, {
success: =>
me.loginGPlusUser(gplusAttrs.gplusID, {
success: -> window.location.reload()
error: @onGPlusLoginError
})
success: (gplusAttrs) ->
existingUser = new User()
existingUser.fetchGPlusUser(gplusAttrs.gplusID, {
success: =>
me.loginGPlusUser(gplusAttrs.gplusID, {
success: -> window.location.reload()
error: @onGPlusLoginError
})
error: @onGPlusLoginError
})
})
})
@ -105,28 +103,22 @@ module.exports = class AuthModal extends ModalView
onClickFacebookLoginButton: ->
btn = @$('#facebook-login-btn')
btn.attr('disabled', true)
application.facebookHandler.loadAPI({
application.facebookHandler.connect({
context: @
success: ->
btn.attr('disabled', false)
application.facebookHandler.connect({
btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in'))
btn.attr('disabled', true)
application.facebookHandler.loadPerson({
context: @
success: ->
btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in'))
btn.attr('disabled', true)
application.facebookHandler.loadPerson({
context: @
success: (facebookAttrs) ->
existingUser = new User()
existingUser.fetchFacebookUser(facebookAttrs.facebookID, {
success: =>
me.loginFacebookUser(facebookAttrs.facebookID, {
success: -> window.location.reload()
error: @onFacebookLoginError
})
success: (facebookAttrs) ->
existingUser = new User()
existingUser.fetchFacebookUser(facebookAttrs.facebookID, {
success: =>
me.loginFacebookUser(facebookAttrs.facebookID, {
success: -> window.location.reload()
error: @onFacebookLoginError
})
error: @onFacebookLoginError
})
})
})
@ -141,3 +133,9 @@ module.exports = class AuthModal extends ModalView
onHidden: ->
super()
@playSound 'game-menu-close'
formSchema = {
type: 'object'
properties: _.pick(User.schema.properties, 'email', 'password')
required: ['email', 'password']
}

View file

@ -29,6 +29,10 @@ module.exports = class CreateAccountModal extends ModalView
initialize: (options={}) ->
@onNameChange = _.debounce(_.bind(@checkNameExists, @), 500)
@previousFormInputs = options.initialValues or {}
# TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render
application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-signup-btn').attr('disabled', false) })
application.facebookHandler.loadAPI({ success: => _.defer => @$('#facebook-signup-btn').attr('disabled', false) })
afterRender: ->
super()
@ -155,32 +159,26 @@ module.exports = class CreateAccountModal extends ModalView
onClickGPlusSignupButton: ->
btn = @$('#gplus-signup-btn')
btn.attr('disabled', true)
application.gplusHandler.loadAPI({
application.gplusHandler.connect({
context: @
success: ->
btn.attr('disabled', false)
application.gplusHandler.connect({
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
btn.attr('disabled', true)
application.gplusHandler.loadPerson({
context: @
success: ->
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
btn.attr('disabled', true)
application.gplusHandler.loadPerson({
success: (@gplusAttrs) ->
existingUser = new User()
existingUser.fetchGPlusUser(@gplusAttrs.gplusID, {
context: @
success: (@gplusAttrs) ->
existingUser = new User()
existingUser.fetchGPlusUser(@gplusAttrs.gplusID, {
context: @
complete: ->
@$('#email-password-row').remove()
success: =>
@$('#gplus-account-exists-row').removeClass('hide')
error: (user, jqxhr) =>
if jqxhr.status is 404
@$('#gplus-logged-in-row').toggleClass('hide')
else
errors.showNotyNetworkError(jqxhr)
})
complete: ->
@$('#email-password-row').remove()
success: =>
@$('#gplus-account-exists-row').removeClass('hide')
error: (user, jqxhr) =>
if jqxhr.status is 404
@$('#gplus-logged-in-row').toggleClass('hide')
else
errors.showNotyNetworkError(jqxhr)
})
})
})
@ -201,32 +199,26 @@ module.exports = class CreateAccountModal extends ModalView
onClickFacebookSignupButton: ->
btn = @$('#facebook-signup-btn')
btn.attr('disabled', true)
application.facebookHandler.loadAPI({
application.facebookHandler.connect({
context: @
success: ->
btn.attr('disabled', false)
application.facebookHandler.connect({
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
btn.attr('disabled', true)
application.facebookHandler.loadPerson({
context: @
success: ->
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
btn.attr('disabled', true)
application.facebookHandler.loadPerson({
success: (@facebookAttrs) ->
existingUser = new User()
existingUser.fetchFacebookUser(@facebookAttrs.facebookID, {
context: @
success: (@facebookAttrs) ->
existingUser = new User()
existingUser.fetchFacebookUser(@facebookAttrs.facebookID, {
context: @
complete: ->
@$('#email-password-row').remove()
success: =>
@$('#facebook-account-exists-row').removeClass('hide')
error: (user, jqxhr) =>
if jqxhr.status is 404
@$('#facebook-logged-in-row').toggleClass('hide')
else
errors.showNotyNetworkError(jqxhr)
})
complete: ->
@$('#email-password-row').remove()
success: =>
@$('#facebook-account-exists-row').removeClass('hide')
error: (user, jqxhr) =>
if jqxhr.status is 404
@$('#facebook-logged-in-row').toggleClass('hide')
else
errors.showNotyNetworkError(jqxhr)
})
})
})

View file

@ -20,17 +20,12 @@ module.exports = class InviteToClassroomModal extends ModalView
emails = _.filter((_.string.trim(email) for email in emails))
if not emails.length
return
url = @classroom.url() + '/invite-members'
@$('#send-invites-btn, #invite-emails-textarea').addClass('hide')
@$('#invite-emails-sending-alert').removeClass('hide')
application.tracker?.trackEvent 'Classroom invite via email', category: 'Courses', classroomID: @classroom.id, emails: emails
$.ajax({
url: url
data: {emails: emails}
method: 'POST'
context: @
success: ->
@classroom.inviteMembers(emails, {
success: =>
@$('#invite-emails-sending-alert').addClass('hide')
@$('#invite-emails-success-alert').removeClass('hide')
})

View file

@ -127,7 +127,6 @@ module.exports = class TeacherClassView extends RootView
@render()
# Model/Collection events
@listenTo @classroom, 'sync change update', ->
@removeDeletedStudents()
classCode = @classroom.get('codeCamel') or @classroom.get('code')
@state.set {
classCode: classCode
@ -144,7 +143,6 @@ module.exports = class TeacherClassView extends RootView
@listenTo @students, 'sync change update add remove reset', ->
# Set state/props of things that depend on students?
# Set specific parts of state based on the models, rather than just dumping the collection there?
@removeDeletedStudents()
@calculateProgressAndLevels()
classStats = @calculateClassStats()
@state.set classStats: classStats if classStats

View file

@ -60,6 +60,14 @@ module.exports = class CampaignEditorView extends RootView
@listenToOnce @levels, 'sync', @onFundamentalLoaded
@listenToOnce @achievements, 'sync', @onFundamentalLoaded
onLeaveMessage: ->
@propagateCampaignIndexes()
for model in @toSave.models
diff = model.getDelta()
if _.size(diff)
console.log 'model, diff', model, diff
return 'You have changes!'
loadThangTypeNames: ->
# Load the names of the ThangTypes that this level's Treema nodes might want to display.
originals = []
@ -143,6 +151,19 @@ module.exports = class CampaignEditorView extends RootView
@updateRewardsForLevel model, level.rewards
super()
propagateCampaignIndexes: ->
campaignLevels = $.extend({}, @campaign.get('levels'))
index = 0
for levelOriginal, campaignLevel of campaignLevels
level = @levels.findWhere({original: levelOriginal})
if level.get('campaignIndex') isnt index
level.set('campaignIndex', index)
campaignLevel.campaignIndex = index
index += 1
@campaign.set('levels', campaignLevels)
onClickPatches: (e) ->
@patchesView = @insertSubView(new PatchesView(@campaign), @$el.find('.patches-view'))
@ -160,6 +181,7 @@ module.exports = class CampaignEditorView extends RootView
break
onClickSaveButton: ->
@propagateCampaignIndexes()
@toSave.set @toSave.filter (m) -> m.hasLocalChanges()
@openModalView new SaveCampaignModal({}, @toSave)

View file

@ -90,6 +90,8 @@ module.exports = class ThangsTabView extends CocoView
getRenderData: (context={}) ->
context = super(context)
return context unless @supermodel.finished()
for thangType in @thangTypes.models
thangType.notInLevel = true
thangTypes = (thangType.attributes for thangType in @supermodel.getModels(ThangType))
thangTypes = _.uniq thangTypes, false, 'original'
thangTypes = _.reject thangTypes, (tt) -> tt.kind in ['Mark', undefined]

View file

@ -42,15 +42,8 @@ module.exports = class VerifierTest extends CocoClass
@register()
configureSession: (session, level) =>
# TODO: reach into and find hero and get the config from the solution
try
hero = _.find level.get("thangs"), id: "Hero Placeholder"
config = _.find(hero.components, (x) -> x.config?.programmableMethods?.plan).config
programmable = config.programmableMethods.plan
solution = _.find (programmable.solutions ? []), language: session.get('codeLanguage')
solution.source = _.template(solution.source)(config?.programmableMethods?.plan.context)
session.solution = solution
session.solution = _.find level.getSolutions(), language: session.get('codeLanguage')
session.set 'heroConfig', session.solution.heroConfig
session.set 'code', {'hero-placeholder': plan: session.solution.source}
state = session.get 'state'

View file

@ -4,66 +4,102 @@ RootView = require 'views/core/RootView'
template = require 'templates/editor/verifier/verifier-view'
VerifierTest = require './VerifierTest'
SuperModel = require 'models/SuperModel'
Campaigns = require 'collections/Campaigns'
Level = require 'models/Level'
module.exports = class VerifierView extends RootView
className: 'style-flat'
template: template
id: 'verifier-view'
events:
'click #go-button': 'onClickGoButton'
constructor: (options, @levelID) ->
super options
# TODO: sort tests by unexpected result first
@passed = 0
@failed = 0
@problem = 0
@testCount = 0
testLevels = [
'dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'kounter-kithwise', 'crawlways-of-kithgard',
'enemy-mine', 'illusory-interruption', 'forgetful-gemsmith', 'signs-and-portents', 'favorable-odds',
'true-names', 'the-prisoner', 'banefire', 'the-raised-sword', 'kithgard-librarian', 'fire-dancing',
'loop-da-loop', 'haunted-kithmaze', 'riddling-kithmaze', 'descending-further', 'the-second-kithmaze',
'dread-door', 'cupboards-of-kithgard', 'hack-and-dash', 'known-enemy', 'master-of-names', 'lowly-kithmen',
'closing-the-distance', 'tactical-strike', 'the-skeleton', 'a-mayhem-of-munchkins', 'the-final-kithmaze',
'the-gauntlet', 'radiant-aura', 'kithgard-gates', 'destroying-angel', 'deadly-dungeon-rescue',
'breakout', 'attack-wisely', 'kithgard-mastery', 'kithgard-apprentice', 'robot-ragnarok',
'defense-of-plainswood', 'peasant-protection', 'forest-fire-dancing', 'course-winding-trail',
'patrol-buster', 'endangered-burl', 'thumb-biter', 'gems-or-death', 'village-guard', 'thornbush-farm',
'back-to-back', 'ogre-encampment', 'woodland-cleaver', 'shield-rush', 'range-finder', 'munchkin-swarm',
'stillness-in-motion', 'the-agrippa-defense', 'backwoods-bombardier', 'coinucopia', 'copper-meadows',
'drop-the-flag', 'mind-the-trap', 'signal-corpse', 'rich-forager',
if @levelID
@levelIDs = [@levelID]
@testLanguages = ['python', 'javascript', 'java', 'lua', 'coffeescript']
@startTestingLevels()
else
@campaigns = new Campaigns()
@supermodel.trackRequest @campaigns.fetch(data: {project: 'slug,type,levels'})
@campaigns.comparator = (m) ->
['intro', 'course-2', 'course-3', 'course-4', 'course-5', 'course-6', 'course-8',
'dungeon', 'forest', 'desert', 'mountain', 'glacier', 'volcano'].indexOf(m.get('slug'))
'the-mighty-sand-yak', 'oasis', 'sarven-road', 'sarven-gaps', 'thunderhooves', 'minesweeper',
'medical-attention', 'sarven-sentry', 'keeping-time', 'hoarding-gold', 'decoy-drill', 'continuous-alchemy',
'dust', 'desert-combat', 'sarven-savior', 'lurkers', 'preferential-treatment', 'sarven-shepherd',
'shine-getter',
onLoaded: ->
super()
return if @levelID
@filterCampaigns()
@filterCodeLanguages()
@render()
'a-fine-mint', 'borrowed-sword', 'cloudrip-commander', 'crag-tag',
'hunters-and-prey', 'hunting-party',
'leave-it-to-cleaver', 'library-tactician', 'mad-maxer', 'mad-maxer-strikes-back',
'mirage-maker', 'mixed-unit-tactics', 'mountain-mercenaries',
'noble-sacrifice', 'odd-sandstorm', 'ogre-gorge-gouger', 'reaping-fire',
'return-to-thornbush-farm', 'ring-bearer', 'sand-snakes',
'slalom', 'steelclaw-gap', 'the-geometry-of-flowers',
'the-two-flowers', 'timber-guard', 'toil-and-trouble', 'village-rover',
'vital-powers', 'zoo-keeper',
]
filterCampaigns: ->
@levelsByCampaign = {}
for campaign in @campaigns.models when campaign.get('type') in ['course', 'hero'] and campaign.get('slug') isnt 'picoctf'
@levelsByCampaign[campaign.get('slug')] ?= {levels: [], checked: true}
campaignInfo = @levelsByCampaign[campaign.get('slug')]
for levelID, level of campaign.get('levels') when level.type not in ['hero-ladder', 'course-ladder', 'game-dev']
campaignInfo.levels.push level.slug
filterCodeLanguages: ->
defaultLanguages = utils.getQueryVariable('languages', 'python,javascript').split(/, ?/)
@codeLanguages ?= ({id: c, checked: c in defaultLanguages} for c in ['python', 'javascript', 'java', 'lua', 'coffeescript'])
onClickGoButton: (e) ->
@filterCampaigns()
@levelIDs = []
for campaign, campaignInfo of @levelsByCampaign
if @$("#campaign-#{campaign}-checkbox").is(':checked')
for level in campaignInfo.levels
@levelIDs.push level unless level in @levelIDs
else
campaignInfo.checked = false
@testLanguages = []
for codeLanguage in @codeLanguages
if @$("#code-language-#{codeLanguage.id}-checkbox").is(':checked')
codeLanguage.checked = true
@testLanguages.push codeLanguage.id
else
codeLanguage.checked = false
@startTestingLevels()
startTestingLevels: ->
@levelsToLoad = @initialLevelsToLoad = @levelIDs.length
for levelID in @levelIDs
level = @supermodel.getModel(Level, levelID) or new Level _id: levelID
if level.loaded
@onLevelLoaded()
else
@listenToOnce @supermodel.loadModel(level).model, 'sync', @onLevelLoaded
onLevelLoaded: (level) ->
if --@levelsToLoad is 0
@onTestLevelsLoaded()
else
@render()
onTestLevelsLoaded: ->
defaultCores = 2
cores = Math.max(window.navigator.hardwareConcurrency, defaultCores)
#testLevels = testLevels.slice 0, 15
@linksQueryString = window.location.search
@levelIDs = if @levelID then [@levelID] else testLevels
languages = utils.getQueryVariable 'languages', 'python,javascript'
#supermodel = if @levelID then @supermodel else undefined
@tests = []
@taskList = []
@tasksList = _.flatten _.map @levelIDs, (v) ->
# TODO: offer good interface for choosing which languages, better performance for skipping missing solutions
#_.map ['python', 'javascript', 'coffeescript', 'lua'], (l) ->
_.map languages.split(','), (l) ->
#_.map ['javascript'], (l) ->
level: v, language: l
@tasksList = []
for levelID in @levelIDs
level = @supermodel.getModel(Level, levelID)
solutions = level?.getSolutions()
for codeLanguage in @testLanguages
if not solutions or _.find(solutions, language: codeLanguage)
@tasksList.push level: levelID, language: codeLanguage
@testCount = @tasksList.length
chunks = _.groupBy @tasksList, (v,i) -> i%cores

View file

@ -15,15 +15,16 @@ module.exports = class MainLadderView extends RootView
id: 'main-ladder-view'
template: template
constructor: (options) ->
super options
@levelStatusMap = {}
@levelPlayCountMap = {}
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', {cache: false}, 0).model
@listenToOnce @sessions, 'sync', @onSessionsLoaded
@getLevelPlayCounts()
initialize: ->
@levelStatusMap = []
@levelPlayCountMap = []
@campaigns = campaigns
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', {cache: false}, 0).model
@listenToOnce @sessions, 'sync', @onSessionsLoaded
@getLevelPlayCounts()
onSessionsLoaded: (e) ->
for session in @sessions.models
@levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'

View file

@ -80,7 +80,6 @@ module.exports = class CampaignView extends RootView
return
@campaign = new Campaign({_id:@terrain})
@campaign.saveBackups = @editorMode
@campaign = @supermodel.loadModel(@campaign).model
# Temporary attempt to make sure all earned rewards are accounted for. Figure out a better solution...

View file

@ -641,7 +641,7 @@ module.exports = class PlayLevelView extends RootView
return unless @$el.hasClass 'real-time'
@$el.removeClass 'real-time'
@onWindowResize()
if @world.frames.length is @world.totalFrames
if @world.frames.length is @world.totalFrames and not @surface.countdownScreen?.showing
_.delay @onSubmissionComplete, 750 # Wait for transition to end.
else
@waitingForSubmissionComplete = true
@ -649,6 +649,7 @@ module.exports = class PlayLevelView extends RootView
onSubmissionComplete: =>
return if @destroyed
Backbone.Mediator.publish 'level:set-time', ratio: 1
return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor
# TODO: Show a victory dialog specific to hero-ladder level
if @goalManager.checkOverallStatus() is 'success' and not @options.realTimeMultiplayerSessionID?

View file

@ -292,7 +292,7 @@ module.exports = class HeroVictoryModal extends ModalView
duration = 1000
ratio = @getEaseRatio (new Date() - @sequentialAnimationStart), duration
if panel.unit is 'xp'
newXP = Math.floor(panel.previousNumber + ratio * (panel.number - panel.previousNumber))
newXP = Math.floor(ratio * (panel.number - panel.previousNumber))
totalXP = @totalXPAnimated + newXP
if totalXP isnt @lastTotalXP
panel.textEl.text('+' + newXP)
@ -304,7 +304,7 @@ module.exports = class HeroVictoryModal extends ModalView
@XPEl.addClass 'five-digits' if totalXP >= 10000 and @lastTotalXP < 10000
@lastTotalXP = totalXP
else if panel.unit is 'gem'
newGems = Math.floor(panel.previousNumber + ratio * (panel.number - panel.previousNumber))
newGems = Math.floor(ratio * (panel.number - panel.previousNumber))
totalGems = @totalGemsAnimated + newGems
if totalGems isnt @lastTotalGems
panel.textEl.text('+' + newGems)
@ -326,9 +326,9 @@ module.exports = class HeroVictoryModal extends ModalView
panel.rootEl.removeClass('animating').find('.reward-image-container img').removeClass('pulse')
@sequentialAnimationStart = new Date()
if panel.unit is 'xp'
@totalXPAnimated += panel.number
@totalXPAnimated += panel.number - panel.previousNumber
else if panel.unit is 'gem'
@totalGemsAnimated += panel.number
@totalGemsAnimated += panel.number - panel.previousNumber
@sequentialAnimatedPanels.shift()
return
panel.rootEl.addClass('animating').find('.reward-image-container').removeClass('pending-reward-image').find('img').addClass('pulse')

View file

@ -1,7 +1,8 @@
CocoView = require 'views/core/CocoView'
utils = require 'core/utils'
module.exports = class ProgressView extends CocoView
id: 'progress-view'
className: 'modal-content'
template: require 'templates/play/level/modal/progress-view'
@ -16,9 +17,13 @@ module.exports = class ProgressView extends CocoView
@classroom = options.classroom
@nextLevel = options.nextLevel
@levelSessions = options.levelSessions
# Translate and Markdownify level description, but take out any images (we don't have room for arena banners, etc.).
# Images in Markdown are like ![description](url)
@nextLevel.get('description', true) # Make sure the defaults are available
@nextLevelDescription = marked(utils.i18n(@nextLevel.attributesWithDefaults, 'description').replace(/!\[.*?\]\(.*?\)\n*/g, ''))
onClickDoneButton: ->
@trigger 'done'
onClickNextLevelButton: ->
@trigger 'next-level'
@trigger 'next-level'

View file

@ -692,6 +692,8 @@ module.exports = class SpellView extends CocoView
@aceDoc.insertNewLine row: lineCount, column: 0 #lastLine.length
@ace.navigateLeft(1) if wasAtEnd
++lineCount
# Force the popup back
@ace?.completer?.showPopup(@ace)
screenLineCount = @aceSession.getScreenLength()
if screenLineCount isnt @lastScreenLineCount
@lastScreenLineCount = screenLineCount

View file

@ -69,7 +69,7 @@ module.exports = class LevelGuideView extends CocoView
if @helpVideos.length
startingTab = 0
else
startingTab = _.findIndex @docs, slug: 'intro'
startingTab = _.findIndex @docs, slug: 'overview'
startingTab = 0 if startingTab is -1
# incredible hackiness. Getting bootstrap tabs to work shouldn't be this complex
@$el.find(".nav-tabs li:nth(#{startingTab})").addClass('active')

View file

@ -20,8 +20,7 @@ module.exports = class OptionsView extends CocoView
events:
'change #option-music': 'updateMusic'
'change #option-key-bindings': 'updateInvisibles'
'change #option-key-bindings': 'updateKeyBindings'
'change #option-invisibles': 'updateInvisibles'
'change #option-indent-guides': 'updateIndentGuides'
'change #option-behaviors': 'updateBehaviors'
'change #option-live-completion': 'updateLiveCompletion'
@ -67,7 +66,7 @@ module.exports = class OptionsView extends CocoView
if @playerName and @playerName isnt me.get('name')
me.set 'name', @playerName
@aceConfig.invisibles = @$el.find('#option-invisibles').prop('checked')
@aceConfig.keyBindings = @$el.find('#option-key-bindings').val()
@aceConfig.keyBindings = 'default' # We used to give them the option, but we took it away.
@aceConfig.indentGuides = @$el.find('#option-indent-guides').prop('checked')
@aceConfig.behaviors = @$el.find('#option-behaviors').prop('checked')
@aceConfig.liveCompletion = @$el.find('#option-live-completion').prop('checked')

View file

@ -51,7 +51,7 @@
"lscache": "~1.0.5",
"esper.js": "http://files.codecombat.com/esper.tar.gz",
"algoliasearch": "^3.13.1",
"algolia-autocomplete.js": "^1.17.0",
"algolia-autocomplete.js": "^0.17.0",
"algolia-autocomplete-no-conflict": "1.0.0"
},
"overrides": {

View file

@ -0,0 +1,43 @@
// Usage: Copy and paste into mongo shell
function removeAnonymousMembers(classroom) {
if(!classroom.members) {
return;
}
print('checking classroom',
classroom._id,
'\n\t',
classroom._id.getTimestamp(),
classroom.members.length,
'owner', classroom.ownerID);
classroom.members.forEach(function(userID) {
var user = db.users.findOne({_id: userID}, {anonymous:1});
if (!user) {
return;
}
if(user.anonymous) {
print('\tRemove user', JSON.stringify(user));
print('\t\tRemoving from course instances',
db.course.instances.update(
{classroomID: classroom._id},
{$pull: {members: userID}})
);
print('\t\tRemoving from classroom',
db.classrooms.update(
{_id: classroom._id},
{$pull: {members: userID}})
);
}
});
}
var startID = ObjectId('566838b00fb44a2e00000000');
while (true) {
var classroom = db.classrooms.findOne({_id: {$gt: startID}});
removeAnonymousMembers(classroom);
startID = classroom._id;
}

View file

@ -95,7 +95,6 @@ ClanHandler = class ClanHandler extends Handler
AnalyticsLogEvent.logEvent req.user, 'Clan left', clanID: clanID, type: clan.get('type')
getMemberAchievements: (req, res, clanID) ->
# TODO: add tests
Clan.findById clanID, (err, clan) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless clan
@ -111,18 +110,16 @@ ClanHandler = class ClanHandler extends Handler
@sendSuccess(res, cleandocs)
getMembers: (req, res, clanID) ->
# TODO: add tests
Clan.findById clanID, (err, clan) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless clan
memberIDs = _.map clan.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID
User.find {_id: {$in: memberIDs}}, 'name nameLower points heroConfig.thangType', {}, (err, users) =>
User.find {_id: {$in: memberIDs}}, 'name nameLower points heroConfig.thangType', {limit: memberLimit}, (err, users) =>
return @sendDatabaseError(res, err) if err
cleandocs = (UserHandler.formatEntity(req, doc) for doc in users)
@sendSuccess(res, cleandocs)
getMemberSessions: (req, res, clanID) ->
# TODO: add tests
# TODO: restrict information returned based on clan type
Clan.findById clanID, (err, clan) =>
return @sendDatabaseError(res, err) if err

View file

@ -28,8 +28,6 @@ ClassroomHandler = class ClassroomHandler extends Handler
false
getByRelationship: (req, res, args...) ->
method = req.method.toLowerCase()
return @inviteStudents(req, res, args[0]) if args[1] is 'invite-members'
return @removeMember(req, res, args[0]) if req.method is 'DELETE' and args[1] is 'members'
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
super(arguments...)
@ -68,32 +66,6 @@ ClassroomHandler = class ClassroomHandler extends Handler
return doc.toObject()
return _.omit(doc.toObject(), 'code', 'codeCamel')
inviteStudents: (req, res, classroomID) ->
return @sendUnauthorizedError(res) if not req.user?
if not req.body.emails
return @sendBadInputError(res, 'Emails not included')
Classroom.findById classroomID, (err, classroom) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless classroom
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
joinCode = (classroom.get('codeCamel') or classroom.get('code'))
context =
email_id: sendwithus.templates.course_invite_email
recipient:
address: email
email_data:
teacher_name: req.user.broadName()
class_name: classroom.get('name')
join_link: "https://codecombat.com/courses?_cc=" + joinCode
join_code: joinCode
sendwithus.api.send context, _.noop
return @sendSuccess(res, {})
get: (req, res) ->
if ownerID = req.query.ownerID
unless req.user and (req.user.isAdmin() or ownerID is req.user.id)

View file

@ -433,9 +433,9 @@ class SubscriptionHandler extends Handler
productName = "#{user.get('country')}_basic_subscription"
Product.findOne({name: productName}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
return done({res: 'Database error.', code: 500}) if err
return done({res: 'basic_subscription product not found.', code: 404}) if not product
if increment
purchased = _.clone(user.get('purchased'))
purchased ?= {}

View file

@ -686,8 +686,8 @@ UserHandler = class UserHandler extends Handler
buildGravatarURL: (user, size, fallback) ->
emailHash = @buildEmailHash user
fallback ?= 'http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png'
fallback = "http://codecombat.com#{fallback}" unless /^http/.test fallback
fallback ?= 'https://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png'
fallback = "https://codecombat.com#{fallback}" unless /^http/.test fallback
"https://www.gravatar.com/avatar/#{emailHash}?s=#{size}&default=#{fallback}"
buildEmailHash: (user) ->

View file

@ -79,4 +79,4 @@ module.exports =
for original, level of campaign.levels
campaign.levels[original] = _.pick level, ['locked', 'disabled', 'original', 'rewards', 'slug']
return campaign
res.status(200).send((formatCampaign(campaign) for campaign in campaigns))
res.status(200).send((formatCampaign(campaign) for campaign in campaigns))

View file

@ -15,6 +15,8 @@ parse = require '../commons/parse'
LevelSession = require '../models/LevelSession'
User = require '../models/User'
CourseInstance = require '../models/CourseInstance'
TrialRequest = require '../models/TrialRequest'
sendwithus = require '../sendwithus'
module.exports =
fetchByCode: wrap (req, res, next) ->
@ -216,3 +218,30 @@ module.exports =
throw new errors.UnprocessableEntity(error.message)
yield student.update({ $set: { passwordHash: User.hashPassword(newPassword) } })
res.status(200).send({})
inviteMembers: wrap (req, res) ->
if not req.body.emails
throw new errors.UnprocessableEntity('Emails not included')
classroom = yield database.getDocFromHandle(req, Classroom)
if not classroom
throw new errors.NotFound('Classroom not found.')
unless classroom.get('ownerID').equals(req.user?._id)
log.debug "classroom_handler.inviteStudents: Can't invite to classroom (#{classroom.id}) you (#{req.user.get('_id')}) don't own"
throw new errors.Forbidden('Must be owner of classroom to send invites.')
for email in req.body.emails
joinCode = (classroom.get('codeCamel') or classroom.get('code'))
context =
email_id: sendwithus.templates.course_invite_email
recipient:
address: email
email_data:
teacher_name: req.user.broadName()
class_name: classroom.get('name')
join_link: "https://codecombat.com/courses?_cc=" + joinCode
join_code: joinCode
sendwithus.api.send context, _.noop
res.status(200).send({})

View file

@ -1,7 +1,7 @@
mw = require '../middleware'
module.exports.setup = (app) ->
passport = require('passport')
app.post('/auth/login', passport.authenticate('local'), mw.auth.afterLogin)
app.post('/auth/login-facebook', mw.auth.loginByFacebook, mw.auth.afterLogin)
@ -14,6 +14,8 @@ module.exports.setup = (app) ->
app.get('/auth/unsubscribe', mw.auth.unsubscribe)
app.get('/auth/whoami', mw.auth.whoAmI)
app.all('/db/*', mw.auth.checkHasUser())
Achievement = require '../models/Achievement'
app.get('/db/achievement', mw.achievements.fetchByRelated, mw.rest.get(Achievement))
app.post('/db/achievement', mw.auth.checkHasPermission(['admin', 'artisan']), mw.rest.post(Achievement))
@ -28,7 +30,7 @@ module.exports.setup = (app) ->
Article = require '../models/Article'
app.get('/db/article', mw.rest.get(Article))
app.post('/db/article', mw.auth.checkHasPermission(['admin', 'artisan']), mw.rest.post(Article))
app.post('/db/article', mw.auth.checkLoggedIn(), mw.auth.checkHasPermission(['admin', 'artisan']), mw.rest.post(Article))
app.get('/db/article/names', mw.named.names(Article))
app.post('/db/article/names', mw.named.names(Article))
app.get('/db/article/:handle', mw.rest.getByHandle(Article))
@ -58,6 +60,7 @@ module.exports.setup = (app) ->
app.get('/db/classroom', mw.classrooms.fetchByCode, mw.classrooms.getByOwner)
app.get('/db/classroom/:handle/levels', mw.classrooms.fetchAllLevels)
app.get('/db/classroom/:handle/courses/:courseID/levels', mw.classrooms.fetchLevelsForCourse)
app.post('/db/classroom/:handle/invite-members', mw.classrooms.inviteMembers)
app.get('/db/classroom/:handle/member-sessions', mw.classrooms.fetchMemberSessions)
app.get('/db/classroom/:handle/members', mw.classrooms.fetchMembers) # TODO: Use mw.auth?
app.post('/db/classroom/:classroomID/members/:memberID/reset-password', mw.classrooms.setStudentPassword)
@ -65,7 +68,7 @@ module.exports.setup = (app) ->
app.get('/db/classroom/:handle', mw.auth.checkLoggedIn()) # TODO: Finish migrating route, adding now so 401 is returned
CodeLog = require ('../models/CodeLog')
app.post('/db/codelogs', mw.auth.checkHasUser(), mw.codelogs.post)
app.post('/db/codelogs', mw.codelogs.post)
app.get('/db/codelogs', mw.auth.checkHasPermission(['admin']), mw.rest.get(CodeLog))
Course = require '../models/Course'
@ -86,7 +89,7 @@ module.exports.setup = (app) ->
app.post('/db/user/:userID/request-verify-email', mw.users.sendVerificationEmail)
app.post('/db/user/:userID/verify/:verificationCode', mw.users.verifyEmailAddress) # TODO: Finalize URL scheme
app.get('/db/level/:handle/session', mw.auth.checkHasUser(), mw.levels.upsertSession)
app.get('/db/level/:handle/session', mw.levels.upsertSession)
app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator)
app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post)

View file

@ -9,7 +9,7 @@ module.exports.setupRoutes = (app) ->
debug = not config.isProduction
module.exports.api =
send: (context, cb) ->
log.debug('Tried to send email with context: ', JSON.stringify(context, null, '\t'))
log.debug('Tried to send email with context: ', JSON.stringify(context, null, ' '))
setTimeout(cb, 10)
if swuAPIKey
@ -29,7 +29,7 @@ module.exports.templates =
generic_email: 'tem_JhRnQ4pvTS4KdQjYoZdbei'
plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y'
next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D'
course_invite_email: 'tem_f5K7BXX5vQ9a7kwYTACbJa'
course_invite_email: 'tem_ic2ZhPkpj8GBADFuyAp4bj'
teacher_free_trial: 'tem_R7d9Hpoba9SceQNiYSXBak'
teacher_free_trial_hoc: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc'
teacher_request_demo: 'tem_cwG3HZjEyb6QE493hZuUra'

View file

@ -76,6 +76,18 @@ beforeEach(function(done) {
cb(err);
});
},
function(cb) {
// Initialize products
var utils = require('../server/utils');
request = require('../server/request');
utils.initUser()
.then(function (user) {
return utils.loginUser(user, {request: request})
})
.then(function () {
cb()
});
},
function(cb) {
// Initialize products
request = require('../server/request');

View file

@ -17,7 +17,7 @@ describe 'GET /db/article', ->
yield utils.loginUser(@admin)
yield request.postAsync(getURL('/db/article'), { json: articleData1 })
yield request.postAsync(getURL('/db/article'), { json: articleData2 })
yield utils.logout()
yield utils.becomeAnonymous()
done()
@ -194,7 +194,7 @@ describe 'POST /db/article', ->
it 'does not allow anonymous users to create Articles', utils.wrap (done) ->
yield utils.clearModels([Article])
yield utils.logout()
yield utils.becomeAnonymous()
[res, body] = yield request.postAsync({uri: getURL('/db/article'), json: articleData })
expect(res.statusCode).toBe(401)
done()
@ -451,7 +451,7 @@ describe 'POST /db/article/:handle/new-version', ->
it 'does not work for anonymous users', utils.wrap (done) ->
yield utils.logout()
yield utils.becomeAnonymous()
yield postNewVersion({ name: 'Article name', body: 'New body' }, 401)
articles = yield Article.find()
expect(articles.length).toBe(1)
@ -580,7 +580,7 @@ describe 'GET and POST /db/article/:handle/names', ->
yield utils.loginUser(admin)
[res, article1] = yield request.postAsync(getURL('/db/article'), { json: articleData1 })
[res, article2] = yield request.postAsync(getURL('/db/article'), { json: articleData2 })
yield utils.logout()
yield utils.becomeAnonymous()
[res, body] = yield request.getAsync { uri: getURL('/db/article/names?ids='+[article1._id, article2._id].join(',')), json: true }
expect(body.length).toBe(2)
expect(body[0].name).toBe('Article 1')
@ -679,4 +679,4 @@ describe 'DELETE /db/article/:handle/watchers', ->
article = yield Article.findById(article._id)
ids = (id.toString() for id in article.get('watchers'))
expect(_.contains(ids, user.id)).toBe(false)
done()
done()

View file

@ -1,9 +1,9 @@
config = require '../../../server_config'
require '../common'
utils = require '../../../app/core/utils' # Must come after require /common
Clan = require '../../../server/models/Clan'
User = require '../../../server/models/User'
request = require '../request'
utils = require '../utils'
describe 'Clans', ->
clanURL = getURL('/db/clan')
@ -53,7 +53,7 @@ describe 'Clans', ->
done()
it 'Anonymous create clan 401', (done) ->
logoutUser ->
utils.logout().then ->
requestBody =
type: 'public'
name: createClanName 'myclan'
@ -152,7 +152,7 @@ describe 'Clans', ->
loginNewUser (user1) ->
createClan user1, 'public', null, (clan1) ->
createClan user1, 'public', null, (clan2) ->
logoutUser ->
utils.becomeAnonymous().then ->
request.get {uri: "#{clanURL}/-/public" }, (err, res, body) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
@ -498,7 +498,7 @@ describe 'Clans', ->
user1.save (err) ->
expect(err).toBeNull()
createClan user1, 'private', 'my private clan', (clan1) ->
logoutUser ->
utils.becomeAnonymous().then ->
request.get {uri: "#{clanURL}/#{clan1.id}" }, (err, res, body) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)

View file

@ -343,18 +343,20 @@ describe 'DELETE /db/classroom/:id/members', ->
describe 'POST /db/classroom/:id/invite-members', ->
it 'takes a list of emails and sends invites', (done) ->
loginNewUser (user1) ->
user1.set('role', 'teacher')
user1.save (err) ->
data = { name: 'Classroom 6' }
request.post {uri: classroomsURL, json: data }, (err, res, body) ->
expect(res.statusCode).toBe(201)
url = classroomsURL + '/' + body._id + '/invite-members'
data = { emails: ['test@test.com'] }
request.post { uri: url, json: data }, (err, res, body) ->
expect(res.statusCode).toBe(200)
done()
it 'takes a list of emails and sends invites', utils.wrap (done) ->
user = yield utils.initUser({role: 'teacher', name: 'Mr Professerson'})
yield utils.loginUser(user)
classroom = yield utils.makeClassroom()
url = classroomsURL + "/#{classroom.id}/invite-members"
data = { emails: ['test@test.com'] }
sendwithus = require '../../../server/sendwithus'
spyOn(sendwithus.api, 'send').and.callFake (context, cb) ->
expect(context.email_id).toBe(sendwithus.templates.course_invite_email)
expect(context.recipient.address).toBe('test@test.com')
expect(context.email_data.teacher_name).toBe('Mr Professerson')
done()
[res, body] = yield request.postAsync { uri: url, json: data }
expect(res.statusCode).toBe(200)
describe 'GET /db/classroom/:handle/member-sessions', ->

View file

@ -23,6 +23,7 @@ describe 'GET /db/course', ->
yield utils.clearModels([Course, User])
yield new Course({ name: 'Course 1' }).save()
yield new Course({ name: 'Course 2' }).save()
yield utils.becomeAnonymous()
done()
@ -36,6 +37,7 @@ describe 'GET /db/course/:handle', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels([Course, User])
@course = yield new Course({ name: 'Some Name' }).save()
yield utils.becomeAnonymous()
done()

View file

@ -544,7 +544,7 @@ describe '/db/prepaid', ->
logoutUser () ->
fetchPrepaid joeCode, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(403)
expect(res.statusCode).toEqual(401)
done()
it 'User can fetch a prepaid code', (done) ->

View file

@ -39,7 +39,7 @@ describe 'POST /db/user', ->
it 'serves the user through /db/user/id', (done) ->
unittest.getNormalJoe (user) ->
request.post getURL('/auth/logout'), ->
utils.becomeAnonymous().then ->
url = getURL(urlUser+'/'+user._id)
request.get url, (err, res, body) ->
expect(res.statusCode).toBe(200)
@ -567,6 +567,12 @@ describe 'DELETE /db/user', ->
expect(classroom.get('members')[0].toString()).toEqual(user2.id)
expect(classroom.get('deletedMembers')[0].toString()).toEqual(user.id)
done()
it 'returns 401 if no cookie session', utils.wrap (done) ->
yield utils.logout()
[res, body] = yield request.delAsync {uri: "#{getURL(urlUser)}/1234"}
expect(res.statusCode).toBe(401)
done()
describe 'Statistics', ->
LevelSession = require '../../../server/models/LevelSession'

View file

@ -11,6 +11,8 @@ Prepaid = require '../../server/models/Prepaid'
Classroom = require '../../server/models/Classroom'
CourseInstance = require '../../server/models/CourseInstance'
moment = require 'moment'
Classroom = require '../../server/models/Classroom'
TrialRequest = require '../../server/models/TrialRequest'
campaignSchema = require '../../app/schemas/models/campaign.schema'
campaignLevelProperties = _.keys(campaignSchema.properties.levels.additionalProperties.properties)
campaignAdjacentCampaignProperties = _.keys(campaignSchema.properties.adjacentCampaigns.additionalProperties.properties)
@ -186,3 +188,17 @@ module.exports = mw =
expect(res.statusCode).toBe(200)
courseInstance = yield CourseInstance.findById(res.body._id)
return courseInstance
makeTrialRequest: Promise.promisify (data, sources, done) ->
args = Array.from(arguments)
[done, [data, sources]] = [args.pop(), args]
data = _.extend({}, {
type: 'course'
properties: {}
}, data)
request.post { uri: getURL('/db/trial.request'), json: data }, (err, res) ->
return done(err) if err
expect(res.statusCode).toBe(201)
TrialRequest.findById(res.body._id).exec done

View file

@ -50,11 +50,10 @@ describe 'Vector', ->
expectEquivalentMethods 'equals', new Vector 7, 7
expectEquivalentMethods 'copy'
it "doesn't mutate when in player code", ->
xit "doesn't mutate when in player code", ->
# We can't run these tests easily because it depends on being in interpreter mode now
expectNoMutation = (fn) ->
v = new Vector 5, 5
# player code detection hack depends on this property being != null
v.__aetherAPIValue = {}
v2 = fn v
expect(v.x).toEqual 5
expect(v).not.toBe v2

View file

@ -6,31 +6,31 @@ describe 'CreateAccountModal', ->
modal = null
initModal = (options) ->
initModal = (options) -> (done) ->
application.facebookHandler.fakeAPI()
application.gplusHandler.fakeAPI()
modal = new CreateAccountModal(options)
modal.render()
modal.render = _.noop
jasmine.demoModal(modal)
_.defer done
afterEach ->
modal.stopListening()
describe 'constructed with showRequiredError is true', ->
beforeEach initModal({showRequiredError: true})
it 'shows a modal explaining to login first', ->
initModal({showRequiredError: true})
expect(modal.$('#required-error-alert').length).toBe(1)
describe 'constructed with showSignupRationale is true', ->
beforeEach initModal({showSignupRationale: true})
it 'shows a modal explaining signup rationale', ->
initModal({showSignupRationale: true})
expect(modal.$('#signup-rationale-alert').length).toBe(1)
describe 'clicking the save button', ->
beforeEach ->
initModal()
beforeEach initModal()
it 'fails if nothing is in the form, showing errors for email, birthday, and password', ->
modal.$('form').each (i, el) -> el.reset()
@ -45,7 +45,7 @@ describe 'CreateAccountModal', ->
expect(jasmine.Ajax.requests.all().length).toBe(0)
expect(modal.$('.has-error').length).toBeTruthy()
it 'fails if birthay is missing', ->
it 'fails if birthday is missing', ->
modal.$('form').each (i, el) -> el.reset()
forms.objectToForm(modal.$el, { email: 'some@email.com', password: 'xyzzy' })
modal.$('form').submit()
@ -110,8 +110,9 @@ describe 'CreateAccountModal', ->
signupButton = null
beforeEach initModal()
beforeEach ->
initModal()
forms.objectToForm(modal.$el, { birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 })
signupButton = modal.$('#gplus-signup-btn')
expect(signupButton.attr('disabled')).toBeFalsy()
@ -176,8 +177,9 @@ describe 'CreateAccountModal', ->
signupButton = null
beforeEach initModal()
beforeEach ->
initModal()
forms.objectToForm(modal.$el, { birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 })
signupButton = modal.$('#facebook-signup-btn')
expect(signupButton.attr('disabled')).toBeFalsy()
@ -235,4 +237,4 @@ describe 'CreateAccountModal', ->
expect(request.method).toBe('PUT')
expect(_.string.startsWith(request.url, '/db/user')).toBe(true)
expect(modal.$('#signup-button').is(':disabled')).toBe(true)
expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234')
expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234')