Merge pull request from codecombat/master

Merge master into production
This commit is contained in:
Michael Schmatz 2014-11-25 10:02:51 -05:00
commit 2300cab908
18 changed files with 229 additions and 86 deletions

View file

@ -96,10 +96,10 @@
logging_in: "Logging In"
log_out: "Log Out"
recover: "recover account"
authenticate_gplus: 'Authenticate G+'
load_profile: 'Load G+ Profile'
load_email: 'Load G+ Email'
finishing: 'Finishing'
authenticate_gplus: "Authenticate G+"
load_profile: "Load G+ Profile"
load_email: "Load G+ Email"
finishing: "Finishing"
signup:
create_account_title: "Create Account to Save Progress"
@ -319,12 +319,15 @@
unequip: "Unequip"
buy_gems:
few_gems: 'A few gems'
pile_gems: 'Pile of gems'
chest_gems: 'Chest of gems'
purchasing: 'Purchasing...'
declined: 'Your card was declined'
retrying: 'Server error, retrying.'
few_gems: "A few gems"
pile_gems: "Pile of gems"
chest_gems: "Chest of gems"
purchasing: "Purchasing..."
declined: "Your card was declined"
retrying: "Server error, retrying."
prompt_title: "Not Enough Gems"
prompt_body: "Do you want to get more?"
prompt_button: "Enter Shop"
choose_hero:
choose_hero: "Choose Your Hero"

View file

@ -10,7 +10,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
for_beginners: "Para Iniciantes"
multiplayer: "Multijogador" # Not currently shown on home page
for_developers: "Para Programadores" # Not currently shown on home page.
# or_ipad: "Or download for iPad"
or_ipad: "Ou descarrega para iPad"
nav:
play: "Níveis" # The top nav bar entry where players choose which levels to play
@ -208,8 +208,8 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
failing: "A falhar"
action_timeline: "Linha do Tempo de Ações"
click_to_select: "Clica numa unidade para selecioná-la."
# control_bar_multiplayer: "Multiplayer"
# control_bar_join_game: "Join Game"
control_bar_multiplayer: "Multijogador"
control_bar_join_game: "Entrar no Jogo"
reload: "Recarregar"
reload_title: "Recarregar o Código Todo?"
reload_really: "Tens a certeza que queres recarregar este nível de volta ao início?"
@ -322,9 +322,9 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
few_gems: "Algumas gemas"
pile_gems: "Pilha de gemas"
chest_gems: "Arca de gemas"
# purchasing: "Purchasing..."
# declined: "Your card was declined"
# retrying: "Server error, retrying."
purchasing: "A adquirir..."
declined: "O teu cartão foi recusado."
retrying: "Erro do servidor, a tentar novamente."
choose_hero:
choose_hero: "Escolhe o Teu Herói"
@ -351,23 +351,23 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
blocks: "Bloqueia" # As in "this shield blocks this much damage"
skills: "Habilidades"
# skill_docs:
# writable: "writable" # Hover over "attack" in Your Skills while playing a level to see most of this
# read_only: "read-only"
# action_name: "name"
# action_cooldown: "Takes"
# action_specific_cooldown: "Cooldown"
# action_damage: "Damage"
# action_range: "Range"
# action_radius: "Radius"
# action_duration: "Duration"
# example: "Example"
# ex: "ex" # Abbreviation of "example"
# current_value: "Current Value"
# default_value: "Default value"
# parameters: "Parameters"
# returns: "Returns"
# granted_by: "Granted by"
skill_docs:
writable: "escrevível" # Hover over "attack" in Your Skills while playing a level to see most of this
read_only: "apenas leitura"
action_name: "nome"
action_cooldown: "Demora"
action_specific_cooldown: "Tempo de Recarga"
action_damage: "Dano"
action_range: "Alcance"
action_radius: "Raio"
action_duration: "Duração"
example: "Exemplo"
ex: "ex" # Abbreviation of "example"
current_value: "Valor Atual"
default_value: "Valor Predefinido"
parameters: "Parâmetros"
returns: "Devolve"
granted_by: "Garantido por"
save_load:
granularity_saved_games: "Guardados"
@ -769,8 +769,8 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
amount_achieved: "Quantidade"
achievement: "Conquista"
category_contributor: "Contribuidor"
# category_ladder: "Ladder"
# category_level: "Level"
category_ladder: "Classificação"
category_level: "vel"
category_miscellaneous: "Vários"
category_levels: "Níveis"
category_undefined: "Sem Categoria"

