mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-13 09:11:22 -05:00
Merge all the bugquest fixes!
This commit is contained in:
commit
4359eb2caa
68 changed files with 619 additions and 432 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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%
|
||||
|
|
21
app/styles/editor/verifier/verifier-view.sass
Normal file
21
app/styles/editor/verifier/verifier-view.sass
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ?= {}
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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({})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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', ->
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue