mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-31 07:12:49 -04:00
Merge branch 'rubenvereecken-achievements_production'
This commit is contained in:
commit
f114d71d9d
36 changed files with 837 additions and 56 deletions
achievement_fixtures.js
app
server
achievements
Achievement.coffeeEarnedAchievement.coffeeachievement_handler.coffeeearned_achievement_handler.coffee
commons
levels/sessions
plugins
users
test/app/lib
12
achievement_fixtures.js
Normal file
12
achievement_fixtures.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Fixtures
|
||||
|
||||
db.achievements.insert({
|
||||
query: '{"level.original": "52d97ecd32362bc86e004e87"}',
|
||||
index: true,
|
||||
slug: 'dungeon-arena-started',
|
||||
name: 'Dungeon Arena started',
|
||||
worth: 1,
|
||||
collection: 'level.session',
|
||||
description: 'Started playing Dungeon Arena.',
|
||||
userField: 'creator'
|
||||
});
|
|
@ -5,6 +5,7 @@ locale = require 'locale/locale'
|
|||
{me} = require 'lib/auth'
|
||||
Tracker = require 'lib/Tracker'
|
||||
CocoView = require 'views/kinds/CocoView'
|
||||
AchievementNotify = require '../../templates/achievement_notify'
|
||||
|
||||
marked.setOptions {gfm: true, sanitize: true, smartLists: true, breaks: false}
|
||||
|
||||
|
@ -39,6 +40,7 @@ Application = initialize: ->
|
|||
@facebookHandler = new FacebookHandler()
|
||||
@gplusHandler = new GPlusHandler()
|
||||
$(document).bind 'keydown', preventBackspace
|
||||
$.notify.addStyle 'achievement', html: $(AchievementNotify())
|
||||
@linkedinHandler = new LinkedInHandler()
|
||||
preload(COMMON_FILES)
|
||||
$.i18n.init {
|
||||
|
|
7
app/collections/NewAchievementCollection.coffee
Normal file
7
app/collections/NewAchievementCollection.coffee
Normal file
|
@ -0,0 +1,7 @@
|
|||
CocoCollection = require 'collections/CocoCollection'
|
||||
|
||||
class NewAchievementCollection extends CocoCollection
|
||||
initialize: (me = require('lib/auth').me) ->
|
||||
@url = "/db/user/#{me.id}/achievements?notified=false"
|
||||
|
||||
module.exports = NewAchievementCollection
|
|
@ -59,4 +59,4 @@ setUpChannels = ->
|
|||
|
||||
setUpDefinitions = ->
|
||||
for definition of definitionSchemas
|
||||
Backbone.Mediator.addDefSchemas definitionSchemas[definition]
|
||||
Backbone.Mediator.addDefSchemas definitionSchemas[definition]
|
||||
|
|
44
app/lib/LocalMongo.coffee
Normal file
44
app/lib/LocalMongo.coffee
Normal file
|
@ -0,0 +1,44 @@
|
|||
LocalMongo = module.exports
|
||||
|
||||
# Checks whether func(l, r) is true for at least one value of left for at least one value of right
|
||||
mapred = (left, right, func) ->
|
||||
_.reduce(left, ((result, singleLeft) ->
|
||||
result or (_.reduce (_.map right, (singleRight) -> func(singleLeft, singleRight)),
|
||||
((intermediate, value) -> intermediate or value), false)), false)
|
||||
|
||||
doQuerySelector = (value, operatorObj) ->
|
||||
value = [value] unless _.isArray value # left hand can be an array too
|
||||
for operator, body of operatorObj
|
||||
body = [body] unless _.isArray body # right hand can be an array too
|
||||
switch operator
|
||||
when '$gt' then return false unless mapred value, body, (l, r) -> l > r
|
||||
when '$gte' then return false unless mapred value, body, (l, r) -> l >= r
|
||||
when '$lt' then return false unless mapred value, body, (l, r) -> l < r
|
||||
when '$lte' then return false unless mapred value, body, (l, r) -> l <= r
|
||||
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
|
||||
else return false
|
||||
true
|
||||
|
||||
matchesQuery = (target, queryObj) ->
|
||||
return true unless queryObj
|
||||
for prop, query of queryObj
|
||||
if prop[0] == '$'
|
||||
switch prop
|
||||
when '$or' then return false unless _.reduce query, ((res, obj) -> res or matchesQuery target, obj), false
|
||||
when '$and' then return false unless _.reduce query, ((res, obj) -> res and matchesQuery target, obj), true
|
||||
else return false
|
||||
else
|
||||
# Do nested properties
|
||||
pieces = prop.split('.')
|
||||
obj = target
|
||||
for piece in pieces
|
||||
return false unless piece of obj
|
||||
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
|
|
@ -67,3 +67,11 @@ module.exports.i18n = (say, target, language=me.lang(), fallback='en') ->
|
|||
return fallbackResult if fallbackResult?
|
||||
return say[target] if target of say
|
||||
null
|
||||
|
||||
module.exports.getByPath = (target, path) ->
|
||||
pieces = path.split('.')
|
||||
obj = target
|
||||
for piece in pieces
|
||||
return undefined unless piece of obj
|
||||
obj = obj[piece]
|
||||
obj
|
||||
|
|
|
@ -458,6 +458,7 @@
|
|||
thang_description: "Build units, defining their default logic, graphics and audio. Currently only supports importing Flash exported vector graphics."
|
||||
level_title: "Level Editor"
|
||||
level_description: "Includes the tools for scripting, uploading audio, and constructing custom logic to create all sorts of levels. Everything we use ourselves!"
|
||||
achievement_title: "Achievement Editor"
|
||||
got_questions: "Questions about using the CodeCombat editors?"
|
||||
contact_us: "Contact us!"
|
||||
hipchat_prefix: "You can also find us in our"
|
||||
|
@ -503,9 +504,12 @@
|
|||
new_article_title_login: "Log In to Create a New Article"
|
||||
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"
|
||||
article_search_title: "Search Articles Here"
|
||||
thang_search_title: "Search Thang Types Here"
|
||||
level_search_title: "Search Levels Here"
|
||||
achievement_search_title: "Search Achievements"
|
||||
read_only_warning2: "Note: you can't save any edits here, because you're not logged in."
|
||||
|
||||
article:
|
||||
|
|
9
app/models/Achievement.coffee
Normal file
9
app/models/Achievement.coffee
Normal file
|
@ -0,0 +1,9 @@
|
|||
CocoModel = require './CocoModel'
|
||||
|
||||
module.exports = class Achievement extends CocoModel
|
||||
@className: 'Achievement'
|
||||
@schema: require 'schemas/models/achievement'
|
||||
urlRoot: '/db/achievement'
|
||||
|
||||
isRepeatable: ->
|
||||
@get('proportionalTo')?
|
|
@ -1,6 +1,8 @@
|
|||
storage = require 'lib/storage'
|
||||
deltasLib = require 'lib/deltas'
|
||||
|
||||
NewAchievementCollection = require '../collections/NewAchievementCollection'
|
||||
|
||||
class CocoModel extends Backbone.Model
|
||||
idAttribute: "_id"
|
||||
loaded: false
|
||||
|
@ -86,6 +88,7 @@ class CocoModel extends Backbone.Model
|
|||
success(@, res) if success
|
||||
@markToRevert() if @_revertAttributes
|
||||
@clearBackup()
|
||||
CocoModel.pollAchievements()
|
||||
options.error = (model, res) =>
|
||||
error(@, res) if error
|
||||
return unless @notyErrors
|
||||
|
@ -266,4 +269,14 @@ class CocoModel extends Backbone.Model
|
|||
getURL: ->
|
||||
return if _.isString @url then @url else @url()
|
||||
|
||||
@pollAchievements: ->
|
||||
achievements = new NewAchievementCollection
|
||||
achievements.fetch(
|
||||
success: (collection) ->
|
||||
me.fetch (success: -> Backbone.Mediator.publish('achievements:new', collection)) unless _.isEmpty(collection.models)
|
||||
)
|
||||
|
||||
|
||||
CocoModel.pollAchievements = _.debounce CocoModel.pollAchievements, 500
|
||||
|
||||
module.exports = CocoModel
|
||||
|
|
|
@ -74,3 +74,21 @@ module.exports = class User extends CocoModel
|
|||
@set('emails', newSubs)
|
||||
|
||||
isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled
|
||||
|
||||
a = 5
|
||||
b = 40
|
||||
|
||||
# y = a * ln(1/b * (x + b)) + 1
|
||||
@levelFromExp: (xp) ->
|
||||
if xp > 0 then Math.floor(a * Math.log((1/b) * (xp + b))) + 1 else 1
|
||||
|
||||
# x = (e^((y-1)/a) - 1) * b
|
||||
@expForLevel: (level) ->
|
||||
Math.ceil((Math.exp((level - 1)/ a) - 1) * b)
|
||||
|
||||
level: ->
|
||||
User.levelFromExp(@get('points'))
|
||||
|
||||
levelFromExp: (xp) -> User.levelFromExp(xp)
|
||||
|
||||
expForLevel: (level) -> User.expForLevel(level)
|
||||
|
|
76
app/schemas/models/achievement.coffee
Normal file
76
app/schemas/models/achievement.coffee
Normal file
|
@ -0,0 +1,76 @@
|
|||
c = require './../schemas'
|
||||
|
||||
# TODO add these: http://docs.mongodb.org/manual/reference/operator/query/
|
||||
MongoQueryOperatorSchema =
|
||||
title: 'MongoDB Query operator'
|
||||
id: 'mongoQueryOperator'
|
||||
type: 'object'
|
||||
properties:
|
||||
'$gt': type: 'number'
|
||||
'$gte': type: 'number'
|
||||
'$in': type: 'array'
|
||||
'$lt': type: 'number'
|
||||
'$lte': type: 'number'
|
||||
'$ne': type: [ 'number', 'string' ]
|
||||
'$nin': type: 'array'
|
||||
additionalProperties: true # TODO set to false when the schema's done
|
||||
|
||||
MongoFindQuerySchema =
|
||||
title: 'MongoDB Query'
|
||||
id: 'mongoFindQuery'
|
||||
type: 'object'
|
||||
patternProperties:
|
||||
#'^[-a-zA-Z0-9_]*$':
|
||||
'^[-a-zA-Z0-9\.]*$':
|
||||
oneOf: [
|
||||
#{ $ref: '#/definitions/' + MongoQueryOperatorSchema.id},
|
||||
{ type: 'string' }
|
||||
{ type: 'object' }
|
||||
]
|
||||
additionalProperties: true # TODO make Treema accept new pattern matched keys
|
||||
definitions: {}
|
||||
|
||||
MongoFindQuerySchema.definitions[MongoQueryOperatorSchema.id] = MongoQueryOperatorSchema
|
||||
|
||||
AchievementSchema = c.object()
|
||||
c.extendNamedProperties AchievementSchema
|
||||
c.extendBasicProperties AchievementSchema, 'article'
|
||||
c.extendSearchableProperties AchievementSchema
|
||||
|
||||
_.extend(AchievementSchema.properties,
|
||||
query:
|
||||
#type:'object'
|
||||
$ref: '#/definitions/' + MongoFindQuerySchema.id
|
||||
worth: { type: 'number' }
|
||||
collection: { type: 'string' }
|
||||
description: { type: 'string' }
|
||||
userField: { type: 'string' }
|
||||
related: c.objectId(description: 'Related entity')
|
||||
icon: { type: 'string', format: 'image-file', title: 'Icon' }
|
||||
proportionalTo:
|
||||
type: 'string'
|
||||
description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations'
|
||||
function:
|
||||
type: 'object'
|
||||
oneOf: [
|
||||
linear:
|
||||
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
|
||||
)
|
||||
|
||||
AchievementSchema.definitions = {}
|
||||
AchievementSchema.definitions[MongoFindQuerySchema.id] = MongoFindQuerySchema
|
||||
|
||||
module.exports = AchievementSchema
|
34
app/schemas/models/earned_achievement.coffee
Normal file
34
app/schemas/models/earned_achievement.coffee
Normal file
|
@ -0,0 +1,34 @@
|
|||
c = require './../schemas'
|
||||
|
||||
module.exports =
|
||||
EarnedAchievementSchema =
|
||||
type: 'object'
|
||||
properties:
|
||||
user: c.objectId
|
||||
links:
|
||||
[
|
||||
{
|
||||
rel: 'extra'
|
||||
href: "/db/user/{($)}"
|
||||
}
|
||||
]
|
||||
achievement: c.objectId
|
||||
links:
|
||||
[
|
||||
{
|
||||
rel: 'extra'
|
||||
href: '/db/achievement/{($)}'
|
||||
}
|
||||
]
|
||||
collection:
|
||||
type: 'string'
|
||||
achievementName:
|
||||
type: 'string'
|
||||
created:
|
||||
type: 'date'
|
||||
changed:
|
||||
type: 'date'
|
||||
achievedAmount:
|
||||
type: 'number'
|
||||
notified:
|
||||
type: 'boolean'
|
|
@ -114,6 +114,7 @@ UserSchema = c.object {},
|
|||
linkedinID: c.shortString {title:"LinkedInID", description: "The user's LinkedIn ID when they signed the contract."}
|
||||
date: c.date {title: "Date signed employer agreement"}
|
||||
data: c.object {description: "Cached LinkedIn data slurped from profile."}
|
||||
points: {type:'number'}
|
||||
|
||||
|
||||
c.extendBasicProperties UserSchema, 'user'
|
||||
|
|
|
@ -17,7 +17,7 @@ me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext)
|
|||
me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext)
|
||||
me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext)
|
||||
# should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient
|
||||
me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext)
|
||||
me.objectId = (ext) -> schema = combine(['object', 'string'], ext)
|
||||
me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext)
|
||||
|
||||
PointSchema = me.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]},
|
||||
|
@ -48,7 +48,7 @@ me.colorConfig = (props) ->
|
|||
# BASICS
|
||||
|
||||
basicProps = (linkFragment) ->
|
||||
_id: me.objectId(links: [{rel: 'self', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden')
|
||||
_id: me.objectId(links: [{rel: 'self', href: "/db/#{linkFragment}/{($)}"}], format:"hidden")
|
||||
__v: { title: 'Mongoose Version', format: 'hidden' }
|
||||
|
||||
me.extendBasicProperties = (schema, linkFragment) ->
|
||||
|
|
47
app/styles/notify.sass
Normal file
47
app/styles/notify.sass
Normal file
|
@ -0,0 +1,47 @@
|
|||
.notifyjs-achievement-base
|
||||
//background: url("/images/pages/base/notify_mockup.png")
|
||||
background-image: url("/images/pages/base/modal_background.png")
|
||||
background-size: 100% 100%
|
||||
width: 500px
|
||||
height: 200px
|
||||
padding: 35px 35px 15px 15px
|
||||
text-align: center
|
||||
cursor: auto
|
||||
|
||||
.achievement-body
|
||||
.achievement-image
|
||||
img
|
||||
float: left
|
||||
width: 100px
|
||||
height: 100px
|
||||
border-radius: 50%
|
||||
margin: 20px 30px 20px 30px
|
||||
-webkit-box-shadow: 0px 0px 36px 0px white
|
||||
-moz-box-shadow: 0px 0px 36px 0px white
|
||||
box-shadow: 0px 0px 36px 0px white
|
||||
|
||||
.achievement-title
|
||||
font-family: Bangers
|
||||
font-size: 28px
|
||||
|
||||
.achievement-description
|
||||
margin-top: 10px
|
||||
font-size: 16px
|
||||
|
||||
.achievement-progress
|
||||
padding: 15px 0px 0px 0px
|
||||
|
||||
.achievement-message
|
||||
font-family: Bangers
|
||||
font-size: 18px
|
||||
&:empty
|
||||
display: none
|
||||
|
||||
.progress-wrapper
|
||||
.progress-bar-wrapper
|
||||
width: 100%
|
||||
.earned-exp
|
||||
padding-left: 5px
|
||||
font-family: Bangers
|
||||
font-size: 16px
|
||||
float: right
|
12
app/templates/achievement_notify.jade
Normal file
12
app/templates/achievement_notify.jade
Normal file
|
@ -0,0 +1,12 @@
|
|||
div
|
||||
.clearfix.achievement-body
|
||||
.achievement-image(data-notify-html="image")
|
||||
.achievement-content
|
||||
.achievement-title(data-notify-html="title")
|
||||
.achievement-description(data-notify-html="description")
|
||||
|
||||
.achievement-progress
|
||||
.achievement-message(data-notify-html="message")
|
||||
.progress-wrapper
|
||||
.earned-exp(data-notify-html="earnedExp")
|
||||
.progress-bar-wrapper(data-notify-html="progressBar")
|
|
@ -12,7 +12,7 @@ body
|
|||
|
||||
a.navbar-brand(href='/')
|
||||
img(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
|
||||
.collapse.navbar-collapse#collapsible-navbar
|
||||
.collapse.navbar-collapse#collapsible-navbar
|
||||
ul.nav.navbar-nav
|
||||
li.play
|
||||
a.header-font(href='/play', data-i18n="nav.play") Levels
|
||||
|
|
29
app/templates/editor/achievement/edit.jade
Normal file
29
app/templates/editor/achievement/edit.jade
Normal file
|
@ -0,0 +1,29 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
if me.isAdmin()
|
||||
div
|
||||
ol.breadcrumb
|
||||
li
|
||||
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
|
||||
li
|
||||
a(href="/editor/achievement", data-i18n="editor.achievement_title") Achievement Editor
|
||||
li.active
|
||||
| #{achievement.attributes.name}
|
||||
|
||||
button(data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary#save-button Save
|
||||
|
||||
h3(data-i18n="achievement.edit_achievement_title") Edit Achievement
|
||||
span
|
||||
|: "#{achievement.attributes.name}"
|
||||
|
||||
#achievement-treema
|
||||
|
||||
#achievement-view
|
||||
|
||||
hr
|
||||
|
||||
div#error-view
|
||||
else
|
||||
.alert.alert-danger
|
||||
span Admin only. Turn around.
|
17
app/templates/editor/achievement/table.jade
Normal file
17
app/templates/editor/achievement/table.jade
Normal file
|
@ -0,0 +1,17 @@
|
|||
extends /templates/kinds/table
|
||||
|
||||
block tableHeader
|
||||
tr
|
||||
th(data-i18n="general.name") Name
|
||||
th(data-i18n="general.description") Description
|
||||
th(data-i18n="general.collection") Collection
|
||||
|
||||
block tableBody
|
||||
for data in documents
|
||||
- data = data.attributes
|
||||
tr
|
||||
td
|
||||
a(href="/editor/achievement/#{data.slug || data._id}")
|
||||
| #{data.name}
|
||||
td #{data.description}
|
||||
td #{data.collection}
|
|
@ -1,38 +1,43 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
div
|
||||
ol.breadcrumb
|
||||
li
|
||||
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
|
||||
li.active(data-i18n="#{currentEditor}")
|
||||
| #{currentEditor}
|
||||
if !unauthorized
|
||||
div
|
||||
ol.breadcrumb
|
||||
li
|
||||
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
|
||||
li.active(data-i18n="#{currentEditor}")
|
||||
| #{currentEditor}
|
||||
|
||||
if me.get('anonymous')
|
||||
a.btn.btn-primary.open-modal-button(data-toggle="coco-modal", data-target="modal/auth", role="button", data-i18n="#{currentNewSignup}") Log in to Create a New Content
|
||||
if me.get('anonymous')
|
||||
a.btn.btn-primary.open-modal-button(data-toggle="coco-modal", data-target="modal/auth", role="button", data-i18n="#{currentNewSignup}") Log in to Create a New Content
|
||||
else
|
||||
a.btn.btn-primary.open-modal-button(href='#new-model-modal', role="button", data-toggle="modal", data-i18n="#{currentNew}") Create a New Something
|
||||
input#search(data-i18n="[placeholder]#{currentSearch}")
|
||||
hr
|
||||
div.results
|
||||
table
|
||||
|
||||
// TODO: make this into a ModalView subview
|
||||
div.modal.fade#new-model-modal
|
||||
.modal-dialog
|
||||
.background-wrapper
|
||||
.modal-content
|
||||
.modal-header
|
||||
h3(data-i18n="#{currentNew}") Create New #{modelLabel}
|
||||
.modal-body
|
||||
form.form
|
||||
.form-group
|
||||
label.control-label(for="name", data-i18n="general.name") Name
|
||||
input#name.form-control(name="name", type="text")
|
||||
.modal-footer
|
||||
button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel
|
||||
button.btn.btn-primary.new-model-submit(data-i18n="common.create") Create
|
||||
.modal-body.wait.secret
|
||||
h3(data-i18n="play_level.tip_reticulating") Reticulating Splines...
|
||||
.progress.progress-striped.active
|
||||
.progress-bar
|
||||
else
|
||||
a.btn.btn-primary.open-modal-button(href='#new-model-modal', role="button", data-toggle="modal", data-i18n="#{currentNew}") Create a New Something
|
||||
input#search(data-i18n="[placeholder]#{currentSearch}")
|
||||
hr
|
||||
div.results
|
||||
table
|
||||
|
||||
// TODO: make this into a ModalView subview
|
||||
div.modal.fade#new-model-modal
|
||||
.modal-dialog
|
||||
.background-wrapper
|
||||
.modal-content
|
||||
.modal-header
|
||||
h3(data-i18n="#{currentNew}") Create New #{modelLabel}
|
||||
.modal-body
|
||||
form.form
|
||||
.form-group
|
||||
label.control-label(for="name", data-i18n="general.name") Name
|
||||
input#name.form-control(name="name", type="text")
|
||||
.modal-footer
|
||||
button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel
|
||||
button.btn.btn-primary.new-model-submit(data-i18n="common.create") Create
|
||||
.modal-body.wait.secret
|
||||
h3(data-i18n="play_level.tip_reticulating") Reticulating Splines...
|
||||
.progress.progress-striped.active
|
||||
.progress-bar
|
||||
.alert.alert-danger
|
||||
span Admin only. Turn around.
|
||||
// TODO Ruben prettify
|
||||
|
|
|
@ -5,17 +5,19 @@ table.table
|
|||
| Results
|
||||
span
|
||||
|: #{documents.length}
|
||||
|
||||
tr
|
||||
th(data-i18n="general.name") Name
|
||||
th(data-i18n="general.description") Description
|
||||
th(data-i18n="general.version") Version
|
||||
|
||||
for data in documents
|
||||
- data = data.attributes;
|
||||
|
||||
block tableHeader
|
||||
tr
|
||||
td
|
||||
a(href="/editor/#{page}/#{data.slug || data._id}")
|
||||
| #{data.name}
|
||||
td.body-row #{data.description}
|
||||
td #{data.version.major}.#{data.version.minor}
|
||||
th(data-i18n="general.name") Name
|
||||
th(data-i18n="general.description") Description
|
||||
th(data-i18n="general.version") Version
|
||||
|
||||
block tableBody
|
||||
for data in documents
|
||||
- data = data.attributes;
|
||||
tr
|
||||
td
|
||||
a(href="/editor/#{page}/#{data.slug || data._id}")
|
||||
| #{data.name}
|
||||
td.body-row #{data.description}
|
||||
td #{data.version.major}.#{data.version.minor}
|
74
app/views/editor/achievement/edit.coffee
Normal file
74
app/views/editor/achievement/edit.coffee
Normal file
|
@ -0,0 +1,74 @@
|
|||
View = require 'views/kinds/RootView'
|
||||
template = require 'templates/editor/achievement/edit'
|
||||
Achievement = require 'models/Achievement'
|
||||
|
||||
module.exports = class AchievementEditView extends View
|
||||
id: "editor-achievement-edit-view"
|
||||
template: template
|
||||
startsLoading: true
|
||||
|
||||
events:
|
||||
'click #save-button': 'saveAchievement'
|
||||
|
||||
subscriptions:
|
||||
'save-new': 'saveAchievement'
|
||||
|
||||
constructor: (options, @achievementID) ->
|
||||
super options
|
||||
@achievement = new Achievement(_id: @achievementID)
|
||||
@achievement.saveBackups = true
|
||||
|
||||
@listenToOnce(@achievement, 'error',
|
||||
() =>
|
||||
@hideLoading()
|
||||
$(@$el).find('.main-content-area').children('*').not('#error-view').remove()
|
||||
|
||||
@insertSubView(new ErrorView())
|
||||
)
|
||||
|
||||
@achievement.fetch()
|
||||
@listenToOnce(@achievement, 'sync', @buildTreema)
|
||||
@pushChangesToPreview = _.throttle(@pushChangesToPreview, 500)
|
||||
|
||||
buildTreema: ->
|
||||
return if @treema? or (not @achievement.loaded)
|
||||
|
||||
@startsLoading = false
|
||||
@render()
|
||||
data = $.extend(true, {}, @achievement.attributes)
|
||||
options =
|
||||
data: data
|
||||
filePath: "db/achievement/#{@achievement.get('_id')}"
|
||||
schema: Achievement.schema
|
||||
readOnly: me.get('anonymous')
|
||||
callbacks:
|
||||
change: @pushChangesToPreview
|
||||
@treema = @$el.find('#achievement-treema').treema(options)
|
||||
|
||||
@treema.build()
|
||||
|
||||
pushChangesToPreview: =>
|
||||
'TODO' # TODO might want some intrinsic preview thing
|
||||
|
||||
getRenderData: (context={}) ->
|
||||
context = super(context)
|
||||
context.achievement = @achievement
|
||||
context.authorized = me.isAdmin()
|
||||
context
|
||||
|
||||
openSaveModal: ->
|
||||
'Maybe later' # TODO
|
||||
|
||||
saveAchievement: (e) ->
|
||||
@treema.endExistingEdits()
|
||||
for key, value of @treema.data
|
||||
@achievement.set(key, value)
|
||||
|
||||
res = @achievement.save()
|
||||
|
||||
res.error (collection, response, options) =>
|
||||
console.error response
|
||||
|
||||
res.success =>
|
||||
url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}"
|
||||
document.location.href = url
|
19
app/views/editor/achievement/home.coffee
Normal file
19
app/views/editor/achievement/home.coffee
Normal file
|
@ -0,0 +1,19 @@
|
|||
SearchView = require 'views/kinds/SearchView'
|
||||
|
||||
module.exports = class AchievementSearchView extends SearchView
|
||||
id: "editor-achievement-home-view"
|
||||
modelLabel: "Achievement"
|
||||
model: require 'models/Achievement'
|
||||
modelURL: '/db/achievement'
|
||||
tableTemplate: require 'templates/editor/achievement/table'
|
||||
projection: ['name', 'description', 'collection', 'slug']
|
||||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
context.currentEditor = 'editor.achievement_title'
|
||||
context.currentNew = 'editor.new_achievement_title'
|
||||
context.currentNewSignup = 'editor.new_achievement_title_login'
|
||||
context.currentSearch = 'editor.achievement_search_title'
|
||||
context.unauthorized = true unless me.isAdmin()
|
||||
@$el.i18n()
|
||||
context
|
|
@ -6,6 +6,9 @@ CocoView = require './CocoView'
|
|||
{logoutUser, me} = require('lib/auth')
|
||||
locale = require 'locale/locale'
|
||||
|
||||
Achievement = require '../../models/Achievement'
|
||||
User = require '../../models/User'
|
||||
|
||||
filterKeyboardEvents = (allowedEvents, func) ->
|
||||
return (splat...) ->
|
||||
e = splat[0]
|
||||
|
@ -19,6 +22,71 @@ module.exports = class RootView extends CocoView
|
|||
'click .toggle-fullscreen': 'toggleFullscreen'
|
||||
'click .auth-button': 'onClickAuthbutton'
|
||||
|
||||
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) ->
|
||||
currentLevel = me.level()
|
||||
nextLevel = currentLevel + 1
|
||||
currentLevelExp = User.expForLevel(currentLevel)
|
||||
nextLevelExp = User.expForLevel(nextLevel)
|
||||
totalExpNeeded = nextLevelExp - currentLevelExp
|
||||
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
|
||||
|
||||
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}"
|
||||
|
||||
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>")
|
||||
emptyBar = $("<div data-toggle='tooltip' class='progress-bar progress-bar-white' style='width:#{100 - newlyAchievedPercentage - alreadyAchievedPercentage}%'></div>")
|
||||
progressBar = $('<div class="progress" data-toggle="tooltip"></div>').append(alreadyAchievedBar).append(newlyAchievedBar).append(emptyBar)
|
||||
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")
|
||||
emptyBar.tooltip(title: "#{nextLevelExp - currentExp} XP until level #{nextLevel}")
|
||||
|
||||
# TODO a default should be linked here
|
||||
imageURL = '/file/' + achievement.get('icon')
|
||||
data =
|
||||
title: achievement.get('name')
|
||||
image: $("<img src='#{imageURL}' />")
|
||||
description: achievement.get('description')
|
||||
progressBar: progressBar
|
||||
earnedExp: "+ #{worth} XP"
|
||||
message: message
|
||||
|
||||
options =
|
||||
autoHideDelay: 10000
|
||||
globalPosition: 'bottom right'
|
||||
showDuration: 400
|
||||
style: 'achievement'
|
||||
autoHide: true
|
||||
clickToHide: true
|
||||
|
||||
$.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
|
||||
)
|
||||
)
|
||||
|
||||
logoutAccount: ->
|
||||
logoutUser($('#login-email').val())
|
||||
|
||||
|
|
|
@ -4,8 +4,12 @@ forms = require('lib/forms')
|
|||
app = require('application')
|
||||
|
||||
class SearchCollection extends Backbone.Collection
|
||||
initialize: (modelURL, @model, @term) ->
|
||||
@url = "#{modelURL}/search?project=true"
|
||||
initialize: (modelURL, @model, @term, @projection) ->
|
||||
@url = "#{modelURL}/search?project="
|
||||
if @projection? and not (@projection == [])
|
||||
@url += projection[0]
|
||||
@url += ',' + projected for projected in projection[1..]
|
||||
else @url += "true"
|
||||
@url += "&term=#{term}" if @term
|
||||
|
||||
module.exports = class SearchView extends View
|
||||
|
@ -17,6 +21,7 @@ module.exports = class SearchView extends View
|
|||
model: null # Article
|
||||
modelURL: null # '/db/article'
|
||||
tableTemplate: null # require 'templates/editor/article/table'
|
||||
projected: null # ['name', 'description', 'version'] or null for default
|
||||
|
||||
events:
|
||||
'change input#search': 'runSearch'
|
||||
|
@ -26,6 +31,7 @@ module.exports = class SearchView extends View
|
|||
'shown.bs.modal #new-model-modal': 'focusOnName'
|
||||
'hidden.bs.modal #new-model-modal': 'onModalHidden'
|
||||
|
||||
|
||||
constructor: (options) ->
|
||||
@runSearch = _.debounce(@runSearch, 500)
|
||||
super options
|
||||
|
@ -44,7 +50,7 @@ module.exports = class SearchView extends View
|
|||
return if @sameSearch(term)
|
||||
@removeOldSearch()
|
||||
|
||||
@collection = new SearchCollection(@modelURL, @model, term)
|
||||
@collection = new SearchCollection(@modelURL, @model, term, @projection)
|
||||
@collection.term = term # needed?
|
||||
@listenTo(@collection, 'sync', @onSearchChange)
|
||||
@showLoading(@$el.find('.results'))
|
||||
|
|
35
server/achievements/Achievement.coffee
Normal file
35
server/achievements/Achievement.coffee
Normal file
|
@ -0,0 +1,35 @@
|
|||
mongoose = require('mongoose')
|
||||
plugins = require('../plugins/plugins')
|
||||
jsonschema = require('../../app/schemas/models/achievement')
|
||||
log = require 'winston'
|
||||
|
||||
# `pre` and `post` are not called for update operations executed directly on the database,
|
||||
# including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order
|
||||
# to utilize `pre` or `post` middleware, you should `find()` the document, and call the `init`, `validate`, `save`,
|
||||
# or `remove` functions on the document. See [explanation](http://github.com/LearnBoost/mongoose/issues/964).
|
||||
|
||||
AchievementSchema = new mongoose.Schema({
|
||||
userField: String
|
||||
}, {strict: false})
|
||||
|
||||
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 = () ->
|
||||
@set('query', JSON.stringify(@get('query'))) if typeof @get('query') != "string"
|
||||
|
||||
AchievementSchema.post('init', (doc) -> doc.objectifyQuery())
|
||||
|
||||
AchievementSchema.pre('save', (next) ->
|
||||
@stringifyQuery()
|
||||
next()
|
||||
)
|
||||
|
||||
AchievementSchema.plugin(plugins.NamedPlugin)
|
||||
AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']})
|
||||
|
||||
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema)
|
19
server/achievements/EarnedAchievement.coffee
Normal file
19
server/achievements/EarnedAchievement.coffee
Normal file
|
@ -0,0 +1,19 @@
|
|||
mongoose = require 'mongoose'
|
||||
jsonschema = require '../../app/schemas/models/earned_achievement'
|
||||
|
||||
EarnedAchievementSchema = new mongoose.Schema({
|
||||
created:
|
||||
type: Date
|
||||
default: Date.now
|
||||
changed:
|
||||
type: Date
|
||||
default: Date.now
|
||||
notified:
|
||||
type: Boolean
|
||||
default: false
|
||||
}, {strict:false})
|
||||
|
||||
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)
|
21
server/achievements/achievement_handler.coffee
Normal file
21
server/achievements/achievement_handler.coffee
Normal file
|
@ -0,0 +1,21 @@
|
|||
Achievement = require './Achievement'
|
||||
Handler = require '../commons/Handler'
|
||||
|
||||
class AchievementHandler extends Handler
|
||||
modelClass: Achievement
|
||||
|
||||
# Used to determine which properties requests may edit
|
||||
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon']
|
||||
jsonSchema = require '../../app/schemas/models/achievement.coffee'
|
||||
|
||||
hasAccess: (req) ->
|
||||
req.method is 'GET' or req.user?.isAdmin()
|
||||
|
||||
get: (req, res) ->
|
||||
query = @modelClass.find({})
|
||||
query.exec (err, documents) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
documents = (@formatEntity(req, doc) for doc in documents)
|
||||
@sendSuccess(res, documents)
|
||||
|
||||
module.exports = new AchievementHandler()
|
12
server/achievements/earned_achievement_handler.coffee
Normal file
12
server/achievements/earned_achievement_handler.coffee
Normal file
|
@ -0,0 +1,12 @@
|
|||
mongoose = require('mongoose')
|
||||
EarnedAchievement = require './EarnedAchievement'
|
||||
Handler = require '../commons/Handler'
|
||||
|
||||
class EarnedAchievementHandler extends Handler
|
||||
modelClass: EarnedAchievement
|
||||
|
||||
# Don't allow POSTs or anything yet
|
||||
hasAccess: (req) ->
|
||||
req.method is 'GET'
|
||||
|
||||
module.exports = new EarnedAchievementHandler()
|
|
@ -153,13 +153,13 @@ module.exports = class Handler
|
|||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, @formatEntity(req, document))
|
||||
|
||||
# project=true or project=name,description,slug for example
|
||||
search: (req, res) ->
|
||||
unless @modelClass.schema.uses_coco_search
|
||||
return @sendNotFoundError(res)
|
||||
|
||||
term = req.query.term
|
||||
matchedObjects = []
|
||||
filters = [{filter: {index: true}}]
|
||||
filters = if @modelClass.schema.uses_coco_versions or @modelClass.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}]
|
||||
if @modelClass.schema.uses_coco_permissions and req.user
|
||||
filters.push {filter: {index: req.user.get('id')}}
|
||||
projection = null
|
||||
|
@ -167,7 +167,7 @@ module.exports = class Handler
|
|||
projection = PROJECT
|
||||
else if req.query.project
|
||||
if @modelClass.className is 'User'
|
||||
projection = PROJECTION
|
||||
projection = PROJECT
|
||||
log.warn "Whoa, we haven't yet thought about public properties for User projection yet."
|
||||
else
|
||||
projection = {}
|
||||
|
|
|
@ -9,6 +9,8 @@ module.exports.handlers =
|
|||
'patch': 'patches/patch_handler'
|
||||
'thang_type': 'levels/thangs/thang_type_handler'
|
||||
'user': 'users/user_handler'
|
||||
'achievement': 'achievements/achievement_handler'
|
||||
'earned_achievement': 'achievements/earned_achievement_handler'
|
||||
|
||||
module.exports.routes =
|
||||
[
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
mongoose = require('mongoose')
|
||||
plugins = require('../../plugins/plugins')
|
||||
AchievablePlugin = require '../../plugins/achievements'
|
||||
jsonschema = require('../../../app/schemas/models/level_session')
|
||||
|
||||
LevelSessionSchema = new mongoose.Schema({
|
||||
|
@ -10,6 +11,7 @@ LevelSessionSchema = new mongoose.Schema({
|
|||
'default': Date.now
|
||||
}, {strict: false})
|
||||
LevelSessionSchema.plugin(plugins.PermissionsPlugin)
|
||||
LevelSessionSchema.plugin(AchievablePlugin)
|
||||
|
||||
LevelSessionSchema.pre 'init', (next) ->
|
||||
# TODO: refactor this into a set of common plugins for all models?
|
||||
|
|
89
server/plugins/achievements.coffee
Normal file
89
server/plugins/achievements.coffee
Normal file
|
@ -0,0 +1,89 @@
|
|||
mongoose = require('mongoose')
|
||||
Achievement = require('../achievements/Achievement')
|
||||
EarnedAchievement = require '../achievements/EarnedAchievement'
|
||||
User = require '../users/User'
|
||||
LocalMongo = require '../../app/lib/LocalMongo'
|
||||
util = require '../../app/lib/utils'
|
||||
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) ->
|
||||
checkForAchievement = (doc) ->
|
||||
collectionName = doc.constructor.modelName
|
||||
|
||||
before = {}
|
||||
|
||||
schema.post 'init', (doc) ->
|
||||
before[doc.id] = doc.toObject()
|
||||
|
||||
schema.post 'save', (doc) ->
|
||||
isNew = not doc.isInit('_id')
|
||||
originalDocObj = before[doc.id] unless isNew
|
||||
|
||||
category = doc.constructor.modelName
|
||||
|
||||
if category of achievements
|
||||
docObj = doc.toObject()
|
||||
for achievement in achievements[category]
|
||||
query = achievement.get('query')
|
||||
isRepeatable = achievement.get('proportionalTo')?
|
||||
alreadyAchieved = if isNew then false else LocalMongo.matchesQuery originalDocObj, query
|
||||
newlyAchieved = LocalMongo.matchesQuery(docObj, query)
|
||||
log.debug 'isRepeatable: ' + isRepeatable
|
||||
log.debug 'alreadyAchieved: ' + alreadyAchieved
|
||||
log.debug 'newlyAchieved: ' + newlyAchieved
|
||||
|
||||
userObjectID = doc.get(achievement.get('userField'))
|
||||
userID = if _.isObject userObjectID then userObjectID.toHexString() else userObjectID # Standardize! Use strings, not ObjectId's
|
||||
|
||||
if newlyAchieved and (not alreadyAchieved or isRepeatable)
|
||||
earned = {
|
||||
user: userID
|
||||
achievement: achievement._id.toHexString()
|
||||
achievementName: achievement.get 'name'
|
||||
}
|
||||
earnedPoints = 0
|
||||
wrapUp = ->
|
||||
# Update user's experience points
|
||||
User.update({_id: userID}, {$inc: {points: earnedPoints}}, {}, (err, count) ->
|
||||
console.error err if err?
|
||||
)
|
||||
|
||||
if isRepeatable
|
||||
log.debug 'Upserting repeatable achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
||||
proportionalTo = achievement.get 'proportionalTo'
|
||||
originalAmount = util.getByPath(originalDocObj, proportionalTo) or 0
|
||||
newAmount = docObj[proportionalTo]
|
||||
|
||||
if originalAmount isnt newAmount
|
||||
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?
|
||||
)
|
||||
|
||||
earnedPoints = achievement.get('worth') * (newAmount - originalAmount)
|
||||
wrapUp()
|
||||
|
||||
else # not alreadyAchieved
|
||||
log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
||||
(new EarnedAchievement(earned)).save (err, doc) ->
|
||||
return log.debug err if err?
|
||||
|
||||
earnedPoints = achievement.get('worth')
|
||||
wrapUp()
|
||||
|
||||
delete before[doc.id] unless isNew # This assumes everything we patch has a _id
|
||||
return
|
|
@ -127,3 +127,6 @@ UserSchema.statics.hashPassword = (password) ->
|
|||
shasum.digest('hex')
|
||||
|
||||
module.exports = User = mongoose.model('User', UserSchema)
|
||||
|
||||
AchievablePlugin = require '../plugins/achievements'
|
||||
UserSchema.plugin(AchievablePlugin)
|
||||
|
|
|
@ -10,6 +10,7 @@ async = require 'async'
|
|||
log = require 'winston'
|
||||
LevelSession = require('../levels/sessions/LevelSession')
|
||||
LevelSessionHandler = require '../levels/sessions/level_session_handler'
|
||||
EarnedAchievement = require '../achievements/EarnedAchievement'
|
||||
|
||||
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset']
|
||||
privateProperties = [
|
||||
|
@ -192,6 +193,7 @@ UserHandler = class UserHandler extends Handler
|
|||
return @getCandidates(req, res) if args[1] is 'candidates'
|
||||
return @getSimulatorLeaderboard(req, res, args[0]) if args[1] is 'simulatorLeaderboard'
|
||||
return @getMySimulatorLeaderboardRank(req, res, args[0]) if args[1] is 'simulator_leaderboard_rank'
|
||||
return @getEarnedAchievements(req, res, args[0]) if args[1] is 'achievements'
|
||||
return @sendNotFoundError(res)
|
||||
super(arguments...)
|
||||
|
||||
|
@ -235,6 +237,18 @@ UserHandler = class UserHandler extends Handler
|
|||
documents = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
|
||||
@sendSuccess(res, documents)
|
||||
|
||||
getEarnedAchievements: (req, res, userID) ->
|
||||
queryObject = {$query: {user: userID}, $orderby: {changed: -1}}
|
||||
queryObject.$query.notified = false if req.query.notified is 'false'
|
||||
query = EarnedAchievement.find(queryObject)
|
||||
query.exec (err, documents) =>
|
||||
return @sendDatabaseError(res, err) if err?
|
||||
cleandocs = (@formatEntity(req, doc) for doc in documents)
|
||||
for doc in documents # Maybe move this logic elsewhere
|
||||
doc.set('notified', true)
|
||||
doc.save()
|
||||
@sendSuccess(res, cleandocs)
|
||||
|
||||
agreeToEmployerAgreement: (req, res) ->
|
||||
userIsAnonymous = req.user?.get('anonymous')
|
||||
if userIsAnonymous then return errors.unauthorized(res, "You need to be logged in to agree to the employer agreeement.")
|
||||
|
|
77
test/app/lib/local_mongo.spec.coffee
Normal file
77
test/app/lib/local_mongo.spec.coffee
Normal file
|
@ -0,0 +1,77 @@
|
|||
describe 'Local Mongo queries', ->
|
||||
LocalMongo = require 'lib/LocalMongo'
|
||||
|
||||
beforeEach ->
|
||||
this.fixture1 =
|
||||
'id': 'somestring'
|
||||
'value': 9000
|
||||
'levels': [3, 8, 21]
|
||||
'worth': 6
|
||||
'type': 'unicorn'
|
||||
'likes': ['poptarts', 'popsicles', 'popcorn']
|
||||
|
||||
this.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()
|
||||
|
||||
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()
|
||||
|
||||
it 'nested match', ->
|
||||
expect(LocalMongo.matchesQuery(this.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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
Loading…
Add table
Reference in a new issue