View file

@ -19,6 +19,10 @@ module.exports = class ThangType extends CocoModel
librarian: '52fbf74b7e01835453bd8d8e'
'potion-master': '52e9adf7427172ae56002172'
sorcerer: '52fd1524c7e6cf99160e7bc9'
@heroClasses:
Warrior: ['captain', 'knight', 'samurai']
Ranger: ['ninja', 'forest-archer', 'trapper']
Wizard: ['librarian', 'potion-master', 'sorcerer']
@items:
'simple-boots': '53e237bf53457600003e3f05'
urlRoot: '/db/thang.type'
@ -435,7 +439,7 @@ module.exports = class ThangType extends CocoModel
name: name, display: display
isSilhouettedItem: ->
return console.error "Trying to determine whether #{@get('name')} should be a silhouetted item, but it has no gem cost." unless @get 'gems'
return console.error "Trying to determine whether #{@get('name')} should be a silhouetted item, but it has no gem cost." unless @get('gems') or @get('tier')
console.info "Add (or make sure you have fetched) a tier for #{@get('name')} to more accurately determine whether it is silhouetted." unless @get('tier')?
tier = @get 'tier'
if tier?
@ -446,5 +450,8 @@ module.exports = class ThangType extends CocoModel
levelRequiredForItem: ->
return console.error "Trying to determine what level is required for #{@get('name')}, but it has no tier." unless @get('tier')?
tier = @get 'tier'
me.constructor.levelForTier(Math.pow(tier, 0.7))
itemTier = @get 'tier'
playerTier = itemTier / 2.5
playerLevel = me.constructor.levelForTier playerTier
#console.log 'Level required for', @get('name'), 'is', playerLevel, 'player tier', playerTier, 'because it is itemTier', itemTier, 'which is normally level', me.constructor.levelForTier(itemTier)
playerLevel

View file

@ -95,6 +95,13 @@ module.exports = class User extends CocoModel
ownsItem: (itemOriginal) -> itemOriginal in @items()
ownsLevel: (levelOriginal) -> levelOriginal in @levels()
getHeroClasses: ->
idsToSlugs = _.invert ThangType.heroes
myHeroSlugs = (idsToSlugs[id] for id in @heroes())
myHeroClasses = []
myHeroClasses.push heroClass for heroClass, heroSlugs of ThangType.heroClasses when _.intersection(myHeroSlugs, heroSlugs).length
myHeroClasses
getBranchingGroup: ->
return @branchingGroup if @branchingGroup
group = me.get('testGroupNumber') % 4
@ -107,4 +114,14 @@ module.exports = class User extends CocoModel
application.tracker.identify branchingGroup: @branchingGroup unless me.isAdmin()
@branchingGroup
getGemPromptGroup: ->
return @gemPromptGroup if @gemPromptGroup
group = me.get('testGroupNumber') % 8
@gemPromptGroup = switch group
when 0, 1, 2, 3 then 'prompt'
when 4, 5, 6, 7 then 'no-prompt'
@gemPromptGroup = 'prompt' if me.isAdmin()
application.tracker.identify gemPromptGroup: @gemPromptGroup unless me.isAdmin()
@gemPromptGroup
tiersByLevel = [-1, 0, 0.05, 0.14, 0.18, 0.32, 0.41, 0.5, 0.64, 0.82, 0.91, 1.04, 1.22, 1.35, 1.48, 1.65, 1.78, 1.96, 2.1, 2.24, 2.38, 2.55, 2.69, 2.86, 3.03, 3.16, 3.29, 3.42, 3.58, 3.74, 3.89, 4.04, 4.19, 4.32, 4.47, 4.64, 4.79, 4.96]

View file

@ -228,6 +228,9 @@ kbd
border-width: 15px 20px
.arrow
display: none
.btn
font-size: 20px
width: 100%
.btn.btn-illustrated
background: 0
@ -242,6 +245,9 @@ kbd
font-weight: bold
color: rgb(248, 197, 146)
&:hover
color: lighten(rgb(248, 197, 146), 5%)
&:active
border-image: url(/images/common/button-background-pressed-border.png) 14 16 16 20 fill round
padding: 2px 0 0 2px

