mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-02 03:47:09 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
9633373d67
15 changed files with 560 additions and 41 deletions
|
@ -76,6 +76,8 @@ module.exports.getByPath = (target, path) ->
|
||||||
obj = obj[piece]
|
obj = obj[piece]
|
||||||
obj
|
obj
|
||||||
|
|
||||||
|
module.exports.isID = (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
|
||||||
|
|
||||||
module.exports.round = _.curry (digits, n) ->
|
module.exports.round = _.curry (digits, n) ->
|
||||||
n = +n.toFixed(digits)
|
n = +n.toFixed(digits)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
GRAVATAR_URL = 'https://www.gravatar.com/'
|
GRAVATAR_URL = 'https://www.gravatar.com/'
|
||||||
cache = {}
|
cache = {}
|
||||||
CocoModel = require './CocoModel'
|
CocoModel = require './CocoModel'
|
||||||
|
util = require 'lib/utils'
|
||||||
|
|
||||||
module.exports = class User extends CocoModel
|
module.exports = class User extends CocoModel
|
||||||
@className: 'User'
|
@className: 'User'
|
||||||
|
@ -45,6 +46,28 @@ module.exports = class User extends CocoModel
|
||||||
cache[id] = user
|
cache[id] = user
|
||||||
user
|
user
|
||||||
|
|
||||||
|
# callbacks can be either success or error
|
||||||
|
@getByIDOrSlug: (idOrSlug, force, callbacks={}) ->
|
||||||
|
{me} = require 'lib/auth'
|
||||||
|
isID = util.isID idOrSlug
|
||||||
|
if me.id is idOrSlug or me.slug is idOrSlug
|
||||||
|
callbacks.success me if callbacks.success?
|
||||||
|
return me
|
||||||
|
cached = cache[idOrSlug]
|
||||||
|
user = cached or new @ _id: idOrSlug
|
||||||
|
if force or not cached
|
||||||
|
user.loading = true
|
||||||
|
user.fetch
|
||||||
|
success: ->
|
||||||
|
user.loading = false
|
||||||
|
Backbone.Mediator.publish 'user:fetched'
|
||||||
|
callbacks.success user if callbacks.success?
|
||||||
|
error: ->
|
||||||
|
user.loading = false
|
||||||
|
callbacks.error user if callbacks.error?
|
||||||
|
cache[idOrSlug] = user
|
||||||
|
user
|
||||||
|
|
||||||
getEnabledEmails: ->
|
getEnabledEmails: ->
|
||||||
@migrateEmails()
|
@migrateEmails()
|
||||||
emails = _.clone(@get('emails')) or {}
|
emails = _.clone(@get('emails')) or {}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
c = require './../schemas'
|
c = require './../schemas'
|
||||||
emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification']
|
emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification']
|
||||||
|
|
||||||
UserSchema = c.object {},
|
UserSchema = c.object
|
||||||
name: c.shortString({title: 'Display Name', default: ''})
|
title: 'User'
|
||||||
|
|
||||||
|
c.extendNamedProperties UserSchema # let's have the name be the first property
|
||||||
|
|
||||||
|
_.extend UserSchema.properties,
|
||||||
email: c.shortString({title: 'Email', format: 'email'})
|
email: c.shortString({title: 'Email', format: 'email'})
|
||||||
firstName: c.shortString({title: 'First Name'})
|
firstName: c.shortString({title: 'First Name'})
|
||||||
lastName: c.shortString({title: 'Last Name'})
|
lastName: c.shortString({title: 'Last Name'})
|
||||||
|
|
128
app/styles/terrain_randomise.sass
Normal file
128
app/styles/terrain_randomise.sass
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
#terrain-randomise-modal
|
||||||
|
|
||||||
|
.choose-option
|
||||||
|
margin-bottom: 15px
|
||||||
|
width: 100%
|
||||||
|
height: 100px
|
||||||
|
overflow: hidden
|
||||||
|
background: white
|
||||||
|
border: 1px solid #333
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
-webkit-transition: opacity 0.3s ease-in-out
|
||||||
|
-moz-transition: opacity 0.3s ease-in-out
|
||||||
|
-ms-transition: opacity 0.3s ease-in-out
|
||||||
|
-o-transition: opacity 0.3s ease-in-out
|
||||||
|
transition: opacity 0.3s ease-in-out
|
||||||
|
|
||||||
|
opacity: 0.4
|
||||||
|
|
||||||
|
border-radius: 5px
|
||||||
|
.only-one
|
||||||
|
-webkit-transition: opacity 0.3s ease-in-out
|
||||||
|
-moz-transition: opacity 0.3s ease-in-out
|
||||||
|
-ms-transition: opacity 0.3s ease-in-out
|
||||||
|
-o-transition: opacity 0.3s ease-in-out
|
||||||
|
transition: opacity 0.3s ease-in-out
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
.choose-option:hover
|
||||||
|
opacity: 1
|
||||||
|
.only-one
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
.my-icon
|
||||||
|
position: relative
|
||||||
|
left: 0
|
||||||
|
top: -10px
|
||||||
|
z-index: 1
|
||||||
|
|
||||||
|
.my-team-icon
|
||||||
|
height: 60px
|
||||||
|
position: relative
|
||||||
|
top: -10px
|
||||||
|
left: 10px
|
||||||
|
z-index: 0
|
||||||
|
|
||||||
|
.opponent-team-icon
|
||||||
|
height: 60px
|
||||||
|
position: relative
|
||||||
|
top: 10px
|
||||||
|
right: 10px
|
||||||
|
z-index: 0
|
||||||
|
float: right
|
||||||
|
-moz-transform: scaleX(-1)
|
||||||
|
-o-transform: scaleX(-1)
|
||||||
|
-webkit-transform: scaleX(-1)
|
||||||
|
transform: scaleX(-1)
|
||||||
|
filter: FlipH
|
||||||
|
-ms-filter: "FlipH"
|
||||||
|
|
||||||
|
.opponent-icon
|
||||||
|
position: relative
|
||||||
|
float: right
|
||||||
|
right: 0
|
||||||
|
top: -10px
|
||||||
|
-moz-transform: scaleX(-1)
|
||||||
|
-o-transform: scaleX(-1)
|
||||||
|
-webkit-transform: scaleX(-1)
|
||||||
|
transform: scaleX(-1)
|
||||||
|
filter: FlipH
|
||||||
|
-ms-filter: "FlipH"
|
||||||
|
z-index: 1
|
||||||
|
|
||||||
|
.name-label
|
||||||
|
border-bottom: 20px solid lightslategray
|
||||||
|
height: 0
|
||||||
|
width: 40%
|
||||||
|
position: absolute
|
||||||
|
bottom: 0
|
||||||
|
color: black
|
||||||
|
font-weight: bold
|
||||||
|
text-align: center
|
||||||
|
z-index: 2
|
||||||
|
|
||||||
|
span
|
||||||
|
position: relative
|
||||||
|
top: 1px
|
||||||
|
|
||||||
|
.my-name
|
||||||
|
border-right: 15px solid transparent
|
||||||
|
left: 0
|
||||||
|
span
|
||||||
|
left: 3px
|
||||||
|
|
||||||
|
.preset-size
|
||||||
|
border-left: 15px solid transparent
|
||||||
|
right: 0
|
||||||
|
//text-align: right
|
||||||
|
span
|
||||||
|
right: 3px
|
||||||
|
|
||||||
|
.preset-name
|
||||||
|
border-top: 25px solid darkgray
|
||||||
|
border-left: 20px solid transparent
|
||||||
|
border-right: 20px solid transparent
|
||||||
|
height: 0
|
||||||
|
width: 30%
|
||||||
|
position: absolute
|
||||||
|
left: 35%
|
||||||
|
top: 0
|
||||||
|
color: black
|
||||||
|
text-align: center
|
||||||
|
font-size: 18px
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
span
|
||||||
|
position: relative
|
||||||
|
top: -25px
|
||||||
|
|
||||||
|
.easy-option .preset-name
|
||||||
|
border-top: 25px solid limegreen
|
||||||
|
|
||||||
|
.medium-option .preset-name
|
||||||
|
border-top: 25px solid darkorange
|
||||||
|
|
||||||
|
.hard-option .preset-name
|
||||||
|
border-top: 25px solid black
|
||||||
|
color: white
|
|
@ -79,6 +79,8 @@ block header
|
||||||
a(data-i18n="common.fork")#fork-level-start-button Fork
|
a(data-i18n="common.fork")#fork-level-start-button Fork
|
||||||
li(class=anonymous ? "disabled": "")
|
li(class=anonymous ? "disabled": "")
|
||||||
a(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert")#revert-button Revert
|
a(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert")#revert-button Revert
|
||||||
|
li(class=anonymous ? "disabled": "")
|
||||||
|
a(data-toggle="coco-modal", data-target="modal/terrain_randomise", data-i18n="editor.randomise")#randomise-button Randomise
|
||||||
li(class=anonymous ? "disabled": "")
|
li(class=anonymous ? "disabled": "")
|
||||||
a(data-i18n="editor.pop_i18n")#pop-level-i18n-button Populate i18n
|
a(data-i18n="editor.pop_i18n")#pop-level-i18n-button Populate i18n
|
||||||
li.divider
|
li.divider
|
||||||
|
|
15
app/templates/modal/terrain_randomise.jade
Normal file
15
app/templates/modal/terrain_randomise.jade
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
extends /templates/modal/modal_base
|
||||||
|
|
||||||
|
block modal-header-content
|
||||||
|
h3(data-i18n="editor.pick_a_terrain") Pick a Terrain
|
||||||
|
|
||||||
|
block modal-body-content
|
||||||
|
div#normal-view
|
||||||
|
a(href="#")
|
||||||
|
div.choose-option(data-preset-type="grassy", data-preset-size="small")
|
||||||
|
div.preset-size.name-label
|
||||||
|
span(data-i18n="ladder.small") Small
|
||||||
|
div.preset-name
|
||||||
|
span(data-i18n="ladder.grassy") Grassy
|
||||||
|
//- for model in models
|
||||||
|
block modal-footer
|
|
@ -43,6 +43,7 @@ module.exports = class ThangsTabView extends View
|
||||||
'sprite:mouse-up': 'onSpriteMouseUp'
|
'sprite:mouse-up': 'onSpriteMouseUp'
|
||||||
'sprite:double-clicked': 'onSpriteDoubleClicked'
|
'sprite:double-clicked': 'onSpriteDoubleClicked'
|
||||||
'surface:stage-mouse-up': 'onStageMouseUp'
|
'surface:stage-mouse-up': 'onStageMouseUp'
|
||||||
|
'randomise:terrain-generated': 'onRandomiseTerrain'
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click #extant-thangs-filter button': 'onFilterExtantThangs'
|
'click #extant-thangs-filter button': 'onFilterExtantThangs'
|
||||||
|
@ -57,6 +58,8 @@ module.exports = class ThangsTabView extends View
|
||||||
'delete, del, backspace': 'deleteSelectedExtantThang'
|
'delete, del, backspace': 'deleteSelectedExtantThang'
|
||||||
'left': -> @moveAddThangSelection -1
|
'left': -> @moveAddThangSelection -1
|
||||||
'right': -> @moveAddThangSelection 1
|
'right': -> @moveAddThangSelection 1
|
||||||
|
'ctrl+z': 'undoAction'
|
||||||
|
'ctrl+shift+z': 'redoAction'
|
||||||
|
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
super options
|
super options
|
||||||
|
@ -221,6 +224,12 @@ module.exports = class ThangsTabView extends View
|
||||||
return unless e.thang
|
return unless e.thang
|
||||||
@editThang thangID: e.thang.id
|
@editThang thangID: e.thang.id
|
||||||
|
|
||||||
|
onRandomiseTerrain: (e) ->
|
||||||
|
for thang in e.thangs
|
||||||
|
@selectAddThangType thang.id
|
||||||
|
@addThang @addThangType, thang.pos
|
||||||
|
@selectAddThangType null
|
||||||
|
|
||||||
# TODO: figure out a good way to have all Surface clicks and Treema clicks just proxy in one direction, so we can maintain only one way of handling selection and deletion
|
# TODO: figure out a good way to have all Surface clicks and Treema clicks just proxy in one direction, so we can maintain only one way of handling selection and deletion
|
||||||
onExtantThangSelected: (e) ->
|
onExtantThangSelected: (e) ->
|
||||||
@selectedExtantSprite?.setNameLabel? null unless @selectedExtantSprite is e.sprite
|
@selectedExtantSprite?.setNameLabel? null unless @selectedExtantSprite is e.sprite
|
||||||
|
@ -450,6 +459,12 @@ module.exports = class ThangsTabView extends View
|
||||||
$('#add-thangs-column').toggle()
|
$('#add-thangs-column').toggle()
|
||||||
@onWindowResize e
|
@onWindowResize e
|
||||||
|
|
||||||
|
undoAction: (e) ->
|
||||||
|
@thangsTreema.undo()
|
||||||
|
|
||||||
|
redoAction: (e) ->
|
||||||
|
@thangsTreema.redo()
|
||||||
|
|
||||||
class ThangsNode extends TreemaNode.nodeMap.array
|
class ThangsNode extends TreemaNode.nodeMap.array
|
||||||
valueClass: 'treema-array-replacement'
|
valueClass: 'treema-array-replacement'
|
||||||
getChildren: ->
|
getChildren: ->
|
||||||
|
|
188
app/views/modal/terrain_randomise_modal.coffee
Normal file
188
app/views/modal/terrain_randomise_modal.coffee
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
ModalView = require 'views/kinds/ModalView'
|
||||||
|
template = require 'templates/modal/terrain_randomise'
|
||||||
|
CocoModel = require 'models/CocoModel'
|
||||||
|
|
||||||
|
clusters = {
|
||||||
|
'rocks': ['Rock 1', 'Rock 2', 'Rock 3', 'Rock 4', 'Rock 5', 'Rock Cluster 1', 'Rock Cluster 2', 'Rock Cluster 3']
|
||||||
|
'trees': ['Tree 1', 'Tree 2', 'Tree 3', 'Tree 4']
|
||||||
|
'shrubs': ['Shrub 1', 'Shrub 2', 'Shrub 3']
|
||||||
|
'houses': ['House 1', 'House 2', 'House 3', 'House 4']
|
||||||
|
'animals': ['Cow', 'Horse']
|
||||||
|
'wood': ['Firewood 1', 'Firewood 2', 'Firewood 3', 'Barrel']
|
||||||
|
'farm': ['Farm']
|
||||||
|
}
|
||||||
|
|
||||||
|
presets = {
|
||||||
|
# 'dungeon': {
|
||||||
|
# 'type':'dungeon'
|
||||||
|
# 'borders':['Dungeon Wall']
|
||||||
|
# 'floors':['Dungeon Floor']
|
||||||
|
# 'decorations':[]
|
||||||
|
# }
|
||||||
|
'grassy': {
|
||||||
|
'type':'grassy'
|
||||||
|
'borders':['Tree 1', 'Tree 2', 'Tree 3']
|
||||||
|
'floors':['Grass01', 'Grass02', 'Grass03']
|
||||||
|
'decorations': {
|
||||||
|
'house': {
|
||||||
|
'num':[1,2] #min-max
|
||||||
|
'width': 20
|
||||||
|
'height': 20
|
||||||
|
'clusters': {
|
||||||
|
'houses':[1,1]
|
||||||
|
'trees':[1,2]
|
||||||
|
'shrubs':[0,3]
|
||||||
|
'rocks':[1,2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'farm': {
|
||||||
|
'num':[1,2] #min-max
|
||||||
|
'width': 20
|
||||||
|
'height': 20
|
||||||
|
'clusters': {
|
||||||
|
'farm':[1,1]
|
||||||
|
'shrubs':[2,3]
|
||||||
|
'wood':[2,4]
|
||||||
|
'animals':[2,3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sizes = {
|
||||||
|
'small': {
|
||||||
|
'x':80
|
||||||
|
'y':68
|
||||||
|
}
|
||||||
|
'large': {
|
||||||
|
'x':160
|
||||||
|
'y':136
|
||||||
|
}
|
||||||
|
'floorSize': {
|
||||||
|
'x':20
|
||||||
|
'y':20
|
||||||
|
}
|
||||||
|
'borderSize': {
|
||||||
|
'x':4
|
||||||
|
'y':4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = class TerrainRandomiseModal extends ModalView
|
||||||
|
id: 'terrain-randomise-modal'
|
||||||
|
template: template
|
||||||
|
thangs = []
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .choose-option': 'onRandomise'
|
||||||
|
|
||||||
|
onRevertModel: (e) ->
|
||||||
|
id = $(e.target).val()
|
||||||
|
CocoModel.backedUp[id].revert()
|
||||||
|
$(e.target).closest('tr').remove()
|
||||||
|
@reloadOnClose = true
|
||||||
|
|
||||||
|
onRandomise: (e) ->
|
||||||
|
target = $(e.target)
|
||||||
|
presetType = target.attr 'data-preset-type'
|
||||||
|
presetSize = target.attr 'data-preset-size'
|
||||||
|
@randomiseThangs presetType, presetSize
|
||||||
|
Backbone.Mediator.publish('randomise:terrain-generated',
|
||||||
|
'thangs': @thangs
|
||||||
|
)
|
||||||
|
@hide()
|
||||||
|
|
||||||
|
randomiseThangs: (presetName, presetSize) ->
|
||||||
|
preset = presets[presetName]
|
||||||
|
presetSize = sizes[presetSize]
|
||||||
|
@thangs = []
|
||||||
|
@randomiseFloor preset, presetSize
|
||||||
|
@randomiseBorder preset, presetSize
|
||||||
|
@randomiseDecorations preset, presetSize
|
||||||
|
|
||||||
|
randomiseFloor: (preset, presetSize) ->
|
||||||
|
for i in _.range(0, presetSize.x, sizes.floorSize.x)
|
||||||
|
for j in _.range(0, presetSize.y, sizes.floorSize.y)
|
||||||
|
@thangs.push {
|
||||||
|
'id': @getRandomThang(preset.floors)
|
||||||
|
'pos': {
|
||||||
|
'x': i
|
||||||
|
'y': j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
randomiseBorder: (preset, presetSize) ->
|
||||||
|
for i in _.range(0-sizes.floorSize.x/2+sizes.borderSize.x, presetSize.x-sizes.floorSize.x/2, sizes.borderSize.x)
|
||||||
|
@thangs.push {
|
||||||
|
'id': @getRandomThang(preset.borders)
|
||||||
|
'pos': {
|
||||||
|
'x': i
|
||||||
|
'y': 0-sizes.floorSize.x/2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@thangs.push {
|
||||||
|
'id': @getRandomThang(preset.borders)
|
||||||
|
'pos': {
|
||||||
|
'x': i
|
||||||
|
'y': presetSize.y - sizes.borderSize.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in _.range(0-sizes.floorSize.y/2, presetSize.y-sizes.borderSize.y, sizes.borderSize.y)
|
||||||
|
@thangs.push {
|
||||||
|
'id': @getRandomThang(preset.borders)
|
||||||
|
'pos': {
|
||||||
|
'x': 0-sizes.floorSize.x/2+sizes.borderSize.x
|
||||||
|
'y': i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@thangs.push {
|
||||||
|
'id': @getRandomThang(preset.borders)
|
||||||
|
'pos': {
|
||||||
|
'x': presetSize.x - sizes.borderSize.x - sizes.floorSize.x/2
|
||||||
|
'y': i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
randomiseDecorations: (preset, presetSize)->
|
||||||
|
for name, decoration of preset.decorations
|
||||||
|
for num in _.range(_.random(decoration.num[0], decoration.num[1]))
|
||||||
|
center =
|
||||||
|
{
|
||||||
|
'x':_.random(decoration.width, presetSize.x - decoration.width),
|
||||||
|
'y':_.random(decoration.height, presetSize.y - decoration.height)
|
||||||
|
}
|
||||||
|
min =
|
||||||
|
{
|
||||||
|
'x':center.x - decoration.width/2
|
||||||
|
'y':center.y - decoration.height/2
|
||||||
|
}
|
||||||
|
max =
|
||||||
|
{
|
||||||
|
'x':center.x + decoration.width/2
|
||||||
|
'y':center.y + decoration.height/2
|
||||||
|
}
|
||||||
|
for cluster, range of decoration.clusters
|
||||||
|
for i in _.range(_.random(range[0], range[1]))
|
||||||
|
@thangs.push {
|
||||||
|
'id':@getRandomThang(clusters[cluster])
|
||||||
|
'pos':{
|
||||||
|
'x':_.random(min.x, max.x)
|
||||||
|
'y':_.random(min.y, max.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getRandomThang: (thangList) ->
|
||||||
|
return thangList[_.random(0, thangList.length-1)]
|
||||||
|
|
||||||
|
getRenderData: ->
|
||||||
|
c = super()
|
||||||
|
models = _.values CocoModel.backedUp
|
||||||
|
models = (m for m in models when m.hasLocalChanges())
|
||||||
|
c.models = models
|
||||||
|
c
|
||||||
|
|
||||||
|
onHidden: ->
|
||||||
|
location.reload() if @reloadOnClose
|
56
scripts/mongodb/migrations/2014-07-09-name-conflicts.js
Normal file
56
scripts/mongodb/migrations/2014-07-09-name-conflicts.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
load('bower_components/lodash/dist/lodash.js');
|
||||||
|
load('bower_components/underscore.string/dist/underscore.string.min.js');
|
||||||
|
|
||||||
|
var slugs = {};
|
||||||
|
var num = 0;
|
||||||
|
|
||||||
|
var unconflictName;
|
||||||
|
|
||||||
|
unconflictName = function(name) {
|
||||||
|
var otherUser, suffix;
|
||||||
|
otherUser = db.users.findOne({
|
||||||
|
slug: _.string.slugify(name)
|
||||||
|
});
|
||||||
|
if (!otherUser) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
suffix = _.random(0, 9) + '';
|
||||||
|
return unconflictName(name + suffix);
|
||||||
|
};
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
name:1,
|
||||||
|
emails:1,
|
||||||
|
email:1,
|
||||||
|
slug:1,
|
||||||
|
dateCreated:1
|
||||||
|
};
|
||||||
|
|
||||||
|
db.users.find({anonymous:false}, params).sort({_id:1}).forEach(function (user) {
|
||||||
|
num += 1;
|
||||||
|
var slug = _.string.slugify(user.name);
|
||||||
|
if(!slug) return;
|
||||||
|
var update = {};
|
||||||
|
if(slugs[slug]) {
|
||||||
|
originalName = slugs[slug];
|
||||||
|
conflictingName = user.name;
|
||||||
|
availableName = unconflictName(conflictingName);
|
||||||
|
conflictingSlug = slug;
|
||||||
|
slug = _.string.slugify(availableName);
|
||||||
|
update.name = availableName;
|
||||||
|
update.nameLower = availableName.toLowerCase();
|
||||||
|
if (!(user.emails && user.emails.anyNotes === false))
|
||||||
|
db.changedEmails.insert({email:user.email, user:user._id, name:user.name});
|
||||||
|
print(_.str.sprintf('\n\n\tConflict! Username "%s" conflicts with "%s" (both sluggify to "%s"). Changing to "%s"\n\n\n',
|
||||||
|
conflictingName, originalName, conflictingSlug, availableName));
|
||||||
|
}
|
||||||
|
update.slug = slug;
|
||||||
|
slugs[slug] = user.name;
|
||||||
|
if(user.slug) return;
|
||||||
|
print(_.str.sprintf('Setting user %s (%s) to slug %s with update %s', user.name, user.dateCreated, slug, JSON.stringify({$set:update})));
|
||||||
|
var res = db.users.update({_id:user._id}, {$set:update});
|
||||||
|
if(res.hasWriteError()) {
|
||||||
|
print("\n\n\n\n\n\n\n\n\n\nOH NOOOOOOOOO\n\n\n\n\n\n\n");
|
||||||
|
db.changedEmails.insert({email:user.email, user:user._id, name:user.name, error:true});
|
||||||
|
}
|
||||||
|
});
|
|
@ -41,9 +41,12 @@ module.exports.NamedPlugin = (schema) ->
|
||||||
err.response = {message: ' is a reserved name', property: 'name'}
|
err.response = {message: ' is a reserved name', property: 'name'}
|
||||||
err.code = 422
|
err.code = 422
|
||||||
return next(err)
|
return next(err)
|
||||||
if newSlug isnt @get('slug')
|
if newSlug not in [@get('slug'), ''] and not @get 'anonymous'
|
||||||
@set('slug', newSlug)
|
@set('slug', newSlug)
|
||||||
@checkSlugConflicts(next)
|
@checkSlugConflicts(next)
|
||||||
|
else if newSlug is '' and @get 'slug'
|
||||||
|
@set 'slug', undefined
|
||||||
|
next()
|
||||||
else
|
else
|
||||||
next()
|
next()
|
||||||
)
|
)
|
||||||
|
|
|
@ -159,15 +159,11 @@ module.exports.setup = (app) ->
|
||||||
|
|
||||||
module.exports.loginUser = loginUser = (req, res, user, send=true, next=null) ->
|
module.exports.loginUser = loginUser = (req, res, user, send=true, next=null) ->
|
||||||
user.save((err) ->
|
user.save((err) ->
|
||||||
if err
|
return errors.serverError res, err if err?
|
||||||
return @sendDatabaseError(res, err)
|
|
||||||
|
|
||||||
req.logIn(user, (err) ->
|
req.logIn(user, (err) ->
|
||||||
if err
|
return errors.serverError res, err if err?
|
||||||
return @sendDatabaseError(res, err)
|
return res.send user if send
|
||||||
|
|
||||||
if send
|
|
||||||
return @sendSuccess(res, user)
|
|
||||||
next() if next
|
next() if next
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ crypto = require 'crypto'
|
||||||
{salt, isProduction} = require '../../server_config'
|
{salt, isProduction} = require '../../server_config'
|
||||||
mail = require '../commons/mail'
|
mail = require '../commons/mail'
|
||||||
log = require 'winston'
|
log = require 'winston'
|
||||||
|
plugins = require '../plugins/plugins'
|
||||||
|
|
||||||
sendwithus = require '../sendwithus'
|
sendwithus = require '../sendwithus'
|
||||||
|
|
||||||
|
@ -29,6 +30,9 @@ UserSchema.methods.isAdmin = ->
|
||||||
p = @get('permissions')
|
p = @get('permissions')
|
||||||
return p and 'admin' in p
|
return p and 'admin' in p
|
||||||
|
|
||||||
|
UserSchema.methods.isAnonymous = ->
|
||||||
|
@get 'anonymous'
|
||||||
|
|
||||||
UserSchema.methods.trackActivity = (activityName, increment) ->
|
UserSchema.methods.trackActivity = (activityName, increment) ->
|
||||||
now = new Date()
|
now = new Date()
|
||||||
increment ?= parseInt increment or 1
|
increment ?= parseInt increment or 1
|
||||||
|
@ -108,6 +112,30 @@ UserSchema.statics.updateMailChimp = (doc, callback) ->
|
||||||
|
|
||||||
mc?.lists.subscribe params, onSuccess, onFailure
|
mc?.lists.subscribe params, onSuccess, onFailure
|
||||||
|
|
||||||
|
UserSchema.statics.unconflictName = unconflictName = (name, done) ->
|
||||||
|
User.findOne {slug: _.str.slugify(name)}, (err, otherUser) ->
|
||||||
|
return done err if err?
|
||||||
|
return done null, name unless otherUser
|
||||||
|
suffix = _.random(0, 9) + ''
|
||||||
|
unconflictName name + suffix, done
|
||||||
|
|
||||||
|
UserSchema.methods.register = (done) ->
|
||||||
|
@set('anonymous', false)
|
||||||
|
@set('permissions', ['admin']) if not isProduction
|
||||||
|
if (name = @get 'name')? and name isnt ''
|
||||||
|
unconflictName name, (err, uniqueName) =>
|
||||||
|
return done err if err
|
||||||
|
@set 'name', uniqueName
|
||||||
|
done()
|
||||||
|
else done()
|
||||||
|
data =
|
||||||
|
email_id: sendwithus.templates.welcome_email
|
||||||
|
recipient:
|
||||||
|
address: @get 'email'
|
||||||
|
sendwithus.api.send data, (err, result) ->
|
||||||
|
log.error "sendwithus post-save error: #{err}, result: #{result}" if err
|
||||||
|
|
||||||
|
|
||||||
UserSchema.pre('save', (next) ->
|
UserSchema.pre('save', (next) ->
|
||||||
@set('emailLower', @get('email')?.toLowerCase())
|
@set('emailLower', @get('email')?.toLowerCase())
|
||||||
@set('nameLower', @get('name')?.toLowerCase())
|
@set('nameLower', @get('name')?.toLowerCase())
|
||||||
|
@ -115,15 +143,9 @@ UserSchema.pre('save', (next) ->
|
||||||
if @get('password')
|
if @get('password')
|
||||||
@set('passwordHash', User.hashPassword(pwd))
|
@set('passwordHash', User.hashPassword(pwd))
|
||||||
@set('password', undefined)
|
@set('password', undefined)
|
||||||
if @get('email') and @get('anonymous')
|
if @get('email') and @get('anonymous') # a user registers
|
||||||
@set('anonymous', false)
|
@register next
|
||||||
@set('permissions', ['admin']) if not isProduction
|
else
|
||||||
data =
|
|
||||||
email_id: sendwithus.templates.welcome_email
|
|
||||||
recipient:
|
|
||||||
address: @get 'email'
|
|
||||||
sendwithus.api.send data, (err, result) ->
|
|
||||||
log.error "sendwithus post-save error: #{err}, result: #{result}" if err
|
|
||||||
next()
|
next()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -136,6 +158,8 @@ UserSchema.statics.hashPassword = (password) ->
|
||||||
shasum.update(salt + password)
|
shasum.update(salt + password)
|
||||||
shasum.digest('hex')
|
shasum.digest('hex')
|
||||||
|
|
||||||
|
UserSchema.plugin plugins.NamedPlugin
|
||||||
|
|
||||||
module.exports = User = mongoose.model('User', UserSchema)
|
module.exports = User = mongoose.model('User', UserSchema)
|
||||||
|
|
||||||
AchievablePlugin = require '../plugins/achievements'
|
AchievablePlugin = require '../plugins/achievements'
|
||||||
|
|
|
@ -104,7 +104,8 @@ UserHandler = class UserHandler extends Handler
|
||||||
return callback(null, req, user) unless req.body.name
|
return callback(null, req, user) unless req.body.name
|
||||||
nameLower = req.body.name?.toLowerCase()
|
nameLower = req.body.name?.toLowerCase()
|
||||||
return callback(null, req, user) unless nameLower
|
return callback(null, req, user) unless nameLower
|
||||||
return callback(null, req, user) if nameLower is user.get('nameLower') and not user.get('anonymous')
|
return callback(null, req, user) if user.get 'anonymous' # anonymous users can have any name
|
||||||
|
return callback(null, req, user) if nameLower is user.get('nameLower')
|
||||||
User.findOne({nameLower: nameLower, anonymous: false}).exec (err, otherUser) ->
|
User.findOne({nameLower: nameLower, anonymous: false}).exec (err, otherUser) ->
|
||||||
log.error "Database error setting user name: #{err}" if err
|
log.error "Database error setting user name: #{err}" if err
|
||||||
return callback(res: 'Database error.', code: 500) if err
|
return callback(res: 'Database error.', code: 500) if err
|
||||||
|
@ -116,7 +117,7 @@ UserHandler = class UserHandler extends Handler
|
||||||
]
|
]
|
||||||
|
|
||||||
getById: (req, res, id) ->
|
getById: (req, res, id) ->
|
||||||
if req.user?._id.equals(id)
|
if Handler.isID(id) and req.user?._id.equals(id)
|
||||||
return @sendSuccess(res, @formatEntity(req, req.user, 256))
|
return @sendSuccess(res, @formatEntity(req, req.user, 256))
|
||||||
super(req, res, id)
|
super(req, res, id)
|
||||||
|
|
||||||
|
|
0
test/app/models/User.spec.coffee
Normal file
0
test/app/models/User.spec.coffee
Normal file
|
@ -44,7 +44,7 @@ describe 'User.updateMailChimp', ->
|
||||||
|
|
||||||
describe 'POST /db/user', ->
|
describe 'POST /db/user', ->
|
||||||
|
|
||||||
createAnonNameUser = (done)->
|
createAnonNameUser = (name, done)->
|
||||||
request.post getURL('/auth/logout'), ->
|
request.post getURL('/auth/logout'), ->
|
||||||
request.get getURL('/auth/whoami'), ->
|
request.get getURL('/auth/whoami'), ->
|
||||||
req = request.post(getURL('/db/user'), (err, response) ->
|
req = request.post(getURL('/db/user'), (err, response) ->
|
||||||
|
@ -52,11 +52,11 @@ describe 'POST /db/user', ->
|
||||||
request.get getURL('/auth/whoami'), (request, response, body) ->
|
request.get getURL('/auth/whoami'), (request, response, body) ->
|
||||||
res = JSON.parse(response.body)
|
res = JSON.parse(response.body)
|
||||||
expect(res.anonymous).toBeTruthy()
|
expect(res.anonymous).toBeTruthy()
|
||||||
expect(res.name).toEqual('Jim')
|
expect(res.name).toEqual(name)
|
||||||
done()
|
done()
|
||||||
)
|
)
|
||||||
form = req.form()
|
form = req.form()
|
||||||
form.append('name', 'Jim')
|
form.append('name', name)
|
||||||
|
|
||||||
it 'preparing test : clears the db first', (done) ->
|
it 'preparing test : clears the db first', (done) ->
|
||||||
clearModels [User], (err) ->
|
clearModels [User], (err) ->
|
||||||
|
@ -105,30 +105,18 @@ describe 'POST /db/user', ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'should allow setting anonymous user name', (done) ->
|
it 'should allow setting anonymous user name', (done) ->
|
||||||
createAnonNameUser(done)
|
createAnonNameUser('Jim', done)
|
||||||
|
|
||||||
it 'should allow multiple anonymous users with same name', (done) ->
|
it 'should allow multiple anonymous users with same name', (done) ->
|
||||||
createAnonNameUser(done)
|
createAnonNameUser('Jim', done)
|
||||||
|
|
||||||
|
|
||||||
it 'should not allow setting existing user name to anonymous user', (done) ->
|
|
||||||
|
|
||||||
createAnonUser = ->
|
|
||||||
request.post getURL('/auth/logout'), ->
|
|
||||||
request.get getURL('/auth/whoami'), ->
|
|
||||||
req = request.post(getURL('/db/user'), (err, response) ->
|
|
||||||
expect(response.statusCode).toBe(409)
|
|
||||||
done()
|
|
||||||
)
|
|
||||||
form = req.form()
|
|
||||||
form.append('name', 'Jim')
|
|
||||||
|
|
||||||
|
it 'should allow setting existing user name to anonymous user', (done) ->
|
||||||
req = request.post(getURL('/db/user'), (err, response, body) ->
|
req = request.post(getURL('/db/user'), (err, response, body) ->
|
||||||
expect(response.statusCode).toBe(200)
|
expect(response.statusCode).toBe(200)
|
||||||
request.get getURL('/auth/whoami'), (request, response, body) ->
|
request.get getURL('/auth/whoami'), (request, response, body) ->
|
||||||
res = JSON.parse(response.body)
|
res = JSON.parse(response.body)
|
||||||
expect(res.anonymous).toBeFalsy()
|
expect(res.anonymous).toBeFalsy()
|
||||||
createAnonUser()
|
createAnonNameUser 'Jim', done
|
||||||
)
|
)
|
||||||
form = req.form()
|
form = req.form()
|
||||||
form.append('email', 'new@user.com')
|
form.append('email', 'new@user.com')
|
||||||
|
@ -213,6 +201,55 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl
|
||||||
form.append('email', 'New@email.com')
|
form.append('email', 'New@email.com')
|
||||||
form.append('name', 'Wilhelm')
|
form.append('name', 'Wilhelm')
|
||||||
|
|
||||||
|
it 'should not allow two users with the same name slug', (done) ->
|
||||||
|
loginSam (sam) ->
|
||||||
|
samsName = sam.get 'name'
|
||||||
|
sam.set 'name', 'admin'
|
||||||
|
request.put {uri:getURL(urlUser + '/' + sam.id), json: sam.toObject()}, (err, response) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(response.statusCode).toBe 409
|
||||||
|
|
||||||
|
# Restore Sam
|
||||||
|
sam.set 'name', samsName
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'should silently rename an anonymous user if their name conflicts upon signup', (done) ->
|
||||||
|
request.post getURL('/auth/logout'), ->
|
||||||
|
request.get getURL('/auth/whoami'), ->
|
||||||
|
req = request.post getURL('/db/user'), (err, response) ->
|
||||||
|
expect(response.statusCode).toBe(200)
|
||||||
|
request.get getURL('/auth/whoami'), (err, response) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
guy = JSON.parse(response.body)
|
||||||
|
expect(guy.anonymous).toBeTruthy()
|
||||||
|
expect(guy.name).toEqual 'admin'
|
||||||
|
|
||||||
|
guy.email = 'blub@blub' # Email means registration
|
||||||
|
req = request.post {url: getURL('/db/user'), json: guy}, (err, response) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
finalGuy = response.body
|
||||||
|
expect(finalGuy.anonymous).toBeFalsy()
|
||||||
|
expect(finalGuy.name).not.toEqual guy.name
|
||||||
|
expect(finalGuy.name.length).toBe guy.name.length + 1
|
||||||
|
done()
|
||||||
|
form = req.form()
|
||||||
|
form.append('name', 'admin')
|
||||||
|
|
||||||
|
it 'should be able to unset a slug by setting an empty name', (done) ->
|
||||||
|
loginSam (sam) ->
|
||||||
|
samsName = sam.get 'name'
|
||||||
|
sam.set 'name', ''
|
||||||
|
request.put {uri:getURL(urlUser + '/' + sam.id), json: sam.toObject()}, (err, response) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(response.statusCode).toBe 200
|
||||||
|
newSam = response.body
|
||||||
|
|
||||||
|
# Restore Sam
|
||||||
|
sam.set 'name', samsName
|
||||||
|
request.put {uri:getURL(urlUser + '/' + sam.id), json: sam.toObject()}, (err, response) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
done()
|
||||||
|
|
||||||
describe 'GET /db/user', ->
|
describe 'GET /db/user', ->
|
||||||
|
|
||||||
it 'logs in as admin', (done) ->
|
it 'logs in as admin', (done) ->
|
||||||
|
@ -267,3 +304,28 @@ describe 'GET /db/user', ->
|
||||||
expect(response.statusCode).toBe(422)
|
expect(response.statusCode).toBe(422)
|
||||||
done()
|
done()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it 'can fetch myself by id completely', (done) ->
|
||||||
|
loginSam (sam) ->
|
||||||
|
request.get {url: getURL(urlUser + '/' + sam.id)}, (err, response) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(response.statusCode).toBe(200)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'can fetch myself by slug completely', (done) ->
|
||||||
|
loginSam (sam) ->
|
||||||
|
request.get {url: getURL(urlUser + '/sam')}, (err, response) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(response.statusCode).toBe(200)
|
||||||
|
guy = JSON.parse response.body
|
||||||
|
expect(guy._id).toBe sam.get('_id').toHexString()
|
||||||
|
expect(guy.name).toBe sam.get 'name'
|
||||||
|
done()
|
||||||
|
|
||||||
|
# TODO Ruben should be able to fetch other users but probably with restricted data access
|
||||||
|
# Add to the test case above an extra data check
|
||||||
|
|
||||||
|
xit 'can fetch another user with restricted fields'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue