mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-31 07:12:49 -04:00
Merge pull request #1150 from rubenvereecken/achievements
Achievements - Update with some tests and admin endpoint
This commit is contained in:
commit
c855327cef
39 changed files with 646 additions and 138 deletions
app
lib
locale
models
schemas/models
styles
templates
views
server
achievements
Achievement.coffeeEarnedAchievement.coffeeachievement_handler.coffeeearned_achievement_handler.coffee
commons
plugins
routes
test
|
@ -18,6 +18,7 @@ doQuerySelector = (value, operatorObj) ->
|
|||
when '$ne' then return false if mapred value, body, (l, r) -> l == r
|
||||
when '$in' then return false unless _.reduce value, ((result, val) -> result or val in body), false
|
||||
when '$nin' then return false if _.reduce value, ((result, val) -> result or val in body), false
|
||||
when '$exists' then return false if value[0]? isnt body[0]
|
||||
else return false
|
||||
true
|
||||
|
||||
|
@ -34,11 +35,13 @@ matchesQuery = (target, queryObj) ->
|
|||
pieces = prop.split('.')
|
||||
obj = target
|
||||
for piece in pieces
|
||||
return false unless piece of obj
|
||||
unless piece of obj
|
||||
obj = null
|
||||
break
|
||||
obj = obj[piece]
|
||||
if typeof query != 'object' or _.isArray query
|
||||
return false unless obj == query or (query in obj if _.isArray obj)
|
||||
else return false unless doQuerySelector obj, query
|
||||
true
|
||||
|
||||
LocalMongo.matchesQuery = matchesQuery
|
||||
LocalMongo.matchesQuery = matchesQuery
|
||||
|
|
|
@ -75,3 +75,25 @@ module.exports.getByPath = (target, path) ->
|
|||
return undefined unless piece of obj
|
||||
obj = obj[piece]
|
||||
obj
|
||||
|
||||
module.exports.round = _.curry (digits, n) ->
|
||||
n = +n.toFixed(digits)
|
||||
|
||||
positify = (func) -> (x) -> if x > 0 then func(x) else 0
|
||||
|
||||
# f(x) = ax + b
|
||||
createLinearFunc = (params) ->
|
||||
(x) -> (params.a or 1) * x + (params.b or 0)
|
||||
|
||||
# f(x) = ax² + bx + c
|
||||
createQuadraticFunc = (params) ->
|
||||
(x) -> (params.a or 1) * x * x + (params.b or 1) * x + (params.c or 0)
|
||||
|
||||
# f(x) = a log(b (x + c)) + d
|
||||
createLogFunc = (params) ->
|
||||
(x) -> if x > 0 then (params.a or 1) * Math.log((params.b or 1) * (x + (params.c or 0))) + (params.d or 0) else 0
|
||||
|
||||
module.exports.functionCreators =
|
||||
linear: positify(createLinearFunc)
|
||||
quadratic: positify(createQuadraticFunc)
|
||||
logarithmic: positify(createLogFunc)
|
||||
|
|
|
@ -507,7 +507,7 @@
|
|||
new_thang_title_login: "Log In to Create a New Thang Type"
|
||||
new_level_title_login: "Log In to Create a New Level"
|
||||
new_achievement_title: "Create a New Achievement"
|
||||
new_achievement_title_login: "Sign Up to Create a New Achievement"
|
||||
new_achievement_title_login: "Log In to Create a New Achievement"
|
||||
article_search_title: "Search Articles Here"
|
||||
thang_search_title: "Search Thang Types Here"
|
||||
level_search_title: "Search Levels Here"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
CocoModel = require './CocoModel'
|
||||
util = require '../lib/utils'
|
||||
|
||||
module.exports = class Achievement extends CocoModel
|
||||
@className: 'Achievement'
|
||||
|
@ -6,4 +7,10 @@ module.exports = class Achievement extends CocoModel
|
|||
urlRoot: '/db/achievement'
|
||||
|
||||
isRepeatable: ->
|
||||
@get('proportionalTo')?
|
||||
@get('proportionalTo')?
|
||||
|
||||
# TODO logic is duplicated in Mongoose Achievement schema
|
||||
getExpFunction: ->
|
||||
kind = @get('function')?.kind or @schema.function.default.kind
|
||||
parameters = @get('function')?.parameters or @schema.function.default.parameters
|
||||
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
|
||||
|
|
|
@ -52,22 +52,17 @@ _.extend(AchievementSchema.properties,
|
|||
description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations'
|
||||
function:
|
||||
type: 'object'
|
||||
oneOf: [
|
||||
linear:
|
||||
properties:
|
||||
kind: {enum: ['linear', 'logarithmic'], default: 'linear'}
|
||||
parameters:
|
||||
type: 'object'
|
||||
properties:
|
||||
a: {type: 'number', default: 1},
|
||||
required: ['a']
|
||||
description: 'f(x) = a * x'
|
||||
logarithmic:
|
||||
type:'object'
|
||||
properties:
|
||||
a: {type: 'number', default: 1}
|
||||
b: {type: 'number', default: 1}
|
||||
required: ['a', 'b']
|
||||
description: 'f(x) = a * ln(1/b * (x + b))'
|
||||
]
|
||||
default: linear: a: 1
|
||||
c: {type: 'number', default: 1}
|
||||
default: {kind: 'linear', parameters: a: 1}
|
||||
required: ['kind', 'parameters']
|
||||
additionalProperties: false
|
||||
)
|
||||
|
||||
AchievementSchema.definitions = {}
|
||||
|
|
|
@ -20,15 +20,11 @@ module.exports =
|
|||
href: '/db/achievement/{($)}'
|
||||
}
|
||||
]
|
||||
collection:
|
||||
type: 'string'
|
||||
achievementName:
|
||||
type: 'string'
|
||||
created:
|
||||
type: 'date'
|
||||
changed:
|
||||
type: 'date'
|
||||
achievedAmount:
|
||||
type: 'number'
|
||||
notified:
|
||||
type: 'boolean'
|
||||
collection: type: 'string'
|
||||
achievementName: type: 'string'
|
||||
created: type: 'date'
|
||||
changed: type: 'date'
|
||||
achievedAmount: type: 'number'
|
||||
earnedPoints: type: 'number'
|
||||
previouslyAchievedAmount: {type: 'number', default: 0}
|
||||
notified: type: 'boolean'
|
||||
|
|
12
app/styles/editor/achievement/edit.sass
Normal file
12
app/styles/editor/achievement/edit.sass
Normal file
|
@ -0,0 +1,12 @@
|
|||
#editor-achievement-edit-view
|
||||
.treema-root
|
||||
margin: 28px 0px 20px
|
||||
|
||||
button
|
||||
float: right
|
||||
margin-top: 15px
|
||||
margin-left: 10px
|
||||
|
||||
textarea
|
||||
width: 92%
|
||||
height: 300px
|
|
@ -45,3 +45,6 @@
|
|||
font-family: Bangers
|
||||
font-size: 16px
|
||||
float: right
|
||||
|
||||
.progress-bar-white
|
||||
background-color: white
|
||||
|
|
|
@ -33,3 +33,11 @@ block content
|
|||
a(href="/admin/base", data-i18n="admin.av_other_debug_base_url") Base (for debugging base.jade)
|
||||
li
|
||||
a(href="/admin/clas", data-i18n="admin.clas") CLAs
|
||||
|
||||
hr
|
||||
|
||||
h3 Achievements
|
||||
p This is just some stuff for temporary achievement testing. Should be replaced by a demo system.
|
||||
|
||||
input#increment-field(type="text")
|
||||
a.btn.btn-secondary#increment-button(href="#") Increment
|
||||
|
|
|
@ -11,19 +11,21 @@ block content
|
|||
li.active
|
||||
| #{achievement.attributes.name}
|
||||
|
||||
button(data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary#save-button Save
|
||||
button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate
|
||||
button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save
|
||||
|
||||
h3(data-i18n="achievement.edit_achievement_title") Edit Achievement
|
||||
span
|
||||
|: "#{achievement.attributes.name}"
|
||||
h3(data-i18n="achievement.edit_achievement_title") Edit Achievement
|
||||
span
|
||||
|: "#{achievement.attributes.name}"
|
||||
|
||||
#achievement-treema
|
||||
#achievement-treema
|
||||
|
||||
#achievement-view
|
||||
#achievement-view
|
||||
|
||||
hr
|
||||
hr
|
||||
|
||||
div#error-view
|
||||
|
||||
div#error-view
|
||||
else
|
||||
.alert.alert-danger
|
||||
span Admin only. Turn around.
|
||||
|
|
|
@ -37,6 +37,7 @@ block content
|
|||
h3(data-i18n="play_level.tip_reticulating") Reticulating Splines...
|
||||
.progress.progress-striped.active
|
||||
.progress-bar
|
||||
|
||||
else
|
||||
.alert.alert-danger
|
||||
span Admin only. Turn around.
|
||||
|
|
11
app/templates/modal/confirm.jade
Normal file
11
app/templates/modal/confirm.jade
Normal file
|
@ -0,0 +1,11 @@
|
|||
extends /templates/modal/modal_base
|
||||
|
||||
block modal-header-content
|
||||
h3 #{confirmTitle}
|
||||
|
||||
block modal-body-content
|
||||
p #{confirmBody}
|
||||
|
||||
block modal-footer-content
|
||||
button.btn.btn-secondary#decline-button(type="button", data-dismiss="modal") #{confirmDecline}
|
||||
button.btn.btn-primary#confirm-button(type="button", data-dismiss=closeOnConfirm === true ? "modal" : undefined) #{confirmConfirm}
|
|
@ -24,4 +24,4 @@
|
|||
block modal-footer
|
||||
.modal-footer
|
||||
block modal-footer-content
|
||||
button.btn.btn-primary(type="button", data-dismiss="modal", aria-hidden="true", data-i18n="modal.okay") Okay
|
||||
button.btn.btn-primary(type="button", data-dismiss="modal", aria-hidden="true", data-i18n="modal.okay") Okay
|
||||
|
|
|
@ -8,6 +8,7 @@ module.exports = class AdminView extends View
|
|||
|
||||
events:
|
||||
'click #enter-espionage-mode': 'enterEspionageMode'
|
||||
'click #increment-button': 'incrementUserAttribute'
|
||||
|
||||
enterEspionageMode: ->
|
||||
userEmail = $("#user-email").val().toLowerCase()
|
||||
|
@ -29,3 +30,8 @@ module.exports = class AdminView extends View
|
|||
|
||||
espionageFailure: (jqxhr, status,error)->
|
||||
console.log "There was an error entering espionage mode: #{error}"
|
||||
|
||||
incrementUserAttribute: (e) ->
|
||||
val = $('#increment-field').val()
|
||||
me.set(val, me.get(val) + 1)
|
||||
me.save()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
View = require 'views/kinds/RootView'
|
||||
template = require 'templates/editor/achievement/edit'
|
||||
Achievement = require 'models/Achievement'
|
||||
ConfirmModal = require 'views/modal/confirm'
|
||||
|
||||
module.exports = class AchievementEditView extends View
|
||||
id: "editor-achievement-edit-view"
|
||||
|
@ -9,6 +10,7 @@ module.exports = class AchievementEditView extends View
|
|||
|
||||
events:
|
||||
'click #save-button': 'saveAchievement'
|
||||
'click #recalculate-button': 'confirmRecalculation'
|
||||
|
||||
subscriptions:
|
||||
'save-new': 'saveAchievement'
|
||||
|
@ -72,3 +74,34 @@ module.exports = class AchievementEditView extends View
|
|||
res.success =>
|
||||
url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}"
|
||||
document.location.href = url
|
||||
|
||||
confirmRecalculation: (e) ->
|
||||
renderData =
|
||||
'confirmTitle': "Are you really sure?"
|
||||
'confirmBody': "This will trigger recalculation of the achievement for all users. Are you really sure you want to go down this path?"
|
||||
'confirmDecline': "Not really"
|
||||
'confirmConfirm': "Definitely"
|
||||
|
||||
confirmModal = new ConfirmModal(renderData)
|
||||
confirmModal.onConfirm @recalculateAchievement
|
||||
@openModalView confirmModal
|
||||
|
||||
recalculateAchievement: =>
|
||||
$.ajax
|
||||
data: JSON.stringify(achievements: [@achievement.get('slug') or @achievement.get('_id')])
|
||||
success: (data, status, jqXHR) ->
|
||||
noty
|
||||
timeout: 5000
|
||||
text: 'Recalculation process started'
|
||||
type: 'success'
|
||||
layout: 'topCenter'
|
||||
error: (jqXHR, status, error) ->
|
||||
console.error jqXHR
|
||||
noty
|
||||
timeout: 5000
|
||||
text: "Starting recalculation process failed with error code #{jqXHR.status}"
|
||||
type: 'error'
|
||||
layout: 'topCenter'
|
||||
url: '/admin/earned.achievement/recalculate'
|
||||
type: 'POST'
|
||||
contentType: 'application/json'
|
||||
|
|
|
@ -8,6 +8,7 @@ locale = require 'locale/locale'
|
|||
|
||||
Achievement = require '../../models/Achievement'
|
||||
User = require '../../models/User'
|
||||
# TODO remove
|
||||
|
||||
filterKeyboardEvents = (allowedEvents, func) ->
|
||||
return (splat...) ->
|
||||
|
@ -25,26 +26,24 @@ module.exports = class RootView extends CocoView
|
|||
subscriptions:
|
||||
'achievements:new': 'handleNewAchievements'
|
||||
|
||||
initialize: ->
|
||||
$ =>
|
||||
# TODO Ruben remove this. Allows for easy testing right now though
|
||||
#test = new Achievement(_id:'537ce4855c91b8d1dda7fda8')
|
||||
#test.fetch(success:@showNewAchievement)
|
||||
|
||||
showNewAchievement: (achievement) ->
|
||||
showNewAchievement: (achievement, earnedAchievement) ->
|
||||
currentLevel = me.level()
|
||||
nextLevel = currentLevel + 1
|
||||
currentLevelExp = User.expForLevel(currentLevel)
|
||||
nextLevelExp = User.expForLevel(nextLevel)
|
||||
totalExpNeeded = nextLevelExp - currentLevelExp
|
||||
expFunction = achievement.getExpFunction()
|
||||
currentExp = me.get('points')
|
||||
worth = achievement.get('worth')
|
||||
leveledUp = currentExp - worth < currentLevelExp
|
||||
alreadyAchievedPercentage = 100 * (currentExp - currentLevelExp - worth) / totalExpNeeded
|
||||
newlyAchievedPercentage = if currentLevelExp is currentExp then 0 else 100 * worth / totalExpNeeded
|
||||
previousExp = currentExp - achievement.get('worth')
|
||||
previousExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable()
|
||||
achievedExp = currentExp - previousExp
|
||||
leveledUp = currentExp - achievedExp < currentLevelExp
|
||||
alreadyAchievedPercentage = 100 * (previousExp - currentLevelExp) / totalExpNeeded
|
||||
newlyAchievedPercentage = if leveledUp then 100 * (currentExp - currentLevelExp) / totalExpNeeded else 100 * achievedExp / totalExpNeeded
|
||||
|
||||
console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelExp} xp)."
|
||||
console.debug "Need a total of #{nextLevelExp - currentLevelExp}, already had #{currentExp - currentLevelExp - worth} and just now earned #{worth} totalling on #{currentExp}"
|
||||
console.debug "Need a total of #{nextLevelExp - currentLevelExp}, already had #{previousExp} and just now earned #{achievedExp} totalling on #{currentExp}"
|
||||
|
||||
alreadyAchievedBar = $("<div class='progress-bar progress-bar-warning' style='width:#{alreadyAchievedPercentage}%'></div>")
|
||||
newlyAchievedBar = $("<div data-toggle='tooltip' class='progress-bar progress-bar-success' style='width:#{newlyAchievedPercentage}%'></div>")
|
||||
|
@ -53,7 +52,7 @@ module.exports = class RootView extends CocoView
|
|||
message = if (currentLevel isnt 1) and leveledUp then "Reached level #{currentLevel}!" else null
|
||||
|
||||
alreadyAchievedBar.tooltip(title: "#{currentExp} XP in total")
|
||||
newlyAchievedBar.tooltip(title: "#{worth} XP earned")
|
||||
newlyAchievedBar.tooltip(title: "#{achievedExp} XP earned")
|
||||
emptyBar.tooltip(title: "#{nextLevelExp - currentExp} XP until level #{nextLevel}")
|
||||
|
||||
# TODO a default should be linked here
|
||||
|
@ -63,7 +62,7 @@ module.exports = class RootView extends CocoView
|
|||
image: $("<img src='#{imageURL}' />")
|
||||
description: achievement.get('description')
|
||||
progressBar: progressBar
|
||||
earnedExp: "+ #{worth} XP"
|
||||
earnedExp: "+ #{achievedExp} XP"
|
||||
message: message
|
||||
|
||||
options =
|
||||
|
@ -77,13 +76,11 @@ module.exports = class RootView extends CocoView
|
|||
$.notify( data, options )
|
||||
|
||||
handleNewAchievements: (earnedAchievements) ->
|
||||
console.debug 'Got new earned achievements'
|
||||
# TODO performance?
|
||||
_.each(earnedAchievements.models, (earnedAchievement) =>
|
||||
achievement = new Achievement(_id: earnedAchievement.get('achievement'))
|
||||
console.log achievement
|
||||
achievement.fetch(
|
||||
success: @showNewAchievement
|
||||
success: (achievement) => @showNewAchievement(achievement, earnedAchievement)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
30
app/views/modal/confirm.coffee
Normal file
30
app/views/modal/confirm.coffee
Normal file
|
@ -0,0 +1,30 @@
|
|||
ModalView = require '../kinds/ModalView'
|
||||
template = require 'templates/modal/confirm'
|
||||
|
||||
module.exports = class ConfirmModal extends ModalView
|
||||
id: "confirm-modal"
|
||||
template: template
|
||||
closeButton: true
|
||||
closeOnConfirm: true
|
||||
|
||||
events:
|
||||
'click #decline-button': 'doDecline'
|
||||
'click #confirm-button': 'doConfirm'
|
||||
|
||||
constructor: (@renderData={}, options={}) ->
|
||||
super(options)
|
||||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
context.closeOnConfirm = @closeOnConfirm
|
||||
_.extend context, @renderData
|
||||
|
||||
setRenderData: (@renderData) ->
|
||||
|
||||
onDecline: (@decline) ->
|
||||
|
||||
onConfirm: (@confirm) ->
|
||||
|
||||
doConfirm: -> @confirm() if @confirm
|
||||
|
||||
doDecline: -> @decline() if @decline
|
|
@ -1,6 +1,8 @@
|
|||
mongoose = require('mongoose')
|
||||
jsonschema = require('../../app/schemas/models/achievement')
|
||||
log = require 'winston'
|
||||
util = require '../../app/lib/utils'
|
||||
plugins = require('../plugins/plugins')
|
||||
|
||||
# `pre` and `post` are not called for update operations executed directly on the database,
|
||||
# including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order
|
||||
|
@ -11,16 +13,21 @@ AchievementSchema = new mongoose.Schema({
|
|||
userField: String
|
||||
}, {strict: false})
|
||||
|
||||
AchievementSchema.methods.objectifyQuery = () ->
|
||||
AchievementSchema.methods.objectifyQuery = ->
|
||||
try
|
||||
@set('query', JSON.parse(@get('query'))) if typeof @get('query') == "string"
|
||||
catch error
|
||||
log.error "Couldn't convert query string to object because of #{error}"
|
||||
@set('query', {})
|
||||
|
||||
AchievementSchema.methods.stringifyQuery = () ->
|
||||
AchievementSchema.methods.stringifyQuery = ->
|
||||
@set('query', JSON.stringify(@get('query'))) if typeof @get('query') != "string"
|
||||
|
||||
getExpFunction: ->
|
||||
kind = @get('function')?.kind or jsonschema.function.default.kind
|
||||
parameters = @get('function')?.parameters or jsonschema.function.default.parameters
|
||||
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
|
||||
|
||||
AchievementSchema.post('init', (doc) -> doc.objectifyQuery())
|
||||
|
||||
AchievementSchema.pre('save', (next) ->
|
||||
|
@ -28,9 +35,11 @@ AchievementSchema.pre('save', (next) ->
|
|||
next()
|
||||
)
|
||||
|
||||
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema)
|
||||
|
||||
plugins = require('../plugins/plugins')
|
||||
|
||||
AchievementSchema.plugin(plugins.NamedPlugin)
|
||||
AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']})
|
||||
|
||||
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema)
|
||||
|
||||
# Reload achievements upon save
|
||||
AchievablePlugin = require '../plugins/achievements'
|
||||
AchievementSchema.post 'save', (doc) -> AchievablePlugin.loadAchievements()
|
||||
|
|
|
@ -13,7 +13,15 @@ EarnedAchievementSchema = new mongoose.Schema({
|
|||
default: false
|
||||
}, {strict:false})
|
||||
|
||||
EarnedAchievementSchema.pre 'save', (next) ->
|
||||
@set('changed', Date.now())
|
||||
next()
|
||||
|
||||
EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'})
|
||||
EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '})
|
||||
|
||||
module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)
|
||||
|
||||
module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ class AchievementHandler extends Handler
|
|||
modelClass: Achievement
|
||||
|
||||
# Used to determine which properties requests may edit
|
||||
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon']
|
||||
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function']
|
||||
jsonSchema = require '../../app/schemas/models/achievement.coffee'
|
||||
|
||||
hasAccess: (req) ->
|
||||
|
|
|
@ -1,12 +1,115 @@
|
|||
mongoose = require('mongoose')
|
||||
log = require 'winston'
|
||||
mongoose = require 'mongoose'
|
||||
async = require 'async'
|
||||
Achievement = require './Achievement'
|
||||
EarnedAchievement = require './EarnedAchievement'
|
||||
User = require '../users/User'
|
||||
Handler = require '../commons/Handler'
|
||||
LocalMongo = require '../../app/lib/LocalMongo'
|
||||
|
||||
class EarnedAchievementHandler extends Handler
|
||||
modelClass: EarnedAchievement
|
||||
|
||||
# Don't allow POSTs or anything yet
|
||||
hasAccess: (req) ->
|
||||
req.method is 'GET'
|
||||
req.method is 'GET' # or req.user.isAdmin()
|
||||
|
||||
recalculate: (req, res) ->
|
||||
onSuccess = (data) => log.debug "Finished recalculating achievements"
|
||||
if 'achievements' of req.body # Support both slugs and IDs separated by commas
|
||||
achievementSlugsOrIDs = req.body.achievements
|
||||
EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess
|
||||
else
|
||||
EarnedAchievementHandler.recalculate onSuccess
|
||||
@sendSuccess res, {}
|
||||
|
||||
# Returns success: boolean
|
||||
# TODO call onFinished
|
||||
@recalculate: (callbackOrSlugsOrIDs, onFinished) ->
|
||||
if _.isArray callbackOrSlugsOrIDs
|
||||
achievementSlugs = (thing for thing in callbackOrSlugsOrIDs when not Handler.isID(thing))
|
||||
achievementIDs = (thing for thing in callbackOrSlugsOrIDs when Handler.isID(thing))
|
||||
else
|
||||
onFinished = callbackOrSlugsOrIDs
|
||||
|
||||
filter = {}
|
||||
filter.$or = [
|
||||
{_id: $in: achievementIDs},
|
||||
{slug: $in: achievementSlugs}
|
||||
] if achievementSlugs? or achievementIDs?
|
||||
|
||||
# Fetch all relevant achievements
|
||||
Achievement.find filter, (err, achievements) ->
|
||||
return log.error err if err?
|
||||
|
||||
# Fetch every single user
|
||||
User.find {}, (err, users) ->
|
||||
_.each users, (user) ->
|
||||
# Keep track of a user's already achieved in order to set the notified values correctly
|
||||
userID = user.get('_id').toHexString()
|
||||
|
||||
# Fetch all of a user's earned achievements
|
||||
EarnedAchievement.find {user: userID}, (err, alreadyEarned) ->
|
||||
alreadyEarnedIDs = []
|
||||
previousPoints = 0
|
||||
_.each alreadyEarned, (earned) ->
|
||||
if (_.find achievements, (single) -> earned.get('achievement') is single.get('_id').toHexString())
|
||||
alreadyEarnedIDs.push earned.get('achievement')
|
||||
previousPoints += earned.get 'earnedPoints'
|
||||
|
||||
# TODO maybe also delete earned? Make sure you don't delete too many
|
||||
|
||||
newTotalPoints = 0
|
||||
|
||||
earnedAchievementSaverGenerator = (achievement) -> (callback) ->
|
||||
isRepeatable = achievement.get('proportionalTo')?
|
||||
model = mongoose.model(achievement.get('collection'))
|
||||
if not model?
|
||||
log.error "Model #{achievement.get 'collection'} doesn't even exist."
|
||||
return callback()
|
||||
|
||||
model.findOne achievement.query, (err, something) ->
|
||||
return callback() unless something
|
||||
|
||||
log.debug "Matched an achievement: #{achievement.get 'name'}"
|
||||
|
||||
earned =
|
||||
user: userID
|
||||
achievement: achievement._id.toHexString()
|
||||
achievementName: achievement.get 'name'
|
||||
notified: achievement._id in alreadyEarnedIDs
|
||||
|
||||
if isRepeatable
|
||||
earned.achievedAmount = something.get(achievement.get 'proportionalTo')
|
||||
earned.previouslyAchievedAmount = 0
|
||||
|
||||
expFunction = achievement.getExpFunction()
|
||||
newPoints = expFunction(earned.achievedAmount) * achievement.get('worth')
|
||||
else
|
||||
newPoints = achievement.get 'worth'
|
||||
|
||||
earned.earnedPoints = newPoints
|
||||
newTotalPoints += newPoints
|
||||
|
||||
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
||||
log.error err if err?
|
||||
callback()
|
||||
|
||||
saveUserPoints = (callback) ->
|
||||
# In principle it is enough to deduct the old amount of points and add the new amount,
|
||||
# but just to be entirely safe let's start from 0 in case we're updating all of a user's achievements
|
||||
log.debug "Matched a total of #{newTotalPoints} new points"
|
||||
if _.isEmpty filter # Completely clean
|
||||
User.update {_id: userID}, {$set: points: newTotalPoints}, {}, (err) -> log.error err if err?
|
||||
else
|
||||
log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}"
|
||||
User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) -> log.error err if err?
|
||||
|
||||
earnedAchievementSavers = (earnedAchievementSaverGenerator(achievement) for achievement in achievements)
|
||||
earnedAchievementSavers.push saveUserPoints
|
||||
|
||||
# We need to have all these database updates chained so we know the final score
|
||||
async.series earnedAchievementSavers
|
||||
|
||||
|
||||
module.exports = new EarnedAchievementHandler()
|
||||
|
|
|
@ -17,6 +17,7 @@ module.exports = class Handler
|
|||
postEditableProperties: []
|
||||
jsonSchema: {}
|
||||
waterfallFunctions: []
|
||||
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH']
|
||||
|
||||
# subclasses should override these methods
|
||||
hasAccess: (req) -> true
|
||||
|
@ -420,3 +421,7 @@ module.exports = class Handler
|
|||
dict[document.id] = document
|
||||
res.send dict
|
||||
res.end()
|
||||
|
||||
delete: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, "DELETE not allowed."
|
||||
|
||||
head: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, "HEAD not allowed."
|
||||
|
|
|
@ -17,8 +17,9 @@ module.exports.notFound = (res, message='Not found.') ->
|
|||
res.send 404, message
|
||||
res.end()
|
||||
|
||||
module.exports.badMethod = (res, message='Method Not Allowed') ->
|
||||
# TODO: The response MUST include an Allow header containing a list of valid methods for the requested resource
|
||||
module.exports.badMethod = (res, allowed=['GET', 'POST', 'PUT', 'PATCH'], message='Method Not Allowed') ->
|
||||
allowHeader = _.reduce allowed, ((str, current) -> str += ', ' + current)
|
||||
res.set 'Allow', allowHeader # TODO not sure if these are always the case
|
||||
res.send 405, message
|
||||
res.end()
|
||||
|
||||
|
@ -40,4 +41,4 @@ module.exports.gatewayTimeoutError = (res, message="Gateway timeout") ->
|
|||
|
||||
module.exports.clientTimeout = (res, message="The server did not recieve the client response in a timely manner") ->
|
||||
res.send 408, message
|
||||
res.end()
|
||||
res.end()
|
||||
|
|
|
@ -14,6 +14,7 @@ module.exports.handlers =
|
|||
|
||||
module.exports.routes =
|
||||
[
|
||||
'routes/admin'
|
||||
'routes/auth'
|
||||
'routes/contact'
|
||||
'routes/db'
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
mongoose = require('mongoose')
|
||||
Achievement = require('../achievements/Achievement')
|
||||
EarnedAchievement = require '../achievements/EarnedAchievement'
|
||||
LocalMongo = require '../../app/lib/LocalMongo'
|
||||
util = require '../../app/lib/utils'
|
||||
|
@ -7,18 +6,9 @@ log = require 'winston'
|
|||
|
||||
achievements = {}
|
||||
|
||||
loadAchievements = ->
|
||||
achievements = {}
|
||||
query = Achievement.find({})
|
||||
query.exec (err, docs) ->
|
||||
_.each docs, (achievement) ->
|
||||
category = achievement.get 'collection'
|
||||
achievements[category] = [] unless category of achievements
|
||||
achievements[category].push achievement
|
||||
loadAchievements()
|
||||
|
||||
module.exports = AchievablePlugin = (schema, options) ->
|
||||
User = require '../users/User'
|
||||
User = require '../users/User' # Avoid mutual inclusion cycles
|
||||
Achievement = require('../achievements/Achievement')
|
||||
|
||||
checkForAchievement = (doc) ->
|
||||
collectionName = doc.constructor.modelName
|
||||
|
@ -54,6 +44,8 @@ module.exports = AchievablePlugin = (schema, options) ->
|
|||
achievement: achievement._id.toHexString()
|
||||
achievementName: achievement.get 'name'
|
||||
}
|
||||
|
||||
worth = achievement.get('worth')
|
||||
earnedPoints = 0
|
||||
wrapUp = ->
|
||||
# Update user's experience points
|
||||
|
@ -68,23 +60,37 @@ module.exports = AchievablePlugin = (schema, options) ->
|
|||
newAmount = docObj[proportionalTo]
|
||||
|
||||
if originalAmount isnt newAmount
|
||||
expFunction = achievement.getExpFunction()
|
||||
earned.notified = false
|
||||
earned.achievedAmount = newAmount
|
||||
earned.changed = Date.now()
|
||||
EarnedAchievement.findOneAndUpdate({achievement:earned.achievement, user:earned.user}, earned, upsert:true, (err, docs) ->
|
||||
return log.debug err if err?
|
||||
)
|
||||
earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth
|
||||
earned.previouslyAchievedAmount = originalAmount
|
||||
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
||||
return log.debug err if err?
|
||||
|
||||
earnedPoints = achievement.get('worth') * (newAmount - originalAmount)
|
||||
earnedPoints = earned.earnedPoints
|
||||
log.debug earnedPoints
|
||||
wrapUp()
|
||||
|
||||
else # not alreadyAchieved
|
||||
log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
||||
earned.earnedPoints = worth
|
||||
(new EarnedAchievement(earned)).save (err, doc) ->
|
||||
return log.debug err if err?
|
||||
|
||||
earnedPoints = achievement.get('worth')
|
||||
earnedPoints = worth
|
||||
wrapUp()
|
||||
|
||||
delete before[doc.id] unless isNew # This assumes everything we patch has a _id
|
||||
return
|
||||
|
||||
module.exports.loadAchievements = ->
|
||||
achievements = {}
|
||||
Achievement = require('../achievements/Achievement')
|
||||
query = Achievement.find({})
|
||||
query.exec (err, docs) ->
|
||||
_.each docs, (achievement) ->
|
||||
category = achievement.get 'collection'
|
||||
achievements[category] = [] unless category of achievements
|
||||
achievements[category].push achievement
|
||||
|
||||
AchievablePlugin.loadAchievements()
|
||||
|
|
27
server/routes/admin.coffee
Normal file
27
server/routes/admin.coffee
Normal file
|
@ -0,0 +1,27 @@
|
|||
log = require 'winston'
|
||||
errors = require '../commons/errors'
|
||||
handlers = require('../commons/mapping').handlers
|
||||
|
||||
mongoose = require('mongoose')
|
||||
|
||||
module.exports.setup = (app) ->
|
||||
app.post '/admin/*', (req, res) ->
|
||||
# TODO apparently I can leave this out as long as I use res.send
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
|
||||
module = req.path[7..]
|
||||
parts = module.split('/')
|
||||
module = parts[0]
|
||||
|
||||
return errors.unauthorized(res, 'Must be admin to access this area.') unless req.user?.isAdmin()
|
||||
|
||||
try
|
||||
moduleName = module.replace '.', '_'
|
||||
name = handlers[moduleName]
|
||||
handler = require('../' + name)
|
||||
|
||||
return handler[parts[1]](req, res, parts[2..]...) if parts[1] of handler
|
||||
|
||||
catch error
|
||||
log.error("Error trying db method '#{req.route.method}' route '#{parts}' from #{name}: #{error}")
|
||||
errors.notFound(res, "Route #{req.path} not found.")
|
|
@ -8,7 +8,7 @@ module.exports.setup = (app) ->
|
|||
app.all '/file*', (req, res) ->
|
||||
return fileGet(req, res) if req.route.method is 'get'
|
||||
return filePost(req, res) if req.route.method is 'post'
|
||||
return errors.badMethod(res)
|
||||
return errors.badMethod(res, ['GET', 'POST'])
|
||||
|
||||
|
||||
fileGet = (req, res) ->
|
||||
|
|
|
@ -4,7 +4,7 @@ errors = require '../commons/errors'
|
|||
module.exports.setup = (app) ->
|
||||
app.all '/folder*', (req, res) ->
|
||||
return folderGet(req, res) if req.route.method is 'get'
|
||||
return errors.badMethod(res)
|
||||
return errors.badMethod(res, ['GET'])
|
||||
|
||||
folderGet = (req, res) ->
|
||||
folder = req.path[7..]
|
||||
|
@ -15,4 +15,4 @@ folderGet = (req, res) ->
|
|||
mongoose.connection.db.collection 'media.files', (errors, collection) ->
|
||||
collection.find({'metadata.path': folder}).toArray (err, results) ->
|
||||
res.send(results)
|
||||
res.end()
|
||||
res.end()
|
||||
|
|
|
@ -11,7 +11,7 @@ module.exports.setup = (app) ->
|
|||
|
||||
app.all '/languages', (req, res) ->
|
||||
# Now that these are in the client, not sure when we would use this, but hey
|
||||
return errors.badMethod(res) if req.route.method isnt 'get'
|
||||
return errors.badMethod(res, ['GET']) if req.route.method isnt 'get'
|
||||
res.send(languages)
|
||||
return res.end()
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ module.exports.setup = (app) ->
|
|||
|
||||
app.all '/queue/*', (req, res) ->
|
||||
setResponseHeaderToJSONContentType res
|
||||
|
||||
|
||||
queueName = getQueueNameFromPath req.path
|
||||
try
|
||||
handler = loadQueueHandler queueName
|
||||
|
@ -64,7 +64,7 @@ isHTTPMethodPost = (req) -> return req.route.method is 'post'
|
|||
isHTTPMethodPut = (req) -> return req.route.method is 'put'
|
||||
|
||||
|
||||
sendMethodNotSupportedError = (req, res) -> errors.badMethod(res,"Queues do not support the HTTP method used." )
|
||||
sendMethodNotSupportedError = (req, res) -> errors.badMethod(res, ['GET', 'POST', 'PUT'], "Queues do not support the HTTP method used." )
|
||||
|
||||
sendQueueError = (req,res, error) -> errors.serverError(res, "Route #{req.path} had a problem: #{error}")
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ describe 'Local Mongo queries', ->
|
|||
LocalMongo = require 'lib/LocalMongo'
|
||||
|
||||
beforeEach ->
|
||||
this.fixture1 =
|
||||
@fixture1 =
|
||||
'id': 'somestring'
|
||||
'value': 9000
|
||||
'levels': [3, 8, 21]
|
||||
|
@ -10,68 +10,72 @@ describe 'Local Mongo queries', ->
|
|||
'type': 'unicorn'
|
||||
'likes': ['poptarts', 'popsicles', 'popcorn']
|
||||
|
||||
this.fixture2 = this: is: so: 'deep'
|
||||
@fixture2 = this: is: so: 'deep'
|
||||
|
||||
it 'regular match of a property', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'gender': 'unicorn')).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type':'unicorn')).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type':'zebra')).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type':'unicorn', 'id':'somestring')).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'gender': 'unicorn')).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'type':'unicorn')).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'type':'zebra')).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'type':'unicorn', 'id':'somestring')).toBeTruthy()
|
||||
|
||||
it 'array match of a property', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes':'poptarts')).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes':'walks on the beach')).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'likes':'poptarts')).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'likes':'walks on the beach')).toBeFalsy()
|
||||
|
||||
it 'nested match', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture2, 'this.is.so':'deep')).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture2, 'this.is.so':'deep')).toBeTruthy()
|
||||
|
||||
it '$gt selector', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': 8000)).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': [8000, 10000])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$gt': [10, 20, 30])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': 9000)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$gt': 8000}, 'worth': {'$gt': 5})).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 8000)).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': [8000, 10000])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$gt': [10, 20, 30])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 9000)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$gt': 8000}, 'worth': {'$gt': 5})).toBeTruthy()
|
||||
|
||||
it '$gte selector', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': 9001)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': 9000)).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': [9000, 10000])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$gte': [21, 30])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': 9001)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': 9000)).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': [9000, 10000])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$gte': [21, 30])).toBeTruthy()
|
||||
|
||||
it '$lt selector', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': 9001)).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': 9000)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': [9001, 9000])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$lt': [10, 20, 30])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$lt': 9001}, 'worth': {'$lt': 7})).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': 9001)).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': 9000)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': [9001, 9000])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$lt': [10, 20, 30])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$lt': 9001}, 'worth': {'$lt': 7})).toBeTruthy()
|
||||
|
||||
it '$lte selector', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lte': 9000)).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lte': 8000)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$lte': 9000}, 'worth': {'$lte': [6, 5]})).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lte': 9000)).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lte': 8000)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$lte': 9000}, 'worth': {'$lte': [6, 5]})).toBeTruthy()
|
||||
|
||||
it '$ne selector', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$ne': 9000)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'id': '$ne': 'otherstring')).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'id': '$ne': ['otherstring', 'somestring'])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$ne': ['popcorn', 'chicken'])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$ne': 9000)).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'id': '$ne': 'otherstring')).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'id': '$ne': ['otherstring', 'somestring'])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$ne': ['popcorn', 'chicken'])).toBeFalsy()
|
||||
|
||||
it '$in selector', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$in': ['unicorn', 'zebra'])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$in': ['cats', 'dogs'])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$in': ['popcorn', 'chicken'])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'type': '$in': ['unicorn', 'zebra'])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'type': '$in': ['cats', 'dogs'])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$in': ['popcorn', 'chicken'])).toBeTruthy()
|
||||
|
||||
it '$nin selector', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$nin': ['unicorn', 'zebra'])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$nin': ['cats', 'dogs'])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$nin': ['popcorn', 'chicken'])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'type': '$nin': ['unicorn', 'zebra'])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'type': '$nin': ['cats', 'dogs'])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$nin': ['popcorn', 'chicken'])).toBeFalsy()
|
||||
|
||||
it '$or operator', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, $or: [{value:9000}, {type:'zebra'}])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, $or: [{value:9001}, {worth:$lt:10}])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, $or: [{value:9000}, {type:'zebra'}])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, $or: [{value:9001}, {worth:$lt:10}])).toBeTruthy()
|
||||
|
||||
it '$and operator', ->
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:9000}, {type:'zebra'}])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:9000}, {type:'unicorn'}])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:$gte:9000}, {worth:$lt:10}])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:9000}, {type:'zebra'}])).toBeFalsy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:9000}, {type:'unicorn'}])).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:$gte:9000}, {worth:$lt:10}])).toBeTruthy()
|
||||
|
||||
it '$exists operator', ->
|
||||
expect(LocalMongo.matchesQuery(@fixture1, type: $exists: true)).toBeTruthy()
|
||||
expect(LocalMongo.matchesQuery(@fixture1, interesting: $exists: false)).toBeTruthy()
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@ models_path = [
|
|||
'../../server/levels/thangs/LevelThangType'
|
||||
'../../server/users/User'
|
||||
'../../server/patches/Patch'
|
||||
'../../server/achievements/Achievement'
|
||||
'../../server/achievements/EarnedAchievement'
|
||||
]
|
||||
|
||||
for m in models_path
|
||||
|
@ -162,4 +164,4 @@ tick = ->
|
|||
mongoose.disconnect()
|
||||
clearTimeout tickInterval
|
||||
|
||||
tickInterval = setInterval tick, 1000
|
||||
tickInterval = setInterval tick, 1000
|
||||
|
|
110
test/server/functional/achievement.spec.coffee
Normal file
110
test/server/functional/achievement.spec.coffee
Normal file
|
@ -0,0 +1,110 @@
|
|||
require '../common'
|
||||
|
||||
unlockable =
|
||||
name: 'Dungeon Arena Started'
|
||||
description: 'Started playing Dungeon Arena.'
|
||||
worth: 3
|
||||
collection: 'level.session'
|
||||
query: "{\"level.original\":\"dungeon-arena\"}"
|
||||
userField: 'creator'
|
||||
|
||||
repeatable =
|
||||
name: 'Simulated'
|
||||
description: 'Simulated Games.'
|
||||
worth: 1
|
||||
collection: 'User'
|
||||
query: "{\"simulatedBy\":{\"$gt\":\"0\"}}"
|
||||
userField: '_id'
|
||||
proportionalTo: 'simulatedBy'
|
||||
|
||||
url = getURL('/db/achievement')
|
||||
|
||||
describe 'Achievement', ->
|
||||
allowHeader = 'GET, POST, PUT, PATCH'
|
||||
|
||||
it 'preparing test: deleting all Achievements first', (done) ->
|
||||
clearModels [Achievement, EarnedAchievement, LevelSession, User], (err) ->
|
||||
expect(err).toBeNull()
|
||||
done()
|
||||
|
||||
it 'can\'t be created by ordinary users', (done) ->
|
||||
loginJoe ->
|
||||
request.post {uri: url, json: unlockable}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'can\'t be updated by ordinary users', (done) ->
|
||||
loginJoe ->
|
||||
request.put {uri: url, json:unlockable}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
|
||||
request {method: 'patch', uri: url, json: unlockable}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'can be created by admins', (done) ->
|
||||
loginAdmin ->
|
||||
request.post {uri: url, json: unlockable}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
unlockable._id = body._id
|
||||
|
||||
request.post {uri: url, json: repeatable}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
repeatable._id = body._id
|
||||
done()
|
||||
|
||||
it 'can get all for ordinary users', (done) ->
|
||||
loginJoe ->
|
||||
request.get {uri: url, json: unlockable}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(2)
|
||||
done()
|
||||
|
||||
it 'can be read by ordinary users', (done) ->
|
||||
loginJoe ->
|
||||
request.get {uri: url + '/' + unlockable._id, json: unlockable}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.name).toBe(unlockable.name)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP HEAD method', (done) ->
|
||||
loginJoe ->
|
||||
request.head {uri: url + '/' + unlockable._id}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP DEL method', (done) ->
|
||||
loginJoe ->
|
||||
request.del {uri: url + '/' + unlockable._id}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'get schema', (done) ->
|
||||
request.get {uri:url + '/schema'}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
body = JSON.parse(body)
|
||||
expect(body.type).toBeDefined()
|
||||
done()
|
||||
|
||||
|
||||
describe 'Achieving Achievements', ->
|
||||
|
||||
it 'allows users to unlock one-time Achievements', (done) ->
|
||||
loginJoe (joe) ->
|
||||
levelSession =
|
||||
creator: joe._id
|
||||
level: original: 'dungeon-arena'
|
||||
|
||||
request.post {uri:getURL('/db/level.session'), json:levelSession}, (session) ->
|
||||
|
||||
done()
|
||||
|
||||
|
||||
xit 'cleaning up test: deleting all Achievements and relates', (done) ->
|
||||
clearModels [Achievement, EarnedAchievement, LevelSession], (err) ->
|
||||
expect(err).toBeNull()
|
||||
done()
|
||||
|
||||
|
|
@ -28,6 +28,8 @@ xdescribe '/file', ->
|
|||
my_buffer_url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif'
|
||||
}
|
||||
|
||||
allowHeader = 'GET, POST'
|
||||
|
||||
it 'preparing test : deletes all the files first', (done) ->
|
||||
dropGridFS ->
|
||||
done()
|
||||
|
@ -147,19 +149,28 @@ xdescribe '/file', ->
|
|||
|
||||
request.post(options, func)
|
||||
|
||||
it ' can\'t be requested with HTTP PATCH method', (done) ->
|
||||
request {method: 'patch', uri:url}, (err, res) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it ' can\'t be requested with HTTP PUT method', (done) ->
|
||||
request.put {uri:url}, (err, res) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it ' can\'t be requested with HTTP HEAD method', (done) ->
|
||||
request.head {uri:url}, (err, res) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it ' can\'t be requested with HTTP DEL method', (done) ->
|
||||
request.del {uri:url}, (err, res) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
# TODO: test server errors, see what they do
|
||||
|
|
35
test/server/functional/folder.spec.coffee
Normal file
35
test/server/functional/folder.spec.coffee
Normal file
|
@ -0,0 +1,35 @@
|
|||
require '../common'
|
||||
|
||||
describe 'folder', ->
|
||||
url = getURL('/folder')
|
||||
allowHeader = 'GET'
|
||||
|
||||
it 'can\'t be requested with HTTP POST method', (done) ->
|
||||
request.post {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP PUT method', (done) ->
|
||||
request.put {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP PATCH method', (done) ->
|
||||
request {method:'patch', uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP HEAD method', (done) ->
|
||||
request.head {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP DELETE method', (done) ->
|
||||
request.del {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
35
test/server/functional/languages.spec.coffee
Normal file
35
test/server/functional/languages.spec.coffee
Normal file
|
@ -0,0 +1,35 @@
|
|||
require '../common'
|
||||
|
||||
describe 'languages', ->
|
||||
url = getURL('/languages')
|
||||
allowHeader = 'GET'
|
||||
|
||||
it 'can\'t be requested with HTTP POST method', (done) ->
|
||||
request.post {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP PUT method', (done) ->
|
||||
request.put {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP PATCH method', (done) ->
|
||||
request {method:'patch', uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP HEAD method', (done) ->
|
||||
request.head {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP DELETE method', (done) ->
|
||||
request.del {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
|
@ -130,17 +130,17 @@ describe 'LevelComponent', ->
|
|||
|
||||
xit ' can\'t be requested with HTTP PUT method', (done) ->
|
||||
request.put {uri:url+'/'+components[0]._id}, (err, res) ->
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(res.statusCode).toBe(405)
|
||||
done()
|
||||
|
||||
it ' can\'t be requested with HTTP HEAD method', (done) ->
|
||||
request.head {uri:url+'/'+components[0]._id}, (err, res) ->
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(res.statusCode).toBe(405)
|
||||
done()
|
||||
|
||||
it ' can\'t be requested with HTTP DEL method', (done) ->
|
||||
request.del {uri:url+'/'+components[0]._id}, (err, res) ->
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(res.statusCode).toBe(405)
|
||||
done()
|
||||
|
||||
it 'get schema', (done) ->
|
||||
|
|
|
@ -123,12 +123,12 @@ describe 'LevelSystem', ->
|
|||
|
||||
it ' can\'t be requested with HTTP HEAD method', (done) ->
|
||||
request.head {uri:url+'/'+systems[0]._id}, (err, res) ->
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(res.statusCode).toBe(405)
|
||||
done()
|
||||
|
||||
it ' can\'t be requested with HTTP DEL method', (done) ->
|
||||
request.del {uri:url+'/'+systems[0]._id}, (err, res) ->
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(res.statusCode).toBe(405)
|
||||
done()
|
||||
|
||||
it 'get schema', (done) ->
|
||||
|
|
25
test/server/functional/queue.spec.coffee
Normal file
25
test/server/functional/queue.spec.coffee
Normal file
|
@ -0,0 +1,25 @@
|
|||
require '../common'
|
||||
|
||||
describe 'queue', ->
|
||||
someURL = getURL('/queue/')
|
||||
allowHeader = 'GET, POST, PUT'
|
||||
|
||||
xit 'can\'t be requested with HTTP PATCH method', (done) ->
|
||||
request {method:'patch', uri: someURL}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
xit 'can\'t be requested with HTTP HEAD method', (done) ->
|
||||
request.head {uri: someURL}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
xit 'can\'t be requested with HTTP DELETE method', (done) ->
|
||||
request.del {uri: someURL}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
|
Loading…
Add table
Reference in a new issue