View file

@ -307,6 +307,13 @@
opacity: 1
color: rgba(255,255,255, 0.4)
// Make sure this shows up above our modals.
.popover.buy-gems-prompt
z-index: 1050
text-align: center
button
margin-top: 20px
//- Use the two-column layout and background image if we are on a narrow screen.

View file

@ -0,0 +1,6 @@
.popover.buy-gems-prompt(role="tooltip")
.arrow
h2(data-i18n="buy_gems.prompt_title") Not Enough Gems
p(data-i18n="buy_gems.prompt_body") Do you want to get more?
button.btn.btn-success.btn-illustrated.btn-lg.buy-gems-prompt-button(data-i18n="buy_gems.prompt_button") Enter Shop

View file

@ -37,7 +37,7 @@ if item && !item.owned
// Temp, while we only have Warriors: prevent them from buying non-Warrior stuff
button.btn.big-font.disabled.unequippable #{item.get('heroClass')} Only
else
button#selected-item-unlock-button.btn.big-font.unlock-button(disabled=!item.affordable, data-item-id=item.id)
button#selected-item-unlock-button.btn.big-font.unlock-button(data-item-id=item.id)
span(data-i18n="play.unlock") Unlock
span.spl= '('+item.get('gems')
img(src="/images/common/gem.png", draggable="false")

View file

@ -53,7 +53,7 @@
// Temp, while we only have Warriors: prevent them from buying non-Warrior stuff
span.big-font.unequippable= item.get('heroClass')
else
button.btn.unlock-button.big-font(data-i18n="play.unlock", disabled=!item.affordable, data-item-id=item.id)
button.btn.unlock-button.big-font(data-i18n="play.unlock", data-item-id=item.id)
.clearfix
#item-details-view

View file

