Merge branch 'master' into production
This commit is contained in:
commit
6868dafb84
44 changed files with 456 additions and 308 deletions
README.md
app
assets/images/pages
core
lib/world
locale
models
Achievement.coffeeArticle.coffeeCocoModel.coffeeLevel.coffeeLevelComponent.coffeeLevelSystem.coffeeThangType.coffeeUser.coffee
styles/play
templates
views
scripts/analytics/mongodb/queries
server
achievements
articles
levels
queues
routes
users
test/server/functional
10
README.md
10
README.md
|
@ -21,11 +21,13 @@ Whether you're novice or pro, the CodeCombat team is ready to help you implement
|
|||
|
||||
### [Join Us!](http://blog.codecombat.com/why-you-should-open-source-your-startup)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
@ -64,6 +66,10 @@ Whether you're novice or pro, the CodeCombat team is ready to help you implement
|
|||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
----------
|
||||
|
|
BIN
app/assets/images/pages/about/cat_small.png
Normal file
BIN
app/assets/images/pages/about/cat_small.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 9 KiB |
BIN
app/assets/images/pages/about/josh_small.png
Normal file
BIN
app/assets/images/pages/about/josh_small.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 9.6 KiB |
Binary file not shown.
Before ![]() (image error) Size: 215 KiB After ![]() (image error) Size: 231 KiB ![]() ![]() |
|
@ -22,7 +22,8 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'account': go('account/MainAccountView')
|
||||
'account/settings': go('account/AccountSettingsRootView')
|
||||
'account/unsubscribe': go('account/UnsubscribeView')
|
||||
'account/profile': go('user/JobProfileView') # legacy URL, sent in emails
|
||||
#'account/profile': go('user/JobProfileView') # legacy URL, sent in emails
|
||||
'account/profile': go('EmployersView') # Show the not-recruiting-now screen
|
||||
'account/payments': go('account/PaymentsView')
|
||||
'account/subscription': go('account/SubscriptionView')
|
||||
|
||||
|
@ -100,7 +101,8 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'test(/*subpath)': go('TestView')
|
||||
|
||||
'user/:slugOrID': go('user/MainUserView')
|
||||
'user/:slugOrID/profile': go('user/JobProfileView')
|
||||
#'user/:slugOrID/profile': go('user/JobProfileView')
|
||||
'user/:slugOrID/profile': go('EmployersView') # Show the not-recruiting-now screen
|
||||
|
||||
'*name': 'showNotFoundView'
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class LiveEditingMarkup extends TreemaNode.nodeMap.ace
|
|||
valEl.append($('<div class="preview"></div>').hide())
|
||||
|
||||
addImageUpload: (valEl) ->
|
||||
return unless me.isAdmin()
|
||||
return unless me.isAdmin() or me.isArtisan()
|
||||
valEl.append(
|
||||
$('<div class="pick-image-button"></div>').append(
|
||||
$('<button>Pick Image</button>')
|
||||
|
|
|
@ -744,3 +744,75 @@ module.exports.thangNames = thangNames =
|
|||
'Warlegs'
|
||||
'Xul Gor'
|
||||
]
|
||||
'Horse': [
|
||||
# TODO: horse names
|
||||
'Wildsilver'
|
||||
'Fleetfire'
|
||||
'Ed'
|
||||
]
|
||||
'Trapper': [
|
||||
'Senick'
|
||||
]
|
||||
'Forest Archer': [
|
||||
'Naria'
|
||||
]
|
||||
'Frog': [
|
||||
'Bighead'
|
||||
'Hypnotoad'
|
||||
'Freddy'
|
||||
'Frogger'
|
||||
'Froggy'
|
||||
'Slippy'
|
||||
'Wart'
|
||||
'Bufo'
|
||||
'Bunda'
|
||||
'Dan\'l Webster'
|
||||
'Mr. Toad'
|
||||
'Trevor'
|
||||
'Wei Qi'
|
||||
]
|
||||
'Raven': [
|
||||
# TODO: raven names
|
||||
'Nevermore'
|
||||
]
|
||||
'Cougar': [
|
||||
# TODO: cougar/puma/catamount/mountain lion/jaguar names
|
||||
'Kitty'
|
||||
]
|
||||
'Raider': [
|
||||
'Arryn'
|
||||
]
|
||||
'Goliath': [
|
||||
'Okar'
|
||||
]
|
||||
'Guardian': [
|
||||
'Illia'
|
||||
]
|
||||
'Pixie': [
|
||||
'Zana'
|
||||
]
|
||||
'Assassin': [
|
||||
'Ritic'
|
||||
]
|
||||
'Necromancer': [
|
||||
'Nalfar'
|
||||
]
|
||||
'Dark Wizard': [
|
||||
'Usara'
|
||||
]
|
||||
'Paladin': [
|
||||
# TODO: paladin names (female)
|
||||
'Celadia'
|
||||
]
|
||||
'Ogre Witch': [
|
||||
# TODO: ogre witch names
|
||||
'Vyrryx'
|
||||
]
|
||||
'Ogre Chieftain': [
|
||||
# TODO: ogre chieftain names (female)
|
||||
'Zagra Ux'
|
||||
]
|
||||
'Ogre Warlock': [
|
||||
# TODO: ogre warlock names
|
||||
'Vyrryx'
|
||||
]
|
||||
|
|
|
@ -520,16 +520,24 @@
|
|||
press_paragraph_1_link: "press packet"
|
||||
press_paragraph_1_suffix: ". All logos and images may be used without contacting us directly."
|
||||
team: "Team"
|
||||
george_title: "CEO"
|
||||
george_title: "Cofounder"
|
||||
george_blurb: "Businesser"
|
||||
scott_title: "Programmer"
|
||||
scott_title: "Cofounder"
|
||||
scott_blurb: "Reasonable One"
|
||||
nick_title: "Programmer"
|
||||
nick_title: "Cofounder"
|
||||
nick_blurb: "Motivation Guru"
|
||||
michael_title: "Programmer"
|
||||
michael_blurb: "Sys Admin"
|
||||
matt_title: "Programmer"
|
||||
matt_blurb: "Bicyclist"
|
||||
cat_title: "Chief Artisan"
|
||||
cat_blurb: "Airbender"
|
||||
josh_title: "Game Designer"
|
||||
josh_blurb: "Floor Is Lava"
|
||||
retrostyle_title: "Illustration"
|
||||
retrostyle_blurb: "RetroStyle"
|
||||
jose_title: "Music"
|
||||
jose_blurb: "Taking Off"
|
||||
|
||||
teachers:
|
||||
title: "CodeCombat for Teachers"
|
||||
|
|
|
@ -5,6 +5,7 @@ module.exports = class Achievement extends CocoModel
|
|||
@className: 'Achievement'
|
||||
@schema: require 'schemas/models/achievement'
|
||||
urlRoot: '/db/achievement'
|
||||
editableByArtisans: true
|
||||
|
||||
isRepeatable: ->
|
||||
@get('proportionalTo')?
|
||||
|
@ -12,7 +13,7 @@ module.exports = class Achievement extends CocoModel
|
|||
getExpFunction: ->
|
||||
func = @get('function', true)
|
||||
return utils.functionCreators[func.kind](func.parameters) if func.kind of utils.functionCreators
|
||||
|
||||
|
||||
save: ->
|
||||
@populateI18N()
|
||||
super(arguments...)
|
||||
|
|
|
@ -5,3 +5,4 @@ module.exports = class Article extends CocoModel
|
|||
@schema: require 'schemas/models/article'
|
||||
urlRoot: '/db/article'
|
||||
saveBackups: true
|
||||
editableByArtisans: true
|
||||
|
|
|
@ -233,6 +233,7 @@ class CocoModel extends Backbone.Model
|
|||
# actor is a User object
|
||||
actor ?= me
|
||||
return true if actor.isAdmin()
|
||||
return true if actor.isArtisan() and @editableByArtisans
|
||||
for permission in (@get('permissions', true) ? [])
|
||||
if permission.target is 'public' or actor.get('_id') is permission.target
|
||||
return true if permission.access in ['owner', 'read']
|
||||
|
@ -243,6 +244,7 @@ class CocoModel extends Backbone.Model
|
|||
# actor is a User object
|
||||
actor ?= me
|
||||
return true if actor.isAdmin()
|
||||
return true if actor.isArtisan() and @editableByArtisans
|
||||
for permission in (@get('permissions', true) ? [])
|
||||
if permission.target is 'public' or actor.get('_id') is permission.target
|
||||
return true if permission.access in ['owner', 'write']
|
||||
|
|
|
@ -10,6 +10,7 @@ module.exports = class Level extends CocoModel
|
|||
'dungeons-of-kithgard': '5411cb3769152f1707be029c'
|
||||
'defense-of-plainswood': '541b67f71ccc8eaae19f3c62'
|
||||
urlRoot: '/db/level'
|
||||
editableByArtisans: true
|
||||
|
||||
serialize: (supermodel, session, otherSession, cached=false) ->
|
||||
o = @denormalize supermodel, session, otherSession # hot spot to optimize
|
||||
|
|
|
@ -17,6 +17,7 @@ module.exports = class LevelComponent extends CocoModel
|
|||
@MissileID: '524cc2593ea855e0ab000142'
|
||||
@FindsPathsID: '52872b0ead92b98561000002'
|
||||
urlRoot: '/db/level.component'
|
||||
editableByArtisans: true
|
||||
|
||||
set: (key, val, options) ->
|
||||
if _.isObject key
|
||||
|
|
|
@ -5,6 +5,7 @@ module.exports = class LevelSystem extends CocoModel
|
|||
@className: 'LevelSystem'
|
||||
@schema: require 'schemas/models/level_system'
|
||||
urlRoot: '/db/level.system'
|
||||
editableByArtisans: true
|
||||
|
||||
set: (key, val, options) ->
|
||||
if _.isObject key
|
||||
|
|
|
@ -27,6 +27,7 @@ module.exports = class ThangType extends CocoModel
|
|||
'simple-boots': '53e237bf53457600003e3f05'
|
||||
urlRoot: '/db/thang.type'
|
||||
building: {}
|
||||
editableByArtisans: true
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
|
|
@ -12,6 +12,7 @@ module.exports = class User extends CocoModel
|
|||
notyErrors: false
|
||||
|
||||
isAdmin: -> 'admin' in @get('permissions', true)
|
||||
isArtisan: -> 'artisan' in @get('permissions', true)
|
||||
isInGodMode: -> 'godmode' in @get('permissions', true)
|
||||
isAnonymous: -> @get('anonymous', true)
|
||||
displayName: -> @get('name', true)
|
||||
|
@ -171,6 +172,7 @@ module.exports = class User extends CocoModel
|
|||
|
||||
isPremium: ->
|
||||
return true if me.isInGodMode()
|
||||
return true if me.isAdmin()
|
||||
return false unless stripe = @get('stripe')
|
||||
return true if stripe.subscriptionID
|
||||
return true if stripe.free is true
|
||||
|
|
|
@ -398,5 +398,6 @@ body.ipad
|
|||
.xp .pulse, .gems .pulse
|
||||
@include animation(none)
|
||||
|
||||
body[lang='ru'] #hero-victory-modal #totals .total-wrapper .total-label
|
||||
font-size: 12px
|
||||
body[lang='ru'], body[lang^='es-ES'], body[lang^='it'], body[lang^='hu'], body[lang^='mk-MK'], body[lang^='ja'], body[lang^='uk']
|
||||
#hero-victory-modal #totals .total-wrapper .total-label
|
||||
font-size: 12px
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
h1
|
||||
position: absolute
|
||||
left: 164px
|
||||
left: 200px
|
||||
top: -70px
|
||||
margin: 0
|
||||
width: 450px
|
||||
width: 612px
|
||||
text-align: center
|
||||
color: rgb(254,188,68)
|
||||
font-size: 38px
|
||||
|
@ -19,7 +19,7 @@
|
|||
.modal-dialog
|
||||
margin: 150px auto 0 auto
|
||||
padding: 0
|
||||
width: 820px
|
||||
width: 1060px
|
||||
height: 460px
|
||||
background: none
|
||||
|
||||
|
@ -33,7 +33,7 @@
|
|||
|
||||
#close-modal
|
||||
position: absolute
|
||||
left: 612px
|
||||
left: 770px
|
||||
top: -80px
|
||||
width: 60px
|
||||
height: 60px
|
||||
|
@ -53,7 +53,7 @@
|
|||
position: absolute
|
||||
left: 55px
|
||||
top: 242px
|
||||
width: 720px
|
||||
width: 960px
|
||||
height: 140px
|
||||
|
||||
.product
|
||||
|
@ -78,8 +78,7 @@
|
|||
width: 80%
|
||||
left: 10%
|
||||
bottom: -30px
|
||||
|
||||
|
||||
|
||||
//- Errors
|
||||
.alert
|
||||
position: absolute
|
||||
|
@ -87,13 +86,3 @@
|
|||
width: 80%
|
||||
top: 20px
|
||||
border: 5px solid gray
|
||||
|
||||
#or-subscribe
|
||||
position: absolute
|
||||
right: 30px
|
||||
top: 420px
|
||||
font-size: 16px
|
||||
color: #eee
|
||||
|
||||
button
|
||||
min-width: 182px
|
||||
|
|
|
@ -57,67 +57,106 @@ block content
|
|||
|
||||
h2(data-i18n="about.team") Team
|
||||
|
||||
img(src="/images/pages/about/george_small.png").img-thumbnail
|
||||
|
||||
.team_bio
|
||||
|
||||
h4.team_name George Saines
|
||||
|
||||
p(data-i18n="about.george_title")
|
||||
| CEO
|
||||
p(data-i18n="about.george_blurb")
|
||||
| Businesser
|
||||
|
||||
img(src="/images/pages/about/scott_small.png").img-thumbnail
|
||||
|
||||
.team_bio
|
||||
|
||||
h4.team_name Scott Erickson
|
||||
|
||||
p(data-i18n="about.scott_title")
|
||||
| Programmer
|
||||
p(data-i18n="about.scott_blurb")
|
||||
| Reasonable one
|
||||
|
||||
li.row
|
||||
|
||||
a(href="http://www.nickwinter.net")
|
||||
img(src="/images/pages/about/nick_small.png").img-thumbnail
|
||||
|
||||
.team_bio
|
||||
|
||||
h4.team_name
|
||||
a(href="http://www.nickwinter.net") Nick Winter
|
||||
|
||||
p(data-i18n="about.nick_title")
|
||||
| Programmer
|
||||
| Cofounder
|
||||
p(data-i18n="about.nick_blurb")
|
||||
| Motivation Guru
|
||||
|
||||
a(href="http://michaelschmatz.com")
|
||||
img(src="/images/pages/about/michael_small.png").img-thumbnail
|
||||
|
||||
.team_bio
|
||||
|
||||
h4.team_name
|
||||
a(href="http://michaelschmatz.com/") Michael Schmatz
|
||||
|
||||
p(data-i18n="about.michael_title")
|
||||
| Programmer
|
||||
p(data-i18n="about.michael_blurb")
|
||||
| Sys Admin
|
||||
|
||||
li.row
|
||||
|
||||
a(href="http://www.mattlott.com/")
|
||||
img(src="/images/pages/about/matt_small.png").img-thumbnail
|
||||
|
||||
.team_bio
|
||||
|
||||
h4.team_name
|
||||
a(href="http://www.mattlott.com/") Matt Lott
|
||||
|
||||
p(data-i18n="about.matt_title")
|
||||
| Programmer
|
||||
p(data-i18n="about.matt_blurb")
|
||||
| Bicyclist
|
||||
|
||||
li.row
|
||||
|
||||
img(src="/images/pages/about/george_small.png").img-thumbnail
|
||||
.team_bio
|
||||
h4.team_name George Saines
|
||||
p(data-i18n="about.george_title")
|
||||
| Cofounder
|
||||
p(data-i18n="about.george_blurb")
|
||||
| Businesser
|
||||
|
||||
a(href="http://cat.zdh.com/")
|
||||
img(src="/images/pages/about/cat_small.png").img-thumbnail
|
||||
.team_bio
|
||||
h4.team_name
|
||||
a(href="http://cat.zdh.com/") Catherine Weresow
|
||||
p(data-i18n="about.cat_title")
|
||||
| Chief Artisan
|
||||
p(data-i18n="about.cat_blurb")
|
||||
| Airbender
|
||||
|
||||
li.row
|
||||
|
||||
img(src="/images/pages/about/scott_small.png").img-thumbnail
|
||||
.team_bio
|
||||
h4.team_name Scott Erickson
|
||||
p(data-i18n="about.scott_title")
|
||||
| Cofounder
|
||||
p(data-i18n="about.scott_blurb")
|
||||
| Reasonable one
|
||||
|
||||
a(href="http://michaelschmatz.com")
|
||||
img(src="/images/pages/about/michael_small.png").img-thumbnail
|
||||
.team_bio
|
||||
h4.team_name
|
||||
a(href="http://michaelschmatz.com/") Michael Schmatz
|
||||
p(data-i18n="about.michael_title")
|
||||
| Programmer
|
||||
p(data-i18n="about.michael_blurb")
|
||||
| Sys Admin
|
||||
|
||||
li.row
|
||||
|
||||
a(href="http://floor.is/lava/")
|
||||
img(src="/images/pages/about/josh_small.png").img-thumbnail
|
||||
.team_bio
|
||||
h4.team_name
|
||||
a(href="http://floor.is/lava/") Josh Lee
|
||||
p(data-i18n="about.josh_title")
|
||||
| Game Designer
|
||||
p(data-i18n="about.josh_blurb")
|
||||
| Floor Is Lava
|
||||
|
||||
a(href="https://soundcloud.com/taking-off")
|
||||
img(src="/images/pages/about/placeholder.png").img-thumbnail
|
||||
.team_bio
|
||||
h4.team_name
|
||||
a(href="https://soundcloud.com/taking-off") Jose Antonini
|
||||
p(data-i18n="about.jose_title")
|
||||
| Music
|
||||
p(data-i18n="about.jose_blurb")
|
||||
| Taking Off
|
||||
|
||||
li.row
|
||||
|
||||
a(href="http://retrostylegames.com/")
|
||||
img(src="/images/pages/about/placeholder.png").img-thumbnail
|
||||
.team_bio
|
||||
h4.team_name
|
||||
a(href="http://retrostylegames.com/") Pavel Konstantinov
|
||||
p(data-i18n="about.pavel_title")
|
||||
| Illustration
|
||||
p(data-i18n="about.pavel_blurb")
|
||||
| RetroStyle Games
|
||||
|
||||
a(href="http://retrostylegames.com/")
|
||||
img(src="/images/pages/about/placeholder.png").img-thumbnail
|
||||
.team_bio
|
||||
h4.team_name
|
||||
a(href="http://retrostylegames.com/") Oleg Ulyanickiy
|
||||
p(data-i18n="about.oleg_title")
|
||||
| Illustration
|
||||
p(data-i18n="about.oleg_blurb")
|
||||
| RetroStyle Games
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
if me.isAdmin()
|
||||
if !unauthorized
|
||||
ol.breadcrumb
|
||||
li
|
||||
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
|
||||
|
@ -13,7 +13,7 @@ block content
|
|||
button.achievement-tool-button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-all-button Recalculate All
|
||||
button.achievement-tool-button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate
|
||||
button.achievement-tool-button(data-i18n="common.delete", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#delete-button Delete
|
||||
button.achievement-tool-button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save
|
||||
button.achievement-tool-button(data-i18n="common.save", disabled=(me.isAdmin() === true || me.isArtisan() === true) ? undefined : "true").btn.btn-primary#save-button Save
|
||||
|
||||
h3(data-i18n="achievement.edit_achievement_title") Edit Achievement
|
||||
span
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
button.btn.btn-primary#new-achievement-button(disabled=me.isAdmin() === true ? undefined : "true" data-i18n="editor.new_achievement_title") Create a New Achievement
|
||||
button.btn.btn-primary#new-achievement-button(disabled=(me.isAdmin() === true || me.isArtisan() === true) ? undefined : "true" data-i18n="editor.new_achievement_title") Create a New Achievement
|
||||
|
||||
if !achievements.models.length
|
||||
.panel
|
||||
|
|
|
@ -11,7 +11,7 @@ nav.navbar.navbar-default(role='navigation')
|
|||
a(href="#system-patches" data-toggle="tab" data-i18n="resources.patches")#system-patches-tab Patches
|
||||
|
||||
ul.nav.navbar-nav.navbar-right
|
||||
if !me.isAdmin()
|
||||
if !me.isAdmin() && !me.isArtisan()
|
||||
li#patch-system-button
|
||||
a(data-i18n="[title]common.submit_patch")
|
||||
span.glyphicon-floppy-disk.glyphicon
|
||||
|
|
|
@ -52,7 +52,7 @@ block header
|
|||
span.glyphicon-chevron-down.glyphicon
|
||||
ul.dropdown-menu
|
||||
li.dropdown-header(data-i18n="common.actions") Actions
|
||||
li(class=!me.isAdmin() ? "disabled": "")
|
||||
li(class=!me.isAdmin() && !me.isArtisan() ? "disabled": "")
|
||||
a(data-i18n="common.fork")#fork-start-button Fork
|
||||
li(class=!authorized ? "disabled": "")
|
||||
a(data-toggle="coco-modal", data-target="modal/RevertModal", data-i18n="editor.revert", disabled=!authorized)#revert-button Revert
|
||||
|
|
|
@ -5,184 +5,184 @@ block content
|
|||
h1(data-i18n="employers.deprecation_warning_title") Sorry, CodeCombat is not recruiting right now.
|
||||
p(data-i18n="employers.deprecation_warning") We are focusing on beginner levels instead of finding expert developers for the time being.
|
||||
|
||||
.deprecated
|
||||
.artisanal-claim
|
||||
if me.get('anonymous')
|
||||
a#login-link(data-i18n="login.log_in") Log In
|
||||
br
|
||||
if !isEmployer && !me.isAdmin()
|
||||
#tagline
|
||||
h1(data-i18n="employers.hire_developers_not_credentials") Hire developers, not credentials.
|
||||
button.btn.get-started-button.employer-button(data-i18n="employers.get_started") Get Started
|
||||
else
|
||||
if !me.get('anonymous')
|
||||
a#logout-link(data-i18n="login.log_out") Log Out
|
||||
br
|
||||
.row
|
||||
- var fullProfiles = isEmployer || me.isAdmin();
|
||||
|
||||
if fullProfiles
|
||||
#filter-column.col-md-3
|
||||
#filter
|
||||
.panel-group#filter_panel
|
||||
a#filter-link(data-toggle="collapse" data-target="#collapseOne")
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h4.panel-title
|
||||
span.glyphicon.glyphicon-folder-open#folder-icon
|
||||
| Filter
|
||||
.panel-collapse.collapse.in#collapseOne
|
||||
.panel-body
|
||||
p
|
||||
strong(data-i18n="employers.already_screened") We've already technically screened all our candidates
|
||||
span(data-i18n="employers.filter_further") , but you can also filter further:
|
||||
form#filters
|
||||
.filter_section#visa_filter
|
||||
h4(data-i18n="employers.filter_visa") Visa
|
||||
label
|
||||
input(type="checkbox" name="visa" value="Authorized to work in the US")
|
||||
span(data-i18n="employers.filter_visa_yes") US Authorized
|
||||
| (#{candidatesInFilter("visa","Authorized to work in the US")})
|
||||
label
|
||||
input(type="checkbox" name="visa" value="Need visa sponsorship")
|
||||
span(data-i18n="employers.filter_visa_no") Not Authorized
|
||||
| (#{candidatesInFilter("visa","Need visa sponsorship")})
|
||||
.filter_section#school_filter
|
||||
h4(data-i18n="account_profile.education") Education
|
||||
label
|
||||
input(type="checkbox" name="schoolFilter" value="Top School")
|
||||
span(data-i18n="employers.filter_education_top") Top School
|
||||
| (#{candidatesInFilter("schoolFilter","Top School")})
|
||||
label
|
||||
input(type="checkbox" name="schoolFilter" value="Other")
|
||||
span(data-i18n="employers.filter_education_other") Other
|
||||
| (#{candidatesInFilter("schoolFilter","Other")})
|
||||
.filter_section#role_filter
|
||||
h4(data-i18n="employers.candidate_role") Role
|
||||
label
|
||||
input(type="checkbox" name="roleFilter" value="Web Developer")
|
||||
span(data-i18n="employers.filter_role_web_developer") Web Developer
|
||||
| (#{candidatesInFilter("roleFilter","Web Developer")})
|
||||
label
|
||||
input(type="checkbox" name="roleFilter" value="Software Developer")
|
||||
span(data-i18n="employers.filter_role_software_developer") Software Developer
|
||||
| (#{candidatesInFilter("roleFilter","Software Developer")})
|
||||
label
|
||||
input(type="checkbox" name="roleFilter" value="Mobile Developer")
|
||||
span(data-i18n="employers.filter_role_mobile_developer") Mobile Developer
|
||||
| (#{candidatesInFilter("roleFilter","Mobile Developer")})
|
||||
.filter_section#seniority_filter
|
||||
h4(data-i18n="employers.filter_experience") Experience
|
||||
label
|
||||
input(type="checkbox" name="seniorityFilter" value="Senior")
|
||||
span(data-i18n="employers.filter_experience_senior") Senior
|
||||
| (#{candidatesInFilter("seniorityFilter","Senior")})
|
||||
label
|
||||
input(type="checkbox" name="seniorityFilter" value="Junior")
|
||||
span(data-i18n="employers.filter_experience_junior") Junior
|
||||
| (#{candidatesInFilter("seniorityFilter","Junior")})
|
||||
label
|
||||
input(type="checkbox" name="seniorityFilter" value="Recent Grad")
|
||||
span(data-i18n="employers.filter_experience_recent_grad") Recent Grad
|
||||
| (#{candidatesInFilter("seniorityFilter","Recent Grad")})
|
||||
label
|
||||
input(type="checkbox" name="seniorityFilter" value="College Student")
|
||||
span(data-i18n="employers.filter_experience_student") College Student
|
||||
| (#{candidatesInFilter("seniorityFilter","College Student")})
|
||||
|
||||
//input#select_all_checkbox(type="checkbox" name="select_all" checked)
|
||||
//| Select all
|
||||
p#results
|
||||
| #{numberOfCandidates}
|
||||
span(data-i18n="employers.results") results
|
||||
h4#filter-alerts-heading Filter Email Alerts
|
||||
p Get an email whenever a candidate meeting certain criteria enters the system.
|
||||
table#saved-filter-table
|
||||
thead
|
||||
tr
|
||||
th Filters
|
||||
th Remove
|
||||
tbody
|
||||
button.btn#create-alert-button Create Alert with Current Filters
|
||||
|
||||
#candidates-column(class=fullProfiles ? "full-profiles col-md-9" : "teaser-profiles col-md-12")
|
||||
if candidates.length
|
||||
#candidate-table
|
||||
table
|
||||
tbody
|
||||
for candidate, index in featuredCandidates
|
||||
- var profile = candidate.get('jobProfile');
|
||||
- var authorized = candidate.id; // If we have the id, then we are authorized.
|
||||
- var profileAge = (new Date() - new Date(profile.updated)) / 86400 / 1000;
|
||||
- var expired = profileAge > 2 * 30.4;
|
||||
- var curated = profile.curated;
|
||||
- var photoSize = fullProfiles ? 75 : 50;
|
||||
|
||||
tr.candidate-row(data-candidate-id=candidate.id, id=candidate.id, class=expired ? "expired" : "")
|
||||
td(rowspan=3)
|
||||
- var photoURL = candidate.getPhotoURL(photoSize, false, true);
|
||||
div(class="candidate-picture " + (/^\/file/.test(photoURL) ? "" : "anonymous"), style='background-image: url(' + encodeURI(photoURL) + ')')
|
||||
if fullProfiles
|
||||
td.candidate-name-cell
|
||||
strong= profile.name
|
||||
| -
|
||||
span= profile.jobTitle
|
||||
tr.description_row(data-candidate-id=candidate.id)
|
||||
if curated && curated.shortDescription
|
||||
td.candidate-description
|
||||
div #{curated.shortDescription}
|
||||
else
|
||||
td.candidate-description
|
||||
div #{profile.shortDescription}
|
||||
tr.border_row(data-candidate-id=candidate.id)
|
||||
if curated
|
||||
- var workHistory = curated.workHistory.join(",");
|
||||
if !fullProfiles
|
||||
td.tag_column
|
||||
img(src="/images/pages/employer/tag.png")
|
||||
| #{profile.jobTitle}
|
||||
td.location_column
|
||||
img(src="/images/pages/employer/location.png")
|
||||
| #{curated.location}
|
||||
td.education_column
|
||||
img(src="/images/pages/employer/education.png")
|
||||
| #{curated.education}
|
||||
td.work_column
|
||||
if workHistory
|
||||
img(src="/images/pages/employer/briefcase.png")
|
||||
| #{workHistory}
|
||||
|
||||
if !fullProfiles
|
||||
div#info_wrapper
|
||||
span.hiring-call-to-action
|
||||
h2#start-hiring(data-i18n="employers.start_hiring") Start hiring.
|
||||
button.btn.get-started-button.employer-button(data-i18n="employers.get_started") Get Started
|
||||
|
||||
h2#hiring-reasons.hiring-call-to-action(data-i18n="employers.reasons") Three reasons you should hire through us:
|
||||
.reasons#top_row
|
||||
.reason
|
||||
img.employer_icon(src="/images/pages/employer/employer_icon2.png")
|
||||
h3(data-i18n="employers.everyone_looking") Everyone here is looking for their next opportunity.
|
||||
p(data-i18n="employers.everyone_looking_blurb") Forget about 20% LinkedIn InMail response rates. Everyone that we list on this site wants to find their next position and will respond to your request for an introduction.
|
||||
.reason
|
||||
img.employer_icon(src="/images/pages/employer/employer_icon6.png")
|
||||
h3(data-i18n="employers.weeding") Sit back; we've done the weeding for you.
|
||||
p(data-i18n="employers.weeding_blurb") Every player that we list has been screened for technical ability. We also perform phone screens for select candidates and make notes on their profiles to save you time.
|
||||
.reason
|
||||
img(class="employer_icon" src="/images/pages/employer/employer_icon3.png")
|
||||
h3(data-i18n="employers.pass_screen") They will pass your technical screen.
|
||||
p(data-i18n="employers.pass_screen_blurb") Review each candidate's code before reaching out. One employer found that 5x as many of our devs passed their technical screen than hiring from Hacker News.
|
||||
span.hiring-call-to-action
|
||||
h2(data-i18n="employers.make_hiring_easier") Make my hiring easier, please.
|
||||
button.btn.get-started-button.employer-button(data-i18n="employers.get_started") Get Started
|
||||
.reasons#bottom_row
|
||||
.reason_long
|
||||
img.employer_icon(src="/images/pages/employer/employer_icon1.png")
|
||||
.reason_text
|
||||
h3(data-i18n="employers.what") What is CodeCombat?
|
||||
p(data-i18n="employers.what_blurb") CodeCombat is a multiplayer browser programming game. Players write code to control their forces in battle against other developers. We support JavaScript, Python, Lua, Clojure, CoffeeScript, and Io.
|
||||
.reason_long
|
||||
img.employer_icon(src="/images/pages/employer/employer_icon5.png")
|
||||
.reason_text
|
||||
h3(data-i18n="employers.cost") How much do we charge?
|
||||
p(data-i18n="employers.cost_blurb") We charge 15% of first year's salary and offer a 100% money back guarantee for 90 days. We don't charge for candidates who are already actively being interviewed at your company.
|
||||
//.deprecated
|
||||
// .artisanal-claim
|
||||
// if me.get('anonymous')
|
||||
// a#login-link(data-i18n="login.log_in") Log In
|
||||
// br
|
||||
// if !isEmployer && !me.isAdmin()
|
||||
// #tagline
|
||||
// h1(data-i18n="employers.hire_developers_not_credentials") Hire developers, not credentials.
|
||||
// button.btn.get-started-button.employer-button(data-i18n="employers.get_started") Get Started
|
||||
// else
|
||||
// if !me.get('anonymous')
|
||||
// a#logout-link(data-i18n="login.log_out") Log Out
|
||||
// br
|
||||
// .row
|
||||
// - var fullProfiles = isEmployer || me.isAdmin();
|
||||
//
|
||||
// if fullProfiles
|
||||
// #filter-column.col-md-3
|
||||
// #filter
|
||||
// .panel-group#filter_panel
|
||||
// a#filter-link(data-toggle="collapse" data-target="#collapseOne")
|
||||
// .panel.panel-default
|
||||
// .panel-heading
|
||||
// h4.panel-title
|
||||
// span.glyphicon.glyphicon-folder-open#folder-icon
|
||||
// | Filter
|
||||
// .panel-collapse.collapse.in#collapseOne
|
||||
// .panel-body
|
||||
// p
|
||||
// strong(data-i18n="employers.already_screened") We've already technically screened all our candidates
|
||||
// span(data-i18n="employers.filter_further") , but you can also filter further:
|
||||
// form#filters
|
||||
// .filter_section#visa_filter
|
||||
// h4(data-i18n="employers.filter_visa") Visa
|
||||
// label
|
||||
// input(type="checkbox" name="visa" value="Authorized to work in the US")
|
||||
// span(data-i18n="employers.filter_visa_yes") US Authorized
|
||||
// | (#{candidatesInFilter("visa","Authorized to work in the US")})
|
||||
// label
|
||||
// input(type="checkbox" name="visa" value="Need visa sponsorship")
|
||||
// span(data-i18n="employers.filter_visa_no") Not Authorized
|
||||
// | (#{candidatesInFilter("visa","Need visa sponsorship")})
|
||||
// .filter_section#school_filter
|
||||
// h4(data-i18n="account_profile.education") Education
|
||||
// label
|
||||
// input(type="checkbox" name="schoolFilter" value="Top School")
|
||||
// span(data-i18n="employers.filter_education_top") Top School
|
||||
// | (#{candidatesInFilter("schoolFilter","Top School")})
|
||||
// label
|
||||
// input(type="checkbox" name="schoolFilter" value="Other")
|
||||
// span(data-i18n="employers.filter_education_other") Other
|
||||
// | (#{candidatesInFilter("schoolFilter","Other")})
|
||||
// .filter_section#role_filter
|
||||
// h4(data-i18n="employers.candidate_role") Role
|
||||
// label
|
||||
// input(type="checkbox" name="roleFilter" value="Web Developer")
|
||||
// span(data-i18n="employers.filter_role_web_developer") Web Developer
|
||||
// | (#{candidatesInFilter("roleFilter","Web Developer")})
|
||||
// label
|
||||
// input(type="checkbox" name="roleFilter" value="Software Developer")
|
||||
// span(data-i18n="employers.filter_role_software_developer") Software Developer
|
||||
// | (#{candidatesInFilter("roleFilter","Software Developer")})
|
||||
// label
|
||||
// input(type="checkbox" name="roleFilter" value="Mobile Developer")
|
||||
// span(data-i18n="employers.filter_role_mobile_developer") Mobile Developer
|
||||
// | (#{candidatesInFilter("roleFilter","Mobile Developer")})
|
||||
// .filter_section#seniority_filter
|
||||
// h4(data-i18n="employers.filter_experience") Experience
|
||||
// label
|
||||
// input(type="checkbox" name="seniorityFilter" value="Senior")
|
||||
// span(data-i18n="employers.filter_experience_senior") Senior
|
||||
// | (#{candidatesInFilter("seniorityFilter","Senior")})
|
||||
// label
|
||||
// input(type="checkbox" name="seniorityFilter" value="Junior")
|
||||
// span(data-i18n="employers.filter_experience_junior") Junior
|
||||
// | (#{candidatesInFilter("seniorityFilter","Junior")})
|
||||
// label
|
||||
// input(type="checkbox" name="seniorityFilter" value="Recent Grad")
|
||||
// span(data-i18n="employers.filter_experience_recent_grad") Recent Grad
|
||||
// | (#{candidatesInFilter("seniorityFilter","Recent Grad")})
|
||||
// label
|
||||
// input(type="checkbox" name="seniorityFilter" value="College Student")
|
||||
// span(data-i18n="employers.filter_experience_student") College Student
|
||||
// | (#{candidatesInFilter("seniorityFilter","College Student")})
|
||||
//
|
||||
// //input#select_all_checkbox(type="checkbox" name="select_all" checked)
|
||||
// //| Select all
|
||||
// p#results
|
||||
// | #{numberOfCandidates}
|
||||
// span(data-i18n="employers.results") results
|
||||
// h4#filter-alerts-heading Filter Email Alerts
|
||||
// p Get an email whenever a candidate meeting certain criteria enters the system.
|
||||
// table#saved-filter-table
|
||||
// thead
|
||||
// tr
|
||||
// th Filters
|
||||
// th Remove
|
||||
// tbody
|
||||
// button.btn#create-alert-button Create Alert with Current Filters
|
||||
//
|
||||
// #candidates-column(class=fullProfiles ? "full-profiles col-md-9" : "teaser-profiles col-md-12")
|
||||
// if candidates.length
|
||||
// #candidate-table
|
||||
// table
|
||||
// tbody
|
||||
// for candidate, index in featuredCandidates
|
||||
// - var profile = candidate.get('jobProfile');
|
||||
// - var authorized = candidate.id; // If we have the id, then we are authorized.
|
||||
// - var profileAge = (new Date() - new Date(profile.updated)) / 86400 / 1000;
|
||||
// - var expired = profileAge > 2 * 30.4;
|
||||
// - var curated = profile.curated;
|
||||
// - var photoSize = fullProfiles ? 75 : 50;
|
||||
//
|
||||
// tr.candidate-row(data-candidate-id=candidate.id, id=candidate.id, class=expired ? "expired" : "")
|
||||
// td(rowspan=3)
|
||||
// - var photoURL = candidate.getPhotoURL(photoSize, false, true);
|
||||
// div(class="candidate-picture " + (/^\/file/.test(photoURL) ? "" : "anonymous"), style='background-image: url(' + encodeURI(photoURL) + ')')
|
||||
// if fullProfiles
|
||||
// td.candidate-name-cell
|
||||
// strong= profile.name
|
||||
// | -
|
||||
// span= profile.jobTitle
|
||||
// tr.description_row(data-candidate-id=candidate.id)
|
||||
// if curated && curated.shortDescription
|
||||
// td.candidate-description
|
||||
// div #{curated.shortDescription}
|
||||
// else
|
||||
// td.candidate-description
|
||||
// div #{profile.shortDescription}
|
||||
// tr.border_row(data-candidate-id=candidate.id)
|
||||
// if curated
|
||||
// - var workHistory = curated.workHistory.join(",");
|
||||
// if !fullProfiles
|
||||
// td.tag_column
|
||||
// img(src="/images/pages/employer/tag.png")
|
||||
// | #{profile.jobTitle}
|
||||
// td.location_column
|
||||
// img(src="/images/pages/employer/location.png")
|
||||
// | #{curated.location}
|
||||
// td.education_column
|
||||
// img(src="/images/pages/employer/education.png")
|
||||
// | #{curated.education}
|
||||
// td.work_column
|
||||
// if workHistory
|
||||
// img(src="/images/pages/employer/briefcase.png")
|
||||
// | #{workHistory}
|
||||
//
|
||||
// if !fullProfiles
|
||||
// div#info_wrapper
|
||||
// span.hiring-call-to-action
|
||||
// h2#start-hiring(data-i18n="employers.start_hiring") Start hiring.
|
||||
// button.btn.get-started-button.employer-button(data-i18n="employers.get_started") Get Started
|
||||
//
|
||||
// h2#hiring-reasons.hiring-call-to-action(data-i18n="employers.reasons") Three reasons you should hire through us:
|
||||
// .reasons#top_row
|
||||
// .reason
|
||||
// img.employer_icon(src="/images/pages/employer/employer_icon2.png")
|
||||
// h3(data-i18n="employers.everyone_looking") Everyone here is looking for their next opportunity.
|
||||
// p(data-i18n="employers.everyone_looking_blurb") Forget about 20% LinkedIn InMail response rates. Everyone that we list on this site wants to find their next position and will respond to your request for an introduction.
|
||||
// .reason
|
||||
// img.employer_icon(src="/images/pages/employer/employer_icon6.png")
|
||||
// h3(data-i18n="employers.weeding") Sit back; we've done the weeding for you.
|
||||
// p(data-i18n="employers.weeding_blurb") Every player that we list has been screened for technical ability. We also perform phone screens for select candidates and make notes on their profiles to save you time.
|
||||
// .reason
|
||||
// img(class="employer_icon" src="/images/pages/employer/employer_icon3.png")
|
||||
// h3(data-i18n="employers.pass_screen") They will pass your technical screen.
|
||||
// p(data-i18n="employers.pass_screen_blurb") Review each candidate's code before reaching out. One employer found that 5x as many of our devs passed their technical screen than hiring from Hacker News.
|
||||
// span.hiring-call-to-action
|
||||
// h2(data-i18n="employers.make_hiring_easier") Make my hiring easier, please.
|
||||
// button.btn.get-started-button.employer-button(data-i18n="employers.get_started") Get Started
|
||||
// .reasons#bottom_row
|
||||
// .reason_long
|
||||
// img.employer_icon(src="/images/pages/employer/employer_icon1.png")
|
||||
// .reason_text
|
||||
// h3(data-i18n="employers.what") What is CodeCombat?
|
||||
// p(data-i18n="employers.what_blurb") CodeCombat is a multiplayer browser programming game. Players write code to control their forces in battle against other developers. We support JavaScript, Python, Lua, Clojure, CoffeeScript, and Io.
|
||||
// .reason_long
|
||||
// img.employer_icon(src="/images/pages/employer/employer_icon5.png")
|
||||
// .reason_text
|
||||
// h3(data-i18n="employers.cost") How much do we charge?
|
||||
// p(data-i18n="employers.cost_blurb") We charge 15% of first year's salary and offer a 100% money back guarantee for 90 days. We don't charge for candidates who are already actively being interviewed at your company.
|
||||
|
|
|
@ -30,7 +30,7 @@ block outer_content
|
|||
strong(data-i18n="home.ipad_browser") Bad news: CodeCombat doesn't run on iPad in the browser. Good news: our native iPad app is awaiting Apple approval.
|
||||
|
||||
block extra_footer_content
|
||||
if true || explainHourOfCode
|
||||
if explainHourOfCode
|
||||
// Does not show up unless lang is en-US.
|
||||
div.hour-of-code-explanation
|
||||
| The 'Hour of Code' is a nationwide initiative by
|
||||
|
|
|
@ -83,8 +83,8 @@ else
|
|||
button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
|
||||
if me.get('anonymous') === false || me.get('iosIdentifierForVendor') || isIPadApp
|
||||
button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
|
||||
button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
|
||||
if me.isAdmin()
|
||||
button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
|
||||
button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
|
||||
else if me.get('anonymous', true)
|
||||
button.btn.settings(data-toggle='coco-modal', data-target='core/AuthModal', data-i18n="[title]play.settings")
|
||||
|
|
|
@ -18,6 +18,11 @@
|
|||
h3(data-i18n=product.i18n)
|
||||
button.btn.btn-illustrated.btn-lg(value=product.id)
|
||||
span= product.price
|
||||
|
||||
.product
|
||||
h4 x3500 / mo
|
||||
h3(data-i18n="account.subscription")
|
||||
button.start-subscription-button.btn.btn-lg.btn-illustrated.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
||||
|
||||
if state === 'declined'
|
||||
#declined-alert.alert.alert-danger.alert-dismissible
|
||||
|
@ -40,8 +45,4 @@
|
|||
|
||||
div#close-modal
|
||||
span.glyphicon.glyphicon-remove
|
||||
|
||||
#or-subscribe
|
||||
span(data-i18n="buy_gems.or_subscribe") Or 3500 gems/mo if you...
|
||||
br
|
||||
button.start-subscription-button.btn.btn-lg.btn-illustrated.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
||||
|
||||
|
|
|
@ -27,20 +27,24 @@ module.exports = class EmployersView extends RootView
|
|||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
return
|
||||
@candidates = @supermodel.loadCollection(new CandidatesCollection(), 'candidates').model
|
||||
@setFilterDefaults()
|
||||
|
||||
onLoaded: ->
|
||||
super()
|
||||
return
|
||||
@setUpScrolling()
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
return
|
||||
@sortTable() if @candidates.models.length
|
||||
@renderSavedFilters()
|
||||
|
||||
afterInsert: ->
|
||||
super()
|
||||
return
|
||||
_.delay @checkForEmployerSignupHash, 500
|
||||
#fairly hacky, change this in the future
|
||||
@originalBackgroundColor = $('body').css 'background-color'
|
||||
|
@ -176,6 +180,7 @@ module.exports = class EmployersView extends RootView
|
|||
|
||||
getRenderData: ->
|
||||
ctx = super()
|
||||
return ctx
|
||||
ctx.isEmployer = @isEmployer()
|
||||
#If you change the candidates displayed, change candidatesInFilter()
|
||||
ctx.candidates = _.sortBy @candidates.models, (c) -> -1 * c.get('jobProfile').experience
|
||||
|
|
|
@ -251,7 +251,7 @@ module.exports = class CocoView extends Backbone.View
|
|||
@_lastLoading = null
|
||||
|
||||
showReadOnly: ->
|
||||
return if me.isAdmin()
|
||||
return if me.isAdmin() or me.isArtisan()
|
||||
warning = $.i18n.t 'editor.read_only_warning2', defaultValue: 'Note: you can\'t save any edits here, because you\'re not logged in.'
|
||||
noty text: warning, layout: 'center', type: 'information', killer: true, timeout: 5000
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ module.exports = class AchievementEditView extends RootView
|
|||
getRenderData: (context={}) ->
|
||||
context = super(context)
|
||||
context.achievement = @achievement
|
||||
context.authorized = me.isAdmin()
|
||||
context.authorized = me.isAdmin() or me.isArtisan()
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
|
|
|
@ -15,6 +15,6 @@ module.exports = class AchievementSearchView extends SearchView
|
|||
context.currentNewSignup = 'editor.new_achievement_title_login'
|
||||
context.currentSearch = 'editor.achievement_search_title'
|
||||
context.newModelsAdminOnly = true
|
||||
context.unauthorized = true unless me.isAdmin()
|
||||
context.unauthorized = true unless me.isAdmin() or me.isArtisan()
|
||||
@$el.i18n()
|
||||
context
|
||||
|
|
|
@ -393,6 +393,9 @@ module.exports = class CampaignView extends RootView
|
|||
|
||||
onClickMap: (e) ->
|
||||
@$levelInfo?.hide()
|
||||
if @sessions.models.length < 3
|
||||
# Restore the next level higlight for very new players who might otherwise get lost.
|
||||
@highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top']
|
||||
|
||||
onClickLevel: (e) ->
|
||||
e.preventDefault()
|
||||
|
|
|
@ -21,7 +21,7 @@ module.exports = class BuyGemsModal extends ModalView
|
|||
'stripe:received-token': 'onStripeReceivedToken'
|
||||
|
||||
events:
|
||||
'click .product button': 'onClickProductButton'
|
||||
'click .product button:not(.start-subscription-button)': 'onClickProductButton'
|
||||
'click #close-modal': 'hide'
|
||||
'click .start-subscription-button': 'onClickStartSubscription'
|
||||
|
||||
|
|
|
@ -295,8 +295,16 @@ module.exports = class PlayHeroesModal extends ModalView
|
|||
|
||||
saveAndHide: ->
|
||||
hero = @selectedHero?.get('original')
|
||||
hero ?= @visibleHero?.get('original') if @visibleHero?.loaded and not @visibleHero.locked
|
||||
unless hero
|
||||
console.error 'Somehow we tried to hide without having a hero selected yet...'
|
||||
noty {
|
||||
text: "Error: hero not loaded. If this keeps happening, please report the bug."
|
||||
layout: 'topCenter'
|
||||
timeout: 10000
|
||||
type: 'error'
|
||||
}
|
||||
return
|
||||
|
||||
if @session
|
||||
changed = @updateHeroConfig(@session, hero)
|
||||
|
|
|
@ -10,11 +10,7 @@
|
|||
|
||||
// Drop count: last started or finished level event for a given unique user
|
||||
|
||||
// TODO: Why are Mixpanel level finish events significantly lower?
|
||||
// TODO: dungeons-of-kithgard completion rate is 62% vs. 77%
|
||||
// TODO: Similar start events, finish events off by 20% (5334 vs 6486)
|
||||
// TODO: Are Mixpanel rates accounting for finishing steps likely to be completed in the future?
|
||||
// TODO: Use Mixpanel export API to investigate
|
||||
// TODO: Convert this to a node script so it can use proper libraries (e.g. slugify)
|
||||
|
||||
try {
|
||||
var scriptStartTime = new Date();
|
||||
|
@ -91,6 +87,17 @@ catch(err) {
|
|||
|
||||
// *** Helper functions ***
|
||||
|
||||
function slugify(text)
|
||||
// https://gist.github.com/mathewbyrne/1280286
|
||||
{
|
||||
return text.toString().toLowerCase()
|
||||
.replace(/\s+/g, '-') // Replace spaces with -
|
||||
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
||||
.replace(/\-\-+/g, '-') // Replace multiple - with single -
|
||||
.replace(/^-+/, '') // Trim - from start of text
|
||||
.replace(/-+$/, ''); // Trim - from end of text
|
||||
}
|
||||
|
||||
function log(str) {
|
||||
print(new Date().toISOString() + " " + str);
|
||||
}
|
||||
|
@ -158,7 +165,7 @@ function getLevelFunnelData(startDay, eventFunnel) {
|
|||
var level;
|
||||
|
||||
// TODO: Switch to properties.levelID for 'Saw Victory'
|
||||
if (event === 'Saw Victory' && properties.level) level = properties.level.toLowerCase().replace(/ /g, '-');
|
||||
if (event === 'Saw Victory' && properties.level) level = slugify(properties.level);
|
||||
else if (properties.levelID) level = properties.levelID
|
||||
else continue
|
||||
|
||||
|
@ -228,7 +235,7 @@ function getLevelDropCounts(startDay, events) {
|
|||
var level;
|
||||
|
||||
// TODO: Switch to properties.levelID for 'Saw Victory'
|
||||
if (event === 'Saw Victory' && properties.level) level = properties.level.toLowerCase().replace(/ /g, '-');
|
||||
if (event === 'Saw Victory' && properties.level) level = slugify(properties.level);
|
||||
else if (properties.levelID) level = properties.levelID
|
||||
else continue
|
||||
|
||||
|
@ -308,7 +315,7 @@ function getLevelSubscriptionCounts(startDay) {
|
|||
var queryParams = {$and: [
|
||||
{_id: {$gte: startObj}},
|
||||
{$or: [
|
||||
{$and: [{'event': 'Show subscription modal'}, {'properties.level': {$exists: true}}]},
|
||||
{$and: [{'event': 'Show subscription modal'}, {'properties.level': {$exists: true}}]},
|
||||
{'event': 'Finished subscription purchase'}]
|
||||
}
|
||||
]};
|
||||
|
@ -331,13 +338,13 @@ function getLevelSubscriptionCounts(startDay) {
|
|||
|
||||
// TODO: This is for legacy data.
|
||||
// TODO: Event tracking updated to use level slug for loading level view on ~1/21/15
|
||||
level = level.toLowerCase().replace(/ /g, '-');
|
||||
level = slugify(level);
|
||||
|
||||
if (!userDataMap[user][event]) userDataMap[user][event] = {};
|
||||
if (!userDataMap[user][event][level] || userDataMap[user][event][level].localeCompare(day) > 0) {
|
||||
userDataMap[user][event][level] = day;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (event === 'Finished subscription purchase') {
|
||||
if (!userDataMap[user][event] || userDataMap[user][event].localeCompare(day) > 0) {
|
||||
userDataMap[user][event] = day;
|
||||
|
|
|
@ -22,18 +22,18 @@ class AchievementHandler extends Handler
|
|||
'i18n'
|
||||
'i18nCoverage'
|
||||
]
|
||||
|
||||
|
||||
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
jsonSchema = require '../../app/schemas/models/achievement.coffee'
|
||||
|
||||
|
||||
hasAccess: (req) ->
|
||||
req.method in ['GET', 'PUT'] or req.user?.isAdmin()
|
||||
req.method in ['GET', 'PUT'] or req.user?.isAdmin() or req.user?.isArtisan()
|
||||
|
||||
hasAccessToDocument: (req, document, method=null) ->
|
||||
method = (method or req.method).toLowerCase()
|
||||
return true if method is 'get'
|
||||
return true if req.user?.isAdmin()
|
||||
return true if req.user?.isAdmin() or req.user?.isArtisan()
|
||||
return true if method is 'put' and @isJustFillingTranslations(req, document)
|
||||
return
|
||||
|
||||
|
@ -49,7 +49,7 @@ class AchievementHandler extends Handler
|
|||
super req, res
|
||||
|
||||
delete: (req, res, slugOrID) ->
|
||||
return @sendForbiddenError res unless req.user?.isAdmin()
|
||||
return @sendForbiddenError res unless req.user?.isAdmin() or req.user?.isArtisan()
|
||||
@getDocumentForIdOrSlug slugOrID, (err, document) => # Check first
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res) unless document?
|
||||
|
|
|
@ -10,7 +10,7 @@ ArticleHandler = class ArticleHandler extends Handler
|
|||
req.method is 'GET' or req.user?.isAdmin()
|
||||
|
||||
hasAccessToDocument: (req, document, method=null) ->
|
||||
return true if req.method is 'GET' or method is 'get' or req.user?.isAdmin()
|
||||
return true if req.method is 'GET' or method is 'get' or req.user?.isAdmin() or req.user?.isArtisan()
|
||||
return false
|
||||
|
||||
module.exports = new ArticleHandler()
|
||||
|
|
|
@ -21,7 +21,7 @@ LevelSystemHandler = class LevelSystemHandler extends Handler
|
|||
props
|
||||
|
||||
hasAccess: (req) ->
|
||||
req.method is 'GET' or req.user?.isAdmin()
|
||||
req.method is 'GET' or req.user?.isAdmin() or req.user?.isArtisan()
|
||||
|
||||
|
||||
module.exports = new LevelSystemHandler()
|
||||
|
|
|
@ -43,7 +43,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
|
|||
hasAccessToDocument: (req, document, method=null) ->
|
||||
method = (method or req.method).toLowerCase()
|
||||
return true if method is 'get'
|
||||
return true if req.user?.isAdmin()
|
||||
return true if req.user?.isAdmin() or req.user?.isArtisan()
|
||||
return true if method is 'post' and @isJustFillingTranslations(req, document)
|
||||
return
|
||||
|
||||
|
|
|
@ -128,7 +128,7 @@ module.exports.getTwoGames = (req, res) ->
|
|||
ogresGameID = req.body.ogresGameID
|
||||
return if simulatorIsTooOld req, res
|
||||
#ladderGameIDs = ['greed', 'criss-cross', 'brawlwood', 'dungeon-arena', 'gold-rush', 'sky-span'] # Let's not give any extra simulations to old ladders.
|
||||
ladderGameIDs = ['dueling-grounds', 'cavern-survival', 'multiplayer-treasure-grove']
|
||||
ladderGameIDs = ['dueling-grounds', 'cavern-survival', 'multiplayer-treasure-grove', 'harrowland']
|
||||
levelID = _.sample ladderGameIDs
|
||||
sortLimit = 200
|
||||
unless ogresGameID and humansGameID
|
||||
|
|
|
@ -17,7 +17,7 @@ module.exports.setup = (app) ->
|
|||
return res.end()
|
||||
|
||||
createMailContext = (req, done) ->
|
||||
sender = req.body.sender
|
||||
sender = req.body.sender or req.body.email
|
||||
message = req.body.message
|
||||
user = req.user
|
||||
recipientID = req.body.recipientID
|
||||
|
|
|
@ -42,6 +42,10 @@ UserSchema.methods.isAdmin = ->
|
|||
p = @get('permissions')
|
||||
return p and 'admin' in p
|
||||
|
||||
UserSchema.methods.isArtisan = ->
|
||||
p = @get('permissions')
|
||||
return p and 'artisan' in p
|
||||
|
||||
UserSchema.methods.isAnonymous = ->
|
||||
@get 'anonymous'
|
||||
|
||||
|
@ -206,6 +210,7 @@ UserSchema.methods.register = (done) ->
|
|||
|
||||
UserSchema.methods.isPremium = ->
|
||||
return true if @isInGodMode()
|
||||
return true if @isAdmin()
|
||||
return false unless stripeObject = @get('stripe')
|
||||
return true if stripeObject.subscriptionID
|
||||
return true if stripeObject.free is true
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
config = require '../../../server_config'
|
||||
require '../common'
|
||||
|
||||
|
@ -19,7 +18,7 @@ describe '/db/user, editing stripe.couponID property', ->
|
|||
#- shared data between tests
|
||||
joeData = null
|
||||
firstSubscriptionID = null
|
||||
|
||||
|
||||
it 'does not work for non-admins', (done) ->
|
||||
loginJoe (joe) ->
|
||||
joeData = joe.toObject()
|
||||
|
@ -29,14 +28,14 @@ describe '/db/user, editing stripe.couponID property', ->
|
|||
expect(res.statusCode).toBe(200) # fails silently
|
||||
expect(res.body.stripe).toBeUndefined() # but still fails
|
||||
done()
|
||||
|
||||
|
||||
it 'does not work with invalid coupons', (done) ->
|
||||
loginAdmin (admin) ->
|
||||
joeData.stripe = { couponID: 'DNE' }
|
||||
request.put {uri: userURL, json: joeData }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(404)
|
||||
done()
|
||||
|
||||
|
||||
it 'sets the couponID on a user without an existing stripe object', (done) ->
|
||||
joeData.stripe = { couponID: '20pct' }
|
||||
request.put {uri: userURL, json: joeData }, (err, res, body) ->
|
||||
|
@ -44,7 +43,7 @@ describe '/db/user, editing stripe.couponID property', ->
|
|||
expect(res.statusCode).toBe(200)
|
||||
expect(body.stripe.couponID).toBe('20pct')
|
||||
done()
|
||||
|
||||
|
||||
it 'just updates the couponID when it changes and there is no existing subscription', (done) ->
|
||||
joeData.stripe.couponID = '500off'
|
||||
request.put {uri: userURL, json: joeData }, (err, res, body) ->
|
||||
|
@ -80,10 +79,10 @@ describe '/db/user, editing stripe.couponID property', ->
|
|||
expect(res.statusCode).toBe(200)
|
||||
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
|
||||
expect(customer.discount).toBeDefined()
|
||||
expect(customer.discount.coupon.id).toBe('500off')
|
||||
expect(customer.discount?.coupon.id).toBe('500off')
|
||||
done()
|
||||
|
||||
|
||||
|
||||
|
||||
it 'updates the discount on the customer when an admin changes the couponID', (done) ->
|
||||
loginAdmin (admin) ->
|
||||
joeData.stripe.couponID = '20pct'
|
||||
|
@ -93,7 +92,7 @@ describe '/db/user, editing stripe.couponID property', ->
|
|||
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
|
||||
expect(customer.discount.coupon.id).toBe('20pct')
|
||||
done()
|
||||
|
||||
|
||||
it 'removes discounts from the customer when an admin removes the couponID', (done) ->
|
||||
delete joeData.stripe.couponID
|
||||
request.put {uri: userURL, json: joeData }, (err, res, body) ->
|
||||
|
@ -111,4 +110,3 @@ describe '/db/user, editing stripe.couponID property', ->
|
|||
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
|
||||
expect(customer.discount.coupon.id).toBe('20pct')
|
||||
done()
|
||||
|
||||
|
|
|
@ -71,13 +71,13 @@ describe '/db/patch', ->
|
|||
it 'does not add duplicate watchers', (done) ->
|
||||
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
|
||||
request.put {uri: watchingURL, json: {on: true}}, (err, res, body) ->
|
||||
expect(body.watchers.length).toBe(4)
|
||||
expect(body.watchers.length).toBe(3)
|
||||
done()
|
||||
|
||||
it 'allows removing yourself', (done) ->
|
||||
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
|
||||
request.put {uri: watchingURL, json: {on: false}}, (err, res, body) ->
|
||||
expect(body.watchers.length).toBe(3)
|
||||
expect(body.watchers.length).toBe(2)
|
||||
done()
|
||||
|
||||
it 'allows the submitter to withdraw the pull request', (done) ->
|
||||
|
@ -157,9 +157,3 @@ describe '/db/patch', ->
|
|||
Patch.findOne({}).exec (err, article) ->
|
||||
expect(article.get('status')).toBe 'accepted'
|
||||
done()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
Reference in a new issue