Merge branch 'master' into production

This commit is contained in:
Nick Winter 2015-02-25 18:41:56 -08:00
commit 6868dafb84
44 changed files with 456 additions and 308 deletions

View file

@ -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)
![Nick Winter](http://codecombat.com/images/pages/about/nick_small.png)
![George Saines](http://codecombat.com/images/pages/about/george_small.png)
![Scott Erickson](http://codecombat.com/images/pages/about/scott_small.png)
![Nick Winter](http://codecombat.com/images/pages/about/nick_small.png)
![Michael Schmatz](http://codecombat.com/images/pages/about/michael_small.png)
![Matt Lott](http://codecombat.com/images/pages/about/matt_small.png)
![Catherine Weresow](http://codecombat.com/images/pages/about/cat_small.png)
![Michael Schmatz](http://codecombat.com/images/pages/about/michael_small.png)
![Josh Lee](http://codecombat.com/images/pages/about/josh_small.png)
![Jeremy Arns](http://codecombat.com/images/pages/about/jeremy_small.png)
![Alex Cotsarelis](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Cotsarelis/alex_100.png)
![Alexandru Caciulescu](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alexandru%20Caciulescu/alexandru_100.png)
@ -64,6 +66,10 @@ Whether you're novice or pro, the CodeCombat team is ready to help you implement
![Tom Steinbrecher](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Tom%20Steinbrecher/tom_100.png)
![Yang Shun Tay](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Yang%20Shun%20Tay/yang_shun_tay_100.png)
![Zach Martin](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Zach%20Martin/zack_100.png)
![Alex Crooks](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Crooks/alex_100.png)
![Danny Whittaker](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Danny%20Whittaker/danny_100.png)
![Kevin Holland](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Kevin%20Holland/kevin_100.png)
----------

Binary file not shown.

After

(image error) Size: 9 KiB

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

Before After
Before After

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,3 +5,4 @@ module.exports = class Article extends CocoModel
@schema: require 'schemas/models/article'
urlRoot: '/db/article'
saveBackups: true
editableByArtisans: true

View file

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

View file

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

View file

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

View file

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

View file

@ -27,6 +27,7 @@ module.exports = class ThangType extends CocoModel
'simple-boots': '53e237bf53457600003e3f05'
urlRoot: '/db/thang.type'
building: {}
editableByArtisans: true
initialize: ->
super()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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