@ -1,5 +1,6 @@
ModalView = require 'views/kinds/ModalView'
template = require 'templates/game-menu/inventory-modal'
buyGemsPromptTemplate = require 'templates/play/modal/buy-gems-prompt'
{me} = require 'lib/auth'
ThangType = require 'models/ThangType'
CocoCollection = require 'collections/CocoCollection'
@ -8,6 +9,7 @@ SpriteBuilder = require 'lib/sprites/SpriteBuilder'
ItemDetailsView = require 'views/play/modal/ItemDetailsView'
Purchase = require 'models/Purchase'
LevelOptions = require 'lib/LevelOptions'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
hasGoneFullScreenOnce = false
@ -31,6 +33,8 @@ module.exports = class InventoryModal extends ModalView
'click #equip-item-viewed': 'onClickEquipItemViewed'
'click #unequip-item-viewed': 'onClickUnequipItemViewed'
'click #close-modal': 'hide'
'click .buy-gems-prompt-button': 'onBuyGemsPromptButtonClicked'
'click': 'onClickedSomewhere'
shortcuts:
'esc': 'clearSelection'
@ -224,40 +228,6 @@ module.exports = class InventoryModal extends ModalView
@selectUnequippedItem(itemEl)
@equipSelectedItem()
onUnlockButtonClicked: (e) ->
button = $(e.target).closest('button')
if button.hasClass('confirm')
item = @items.get($(e.target).data('item-id'))
purchase = Purchase.makeFor(item)
purchase.save()
#- set local changes to mimic what should happen on the server...
purchased = me.get('purchased') ? {}
purchased.items ?= []
purchased.items.push(item.get('original'))
me.set('purchased', purchased)
me.set('spent', (me.get('spent') ? 0) + item.get('gems'))
#- ...then rerender key bits
@itemGroups.lockedItems.remove(item)
# Redo all item sorting to make sure that we don't clobber state changes since last render.
equipped = _.values @getCurrentEquipmentConfig()
@sortItem(item, equipped) for item in @items.models
@renderSelectors('#unequipped', '#gems-count')
@requireLevelEquipment()
@delegateEvents()
@setUpDraggableEventsForAvailableEquipment()
@itemDetailsView.setItem(item)
Backbone.Mediator.publish 'store:item-purchased', item: item, itemSlug: item.get('slug')
else
button.addClass('confirm').text($.i18n.t('play.confirm'))
@$el.one 'click', (e) ->
button.removeClass('confirm').text($.i18n.t('play.unlock')) if e.target isnt button[0]
#- Select/equip higher-level, all encompassing methods the callbacks all use
selectItemSlot: (slotEl) ->
@ -470,7 +440,70 @@ module.exports = class InventoryModal extends ModalView
else
callback?()
#- TODO: DRY this between PlayItemsModal and InventoryModal
onUnlockButtonClicked: (e) ->
e.stopPropagation()
button = $(e.target).closest('button')
item = @items.get(button.data('item-id'))
affordable = item.affordable
if not affordable
@askToBuyGems button
else if button.hasClass('confirm')
purchase = Purchase.makeFor(item)
purchase.save()
#- set local changes to mimic what should happen on the server...
purchased = me.get('purchased') ? {}
purchased.items ?= []
purchased.items.push(item.get('original'))
me.set('purchased', purchased)
me.set('spent', (me.get('spent') ? 0) + item.get('gems'))
#- ...then rerender key bits
@itemGroups.lockedItems.remove(item)
# Redo all item sorting to make sure that we don't clobber state changes since last render.
equipped = _.values @getCurrentEquipmentConfig()
@sortItem(item, equipped) for item in @items.models
@renderSelectors('#unequipped', '#gems-count')
@requireLevelEquipment()
@delegateEvents()
@setUpDraggableEventsForAvailableEquipment()
@itemDetailsView.setItem(item)
Backbone.Mediator.publish 'store:item-purchased', item: item, itemSlug: item.get('slug')
else
button.addClass('confirm').text($.i18n.t('play.confirm'))
@$el.one 'click', (e) ->
button.removeClass('confirm').text($.i18n.t('play.unlock')) if e.target isnt button[0]
askToBuyGems: (unlockButton) ->
if me.getGemPromptGroup() is 'no-prompt'
return @openModalView new BuyGemsModal()
@$el.find('.unlock-button').popover 'destroy'
popoverTemplate = buyGemsPromptTemplate {}
unlockButton.popover(
animation: true
trigger: 'manual'
placement: 'top'
content: ' ' # template has it
container: @$el
template: popoverTemplate
).popover 'show'
popover = unlockButton.data('bs.popover')
popover?.$tip?.i18n()
onBuyGemsPromptButtonClicked: (e) ->
@openModalView new BuyGemsModal()
onClickedSomewhere: (e) ->
return if @destroyed
@$el.find('.unlock-button').popover 'destroy'
destroy: ->
@$el.find('.unlock-button').popover 'destroy'
@stage?.removeAllChildren()
super()

View file

@ -697,7 +697,7 @@ module.exports = class PlayLevelView extends RootView
data = snapshot.val()
if data? and data.id isnt me.id
@realTimeOpponentData = data
console.log 'PlayLevelView onRealTimePlayerAdded opponent', @realTimeOpponentData, @realTimePlayersData
# console.log 'PlayLevelView onRealTimePlayerAdded opponent', @realTimeOpponentData, @realTimePlayersData
@realTimePlayersData[@realTimeOpponentData.id] = @realTimeOpponentData
if @realTimeSessionData?.state is 'creating'
@realTimeSessionRef.update 'state': 'coding'
@ -758,7 +758,7 @@ module.exports = class PlayLevelView extends RootView
# console.error 'Joining real-time multiplayer game with an existing @realTimeSessionRef.'
onRealTimeMultiplayerJoinedGame: (e) ->
console.log 'PlayLevelView onRealTimeMultiplayerJoinedGame', e
# console.log 'PlayLevelView onRealTimeMultiplayerJoinedGame', e
@joinRealTimeMultiplayerGame e
@realTimePlayerGameRef.update 'state': 'coding'
@realTimePlayerRef.update 'state': 'unavailable'
@ -828,7 +828,7 @@ module.exports = class PlayLevelView extends RootView
if me.id is @realTimeSessionData.creator
sessionState = @session.get('state')
if sessionState?
submissionCount = sessionState.submissionCount
submissionCount = sessionState.submissionCount ? 0
console.info 'Setting multiplayer submissionCount to', submissionCount
@realTimeSessionRef.update 'submissionCount': submissionCount
else
@ -849,9 +849,13 @@ module.exports = class PlayLevelView extends RootView
transpiledCode[thang][spellID] = aether.transpile spell
# console.log "PlayLevelView transpiled code", transpiledCode
@session.set 'transpiledCode', transpiledCode
permissions = @session.get 'permissions' ? []
unless _.find(permissions, (p) -> p.target is 'public' and p.access is 'read')
permissions.push target:'public', access:'read'
@session.set 'permissions', permissions
@session.patch()
@realTimePlayerGameRef.update 'state': 'submitted'
console.info 'Other player is', @realTimeOpponentData.state
if @realTimeOpponentData.state in ['submitted', 'ready']
@realTimeOpponentSubmittedCode @realTimeOpponentData, @realTimePlayerGameData
@ -926,7 +930,7 @@ module.exports = class PlayLevelView extends RootView
[newThang, newSpell] = newSpellKey.split '/'
if newSpell is oldSpell
# Found spell location under new team
console.log "Swapping spell=#{oldSpell} from #{oldThang} to #{newThang}"
# console.log "Swapping spell=#{oldSpell} from #{oldThang} to #{newThang}"
if code[newThang]?[oldSpell]?
# Option 1: have a new spell to swap
code[oldThang][oldSpell] = code[newThang][oldSpell]

