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
|
||||
|
||||
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) ->
|
||||
n = +n.toFixed(digits)
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
GRAVATAR_URL = 'https://www.gravatar.com/'
|
||||
cache = {}
|
||||
CocoModel = require './CocoModel'
|
||||
util = require 'lib/utils'
|
||||
|
||||
module.exports = class User extends CocoModel
|
||||
@className: 'User'
|
||||
|
@ -45,6 +46,28 @@ module.exports = class User extends CocoModel
|
|||
cache[id] = 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: ->
|
||||
@migrateEmails()
|
||||
emails = _.clone(@get('emails')) or {}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
c = require './../schemas'
|
||||
emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification']
|
||||
|
||||
UserSchema = c.object {},
|
||||
name: c.shortString({title: 'Display Name', default: ''})
|
||||
UserSchema = c.object
|
||||
title: 'User'
|
||||
|
||||
c.extendNamedProperties UserSchema # let's have the name be the first property
|
||||
|
||||
_.extend UserSchema.properties,
|
||||
email: c.shortString({title: 'Email', format: 'email'})
|
||||
firstName: c.shortString({title: 'First 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
|
||||
li(class=anonymous ? "disabled": "")
|
||||
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": "")
|
||||
a(data-i18n="editor.pop_i18n")#pop-level-i18n-button Populate i18n
|
||||
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:double-clicked': 'onSpriteDoubleClicked'
|
||||
'surface:stage-mouse-up': 'onStageMouseUp'
|
||||
'randomise:terrain-generated': 'onRandomiseTerrain'
|
||||
|
||||
events:
|
||||
'click #extant-thangs-filter button': 'onFilterExtantThangs'
|
||||
|
@ -57,6 +58,8 @@ module.exports = class ThangsTabView extends View
|
|||
'delete, del, backspace': 'deleteSelectedExtantThang'
|
||||
'left': -> @moveAddThangSelection -1
|
||||
'right': -> @moveAddThangSelection 1
|
||||
'ctrl+z': 'undoAction'
|
||||
'ctrl+shift+z': 'redoAction'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
|
@ -221,6 +224,12 @@ module.exports = class ThangsTabView extends View
|
|||
return unless e.thang
|
||||
@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
|
||||
onExtantThangSelected: (e) ->
|
||||
@selectedExtantSprite?.setNameLabel? null unless @selectedExtantSprite is e.sprite
|
||||
|
@ -450,6 +459,12 @@ module.exports = class ThangsTabView extends View
|
|||
$('#add-thangs-column').toggle()
|
||||
@onWindowResize e
|
||||
|
||||
undoAction: (e) ->
|
||||
@thangsTreema.undo()
|
||||
|
||||
redoAction: (e) ->
|
||||
@thangsTreema.redo()
|
||||
|
||||
class ThangsNode extends TreemaNode.nodeMap.array
|
||||
valueClass: 'treema-array-replacement'
|
||||
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.code = 422
|
||||
return next(err)
|
||||
if newSlug isnt @get('slug')
|
||||
if newSlug not in [@get('slug'), ''] and not @get 'anonymous'
|
||||
@set('slug', newSlug)
|
||||
@checkSlugConflicts(next)
|
||||
else if newSlug is '' and @get 'slug'
|
||||
@set 'slug', undefined
|
||||
next()
|
||||
else
|
||||
next()
|
||||
)
|
||||
|
|
|
@ -159,15 +159,11 @@ module.exports.setup = (app) ->
|
|||
|
||||
module.exports.loginUser = loginUser = (req, res, user, send=true, next=null) ->
|
||||
user.save((err) ->
|
||||
if err
|
||||
return @sendDatabaseError(res, err)
|
||||
return errors.serverError res, err if err?
|
||||
|
||||
req.logIn(user, (err) ->
|
||||
if err
|
||||
return @sendDatabaseError(res, err)
|
||||
|
||||
if send
|
||||
return @sendSuccess(res, user)
|
||||
return errors.serverError res, err if err?
|
||||
return res.send user if send
|
||||
next() if next
|
||||
)
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ crypto = require 'crypto'
|
|||
{salt, isProduction} = require '../../server_config'
|
||||
mail = require '../commons/mail'
|
||||
log = require 'winston'
|
||||
plugins = require '../plugins/plugins'
|
||||
|
||||
sendwithus = require '../sendwithus'
|
||||
|
||||
|
@ -29,6 +30,9 @@ UserSchema.methods.isAdmin = ->
|
|||
p = @get('permissions')
|
||||
return p and 'admin' in p
|
||||
|
||||
UserSchema.methods.isAnonymous = ->
|
||||
@get 'anonymous'
|
||||
|
||||
UserSchema.methods.trackActivity = (activityName, increment) ->
|
||||
now = new Date()
|
||||
increment ?= parseInt increment or 1
|
||||
|
@ -108,6 +112,30 @@ UserSchema.statics.updateMailChimp = (doc, callback) ->
|
|||
|
||||
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) ->
|
||||
@set('emailLower', @get('email')?.toLowerCase())
|
||||
@set('nameLower', @get('name')?.toLowerCase())
|
||||
|
@ -115,16 +143,10 @@ UserSchema.pre('save', (next) ->
|
|||
if @get('password')
|
||||
@set('passwordHash', User.hashPassword(pwd))
|
||||
@set('password', undefined)
|
||||
if @get('email') and @get('anonymous')
|
||||
@set('anonymous', false)
|
||||
@set('permissions', ['admin']) if not isProduction
|
||||
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()
|
||||
if @get('email') and @get('anonymous') # a user registers
|
||||
@register next
|
||||
else
|
||||
next()
|
||||
)
|
||||
|
||||
UserSchema.post 'save', (doc) ->
|
||||
|
@ -136,6 +158,8 @@ UserSchema.statics.hashPassword = (password) ->
|
|||
shasum.update(salt + password)
|
||||
shasum.digest('hex')
|
||||
|
||||
UserSchema.plugin plugins.NamedPlugin
|
||||
|
||||
module.exports = User = mongoose.model('User', UserSchema)
|
||||
|
||||
AchievablePlugin = require '../plugins/achievements'
|
||||
|
|
|
@ -104,7 +104,8 @@ UserHandler = class UserHandler extends Handler
|
|||
return callback(null, req, user) unless req.body.name
|
||||
nameLower = req.body.name?.toLowerCase()
|
||||
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) ->
|
||||
log.error "Database error setting user name: #{err}" if err
|
||||
return callback(res: 'Database error.', code: 500) if err
|
||||
|
@ -116,7 +117,7 @@ UserHandler = class UserHandler extends Handler
|
|||
]
|
||||
|
||||
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))
|
||||
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', ->
|
||||
|
||||
createAnonNameUser = (done)->
|
||||
createAnonNameUser = (name, done)->
|
||||
request.post getURL('/auth/logout'), ->
|
||||
request.get getURL('/auth/whoami'), ->
|
||||
req = request.post(getURL('/db/user'), (err, response) ->
|
||||
|
@ -52,11 +52,11 @@ describe 'POST /db/user', ->
|
|||
request.get getURL('/auth/whoami'), (request, response, body) ->
|
||||
res = JSON.parse(response.body)
|
||||
expect(res.anonymous).toBeTruthy()
|
||||
expect(res.name).toEqual('Jim')
|
||||
expect(res.name).toEqual(name)
|
||||
done()
|
||||
)
|
||||
form = req.form()
|
||||
form.append('name', 'Jim')
|
||||
form.append('name', name)
|
||||
|
||||
it 'preparing test : clears the db first', (done) ->
|
||||
clearModels [User], (err) ->
|
||||
|
@ -105,30 +105,18 @@ describe 'POST /db/user', ->
|
|||
done()
|
||||
|
||||
it 'should allow setting anonymous user name', (done) ->
|
||||
createAnonNameUser(done)
|
||||
createAnonNameUser('Jim', done)
|
||||
|
||||
it 'should allow multiple anonymous users with same name', (done) ->
|
||||
createAnonNameUser(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')
|
||||
createAnonNameUser('Jim', done)
|
||||
|
||||
it 'should allow setting existing user name to anonymous user', (done) ->
|
||||
req = request.post(getURL('/db/user'), (err, response, body) ->
|
||||
expect(response.statusCode).toBe(200)
|
||||
request.get getURL('/auth/whoami'), (request, response, body) ->
|
||||
res = JSON.parse(response.body)
|
||||
expect(res.anonymous).toBeFalsy()
|
||||
createAnonUser()
|
||||
createAnonNameUser 'Jim', done
|
||||
)
|
||||
form = req.form()
|
||||
form.append('email', 'new@user.com')
|
||||
|
@ -213,6 +201,55 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl
|
|||
form.append('email', 'New@email.com')
|
||||
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', ->
|
||||
|
||||
it 'logs in as admin', (done) ->
|
||||
|
@ -267,3 +304,28 @@ describe 'GET /db/user', ->
|
|||
expect(response.statusCode).toBe(422)
|
||||
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