View file

@ -77,7 +77,7 @@ module.exports = class ProblemAlertView extends CocoView
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'error_appear', volume: 1.0
pauseJiggle = =>
@$el?.removeClass 'jiggling'
_.delay pauseJiggle, 2000
_.delay pauseJiggle, 1000
onHideProblemAlert: ->
@onRemoveClicked()

View file

@ -1,6 +1,8 @@
ModalView = require 'views/kinds/ModalView'
template = require 'templates/play/modal/play-items-modal'
buyGemsPromptTemplate = require 'templates/play/modal/buy-gems-prompt'
ItemDetailsView = require './ItemDetailsView'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
CocoCollection = require 'collections/CocoCollection'
ThangType = require 'models/ThangType'
@ -46,7 +48,9 @@ module.exports = class PlayItemsModal extends ModalView
'click .item': 'onItemClicked'
'shown.bs.tab': 'onTabClicked'
'click .unlock-button': 'onUnlockButtonClicked'
'click .buy-gems-prompt-button': 'onBuyGemsPromptButtonClicked'
'click #close-modal': 'hide'
'click': 'onClickedSomewhere'
constructor: (options) ->
super options
@ -89,7 +93,7 @@ module.exports = class PlayItemsModal extends ModalView
model.affordable = cost <= gemsOwned
model.silhouetted = not model.owned and model.isSilhouettedItem()
model.level = model.levelRequiredForItem() if model.get('tier')?
model.unequippable = not ('Warrior' in model.getAllowedHeroClasses()) # Temp: while there are no wizards/rangers
model.unequippable = not _.intersection(me.getHeroClasses(), model.getAllowedHeroClasses()).length
model.comingSoon = not model.getFrontFacingStats().props.length and not _.size(model.getFrontFacingStats().stats) and not model.owned # Temp: while there are placeholder items
@idToItem[model.id] = model
@ -140,9 +144,14 @@ module.exports = class PlayItemsModal extends ModalView
$($(e.target).attr('href')).find('.nano').nanoScroller({alwaysVisible: true})
onUnlockButtonClicked: (e) ->
e.stopPropagation()
button = $(e.target).closest('button')
if button.hasClass('confirm')
item = @idToItem[$(e.target).data('item-id')]
item = @idToItem[button.data('item-id')]
affordable = item.affordable
if not affordable
@askToBuyGems button
else if button.hasClass('confirm')
purchase = Purchase.makeFor(item)
purchase.save()
@ -163,3 +172,29 @@ module.exports = class PlayItemsModal extends ModalView
button.addClass('confirm').text($.i18n.t('play.confirm'))
@$el.one 'click', (e) ->
button.removeClass('confirm').text($.i18n.t('play.unlock')) if e.target isnt button[0]
askToBuyGems: (unlockButton) ->
if me.getGemPromptGroup() is 'no-prompt'
return @openModalView new BuyGemsModal()
@$el.find('.unlock-button').popover 'destroy'
popoverTemplate = buyGemsPromptTemplate {}
unlockButton.popover(
animation: true
trigger: 'manual'
content: ' ' # template has it
container: @$el
template: popoverTemplate
).popover 'show'
popover = unlockButton.data('bs.popover')
popover?.$tip?.i18n()
onBuyGemsPromptButtonClicked: (e) ->
@openModalView new BuyGemsModal()
onClickedSomewhere: (e) ->
return if @destroyed
@$el.find('.unlock-button').popover 'destroy'
destroy: ->
@$el.find('.unlock-button').popover 'destroy'
super()

View file

@ -5,7 +5,7 @@ if cluster.isMaster
for i in [0...numCPUs]
cluster.fork()
cluster.on 'exit', (worker, code, signal) ->
console.log 'worker' + worker.process.id + 'died'
console.log 'worker ' + worker.id + ' died'
cluster.fork()
else
require('coffee-script')

View file

@ -14,3 +14,15 @@ module.exports.sendHipChatMessage = sendHipChatMessage = (message) ->
request.post {uri: url, json: form}, (err, res, body) ->
return log.error 'Error sending HipChat patch request:', err or body if err or /error/i.test body
#log.info "Got HipChat patch response:", body
module.exports.sendTowerHipChatMessage = sendTowerHipChatMessage = (message) ->
return unless key = config.hipchatTowerAPIKey
roomID = 318356
form =
color: 'red'
notify: true
message: message
messageFormat: 'html'
url = "https://api.hipchat.com/v2/room/#{roomID}/notification?auth_token=#{key}"
request.post {uri: url, json: form}, (err, res, body) ->
return log.error 'Error sending HipChat Tower message:', err or body if err or /error/i.test body

View file

@ -27,7 +27,10 @@ module.exports.setup = (app) ->
request.get {uri: 'https://api.github.com/user', headers: headers}, (err, r, response) ->
return log.error err if err?
githubUser = JSON.parse response
emailLower = githubUser.email.toLowerCase()
log.info 'Got GitHub auth callback response', githubUser
emailLower = githubUser.email?.toLowerCase()
if not emailLower
return errors.serverError res, "Problem finding GitHub user with that identity."
# GitHub users can change emails
User.findOne {$or: [{emailLower: emailLower}, {githubID: githubUser.id}]}, (err, user) ->

View file

@ -48,7 +48,7 @@ config.mail =
cronHandlerPrivateIP: process.env.COCO_CRON_PRIVATE_IP or ''
config.hipchatAPIKey = process.env.COCO_HIPCHAT_API_KEY or ''
config.hipchatTowerAPIKey = process.env.COCO_HIPCHAT_TOWER_API_KEY or ''
config.queue =
accessKeyId: process.env.COCO_AWS_ACCESS_KEY_ID or ''
secretAccessKey: process.env.COCO_AWS_SECRET_ACCESS_KEY or ''

View file

@ -13,6 +13,7 @@ logging = require './server/commons/logging'
config = require './server_config'
auth = require './server/routes/auth'
UserHandler = require './server/users/user_handler'
hipchat = require './server/hipchat'
global.tv4 = require 'tv4' # required for TreemaUtils to work
global.jsondiffpatch = require 'jsondiffpatch'
@ -38,6 +39,14 @@ developmentLogging = (tokens, req, res) ->
elapsedColor = if elapsed < 500 then 90 else 31
"\x1b[90m#{req.method} #{req.originalUrl} \x1b[#{color}m#{res.statusCode} \x1b[#{elapsedColor}m#{elapsed}ms\x1b[0m"
setupErrorMiddleware = (app) ->
app.use (err, req, res, next) ->
if err
res.status(500).send(error: "Something went wrong!")
hipchat.sendTowerHipChatMessage("The server crashed. Stack trace: <br> <code>#{err.stack}</code>")
console.log "Got a server error: #{err.stack}"
else
next(err)
setupExpressMiddleware = (app) ->
if config.isProduction
express.logger.format('prod', productionLogging)
@ -101,6 +110,7 @@ exports.setupMiddleware = (app) ->
setupOneSecondDelayMiddleware app
setupTrailingSlashRemovingMiddleware app
setupRedirectMiddleware app
setupErrorMiddleware app
###Routing function implementations###