Merge branch 'master' into production

This commit is contained in:
Nick Winter 2015-11-12 15:36:26 -08:00
commit 9ab2ccd905
9 changed files with 170 additions and 71 deletions

View file

@ -725,6 +725,8 @@
wrong_password: "Wrong Password"
upload_picture: "Upload a picture"
delete_this_account: "Delete this account permanently"
reset_progress_tab: "Reset All Progress"
reset_your_progress: "Clear all your progress and start over"
god_mode: "God Mode"
password_tab: "Password"
emails_tab: "Emails"
@ -732,6 +734,7 @@
new_password: "New Password"
new_password_verify: "Verify"
type_in_email: "Type in your email to confirm account deletion."
type_in_email_progress: "Type in your email to confirm deleting your progress."
type_in_password: "Also, type in your password."
email_subscriptions: "Email Subscriptions"
email_subscriptions_none: "No Email Subscriptions."

View file

@ -52,13 +52,13 @@ else
.panel-heading
.panel-title#delete-account-panel-title(data-i18n="account_settings.delete_account_tab")
.panel-body
.form
.form#delete-account-form
.form-group
label.control-label(for="email1", data-i18n="account_settings.type_in_email")
input#email1.form-control(name="email1", type="text")
.form-group
input#email1.form-control(name="email", type="email")
.form-group
label.control-label(for="password1", data-i18n="account_settings.type_in_password")
input#password1.form-control(name="password1", type="password")
input#password1.form-control(name="password", type="password")
button#delete-account-btn.btn.form-control.btn-primary(data-i18n="account_settings.delete_this_account")
.col-md-6
@ -96,7 +96,7 @@ else
hr
h4(data-i18n="account_settings.contributor_emails") Contributor Class Emails
span(data-i18n="account_settings.contribute_prefix") We're looking for people to join our party! Check out the
span(data-i18n="account_settings.contribute_prefix") We\'re looking for people to join our party! Check out the
a(href="/contribute", data-i18n="account_settings.contribute_page") contribute page
span(data-i18n="account_settings.contribute_suffix") to find out more.
@ -163,4 +163,19 @@ else
button#toggle-all-btn.btn.btn-primary.form-control(data-i18n="account_settings.email_toggle") Toggle All
.panel.panel-default
.panel-heading
.panel-title#reset-progress-panel-title(data-i18n="account_settings.reset_progress_tab")
.panel-body
.form#reset-progress-form
.form-group
label.control-label(for="email-reset-progress", data-i18n="account_settings.type_in_email_progress")
input#email-reset-progress.form-control(name="email", type="email")
.form-group
label.control-label(for="password-reset-progress", data-i18n="account_settings.type_in_password")
input#password-reset-progress.form-control(name="password", type="password")
button#reset-progress-btn.btn.form-control.btn-primary(data-i18n="account_settings.reset_your_progress")
.clearfix

View file

@ -18,6 +18,7 @@ module.exports = class AccountSettingsView extends CocoView
'click #toggle-all-btn': 'onClickToggleAllButton'
'click #profile-photo-panel-body': 'onClickProfilePhotoPanelBody'
'click #delete-account-btn': 'onClickDeleteAccountButton'
'click #reset-progress-btn': 'onClickResetProgressButton'
constructor: (options) ->
super options
@ -62,61 +63,68 @@ module.exports = class AccountSettingsView extends CocoView
@trigger 'inputChanged', e
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
onClickDeleteAccountButton: (e) ->
@validateCredentialsForDestruction @$el.find('#delete-account-form'), =>
renderData =
confirmTitle: 'Are you really sure?'
confirmBody: 'This will completely delete your account. This action CANNOT be undone. Are you entirely sure?'
confirmDecline: 'Not really'
confirmConfirm: 'Definitely'
confirmModal = new ConfirmModal renderData
confirmModal.on 'confirm', @deleteAccount
@openModalView confirmModal
#- Just copied from OptionsView, TODO refactor
onClickDeleteAccountButton: ->
forms.clearFormAlerts(@$el)
myEmail = me.get 'email'
email1 = document.getElementById('email1').value
password1 = document.getElementById('password1').value
if Boolean(email1) and email1 is myEmail
onClickResetProgressButton: ->
@validateCredentialsForDestruction @$el.find('#reset-progress-form'), =>
renderData =
confirmTitle: 'Are you really sure?'
confirmBody: 'This will completely erase your progress: code, levels, achievements, earned gems, etc. This action CANNOT be undone. Are you entirely sure?'
confirmDecline: 'Not really'
confirmConfirm: 'Definitely'
confirmModal = new ConfirmModal renderData
confirmModal.on 'confirm', @resetProgress
@openModalView confirmModal
validateCredentialsForDestruction: ($form, onSuccess) ->
forms.clearFormAlerts($form)
enteredEmail = $form.find('input[type="email"]').val()
enteredPassword = $form.find('input[type="password"]').val()
if enteredEmail and enteredEmail is me.get('email')
isPasswordCorrect = false
toBeDelayed = true
$.ajax
url: '/auth/login'
type: 'POST'
data:
{
username: email1,
password: password1
}
username: enteredEmail
password: enteredPassword
parse: true
error: (error) ->
toBeDelayed = false
'Bad Error. Can\'t connect to server or something. ' + error
success: (response, textStatus, jqXHR) ->
toBeDelayed = false
unless jqXHR.status is 200
return
return unless jqXHR.status is 200
isPasswordCorrect = true
callback = =>
if toBeDelayed
setTimeout callback, 100
else
if isPasswordCorrect
renderData =
'confirmTitle': 'Are you really sure?'
'confirmBody': 'This will completely delete your account. This action CANNOT be undone. Are you entirely sure?'
'confirmDecline': 'Not really'
'confirmConfirm': 'Definitely'
confirmModal = new ConfirmModal renderData
confirmModal.on 'confirm', @deleteAccount
@openModalView confirmModal
onSuccess()
else
message = $.i18n.t('account_settings.wrong_password', defaultValue: 'Wrong Password.')
err = [message: message, property: 'password1', formatted: true]
forms.applyErrorsToForm(@$el, err)
err = [message: message, property: 'password', formatted: true]
forms.applyErrorsToForm($form, err)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
setTimeout callback, 100
else
message = $.i18n.t('account_settings.wrong_email', defaultValue: 'Wrong Email.')
err = [message: message, property: 'email1', formatted: true]
forms.applyErrorsToForm(@$el, err)
err = [message: message, property: 'email', formatted: true]
forms.applyErrorsToForm($form, err)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
deleteAccount: ->
myID = me.id
$.ajax
type: 'DELETE'
success: ->
@ -133,11 +141,33 @@ module.exports = class AccountSettingsView extends CocoView
, 500
error: (jqXHR, status, error) ->
console.error jqXHR
timeout: 5000
text: "Deleting account failed with error code #{jqXHR.status}"
type: 'error'
layout: 'topCenter'
url: "/db/user/#{myID}"
noty
timeout: 5000
text: "Deleting account failed with error code #{jqXHR.status}"
type: 'error'
layout: 'topCenter'
url: "/db/user/#{me.id}"
resetProgress: ->
$.ajax
type: 'POST'
success: ->
noty
timeout: 5000
text: 'Your progress is gone.'
type: 'success'
layout: 'topCenter'
localStorage.clear()
me.fetch cache: false
_.delay (-> window.location.reload()), 1000
error: (jqXHR, status, error) ->
console.error jqXHR
noty
timeout: 5000
text: "Resetting progress failed with error code #{jqXHR.status}"
type: 'error'
layout: 'topCenter'
url: "/db/user/#{me.id}/reset_progress"
onClickProfilePhotoPanelBody: (e) ->
return if window.application.isIPadApp # TODO: have an iPad-native way of uploading a photo, since we don't want to load FilePicker on iPad (memory)

View file

@ -68,6 +68,8 @@ module.exports = class AdministerUserModal extends ModalView
maxRedeemers: maxRedeemers
type: 'course'
creator: @user.id
properties:
adminAdded: me.id
})
prepaid.save()
@state = 'creating-prepaid'

View file

@ -87,6 +87,7 @@ module.exports = class PollModal extends ModalView
randomNumber = reward.random
randomGems = Math.ceil 2 * randomNumber * reward.level
totalGems = if @previousReward then me.gems() else Math.round me.gems() + randomGems
playSound = @playSound
if @previousReward
utils.replaceText @$randomNumber.show(), commentStart + randomNumber.toFixed(7)
@ -103,7 +104,7 @@ module.exports = class PollModal extends ModalView
if Math.random() < randomGems / 40
gemTrigger = 'gem-' + (gemNoisesPlayed % 4) # 4 gem sounds
++gemNoisesPlayed
@playSound gemTrigger, (0.475 + i / 2000)
playSound gemTrigger, (0.475 + i / 2000)
@$randomNumber.delay 25
@$randomGems.delay(1100).queue ->
utils.replaceText $(@), commentStart + randomGems

View file

@ -32,6 +32,22 @@ module.exports.handlers =
'trial_request': 'trial_requests/trial_request_handler'
'user_polls_record': 'polls/user_polls_record_handler'
module.exports.handlerUrlOverrides =
'analytics_log_event': 'analytics_log_event'
'analytics_perday': 'analytics.perday'
'analytics_string': 'analytics.string'
'analytics_stripe_invoice': 'analytics.stripe.invoice'
'level_component': 'level.component'
'level_feedback': 'level.feedback'
'level_session': 'level.session'
'level_system': 'level.system'
'thang_type': 'thang.type'
'thang_component': 'thang.component'
'user_code_problem': 'user.code.problem'
'user_remark': 'user.remark'
'mail_sent': 'mail.sent'
'user_polls_record': 'user.polls.record'
module.exports.routes =
[
'routes/admin'

View file

@ -15,7 +15,7 @@ PrepaidSchema.statics.generateNewCode = (done) ->
return done(code) unless prepaid
tryCode()
tryCode()
PrepaidSchema.pre('save', (next) ->
@set('exhausted', @get('maxRedeemers') <= _.size(@get('redeemers')))
if not @get('code')
@ -30,7 +30,7 @@ PrepaidSchema.post 'init', (doc) ->
doc.set('maxRedeemers', parseInt(doc.get('maxRedeemers')))
PrepaidSchema.statics.postEditableProperties = [
'creator', 'maxRedeemers', 'type'
'creator', 'maxRedeemers', 'properties', 'type'
]
module.exports = Prepaid = mongoose.model('prepaid', PrepaidSchema)

View file

@ -1,6 +1,7 @@
log = require 'winston'
errors = require '../commons/errors'
handlers = require('../commons/mapping').handlers
handlerUrlOverrides = require('../commons/mapping').handlerUrlOverrides
mongoose = require 'mongoose'
hipchat = require '../hipchat'
@ -19,40 +20,55 @@ module.exports.setup = (app) ->
res.send docs
res.end
app.all '/db/*', (req, res) ->
bindHandler = (name, module) ->
routeHandler = (req, res) ->
res.setHeader('Content-Type', 'application/json')
parts = req.path[4..].split('/')
if (not req.user) and req.route.method isnt 'get'
return errors.unauthorized(res, 'Must have an identity to do anything with the db. Do you have cookies enabled?')
try
handler = require('../' + name)
return handler.getLatestVersion(req, res, parts[1], parts[3]) if parts[2] is 'version'
return handler.versions(req, res, parts[1]) if parts[2] is 'versions'
return handler.files(req, res, parts[1]) if parts[2] is 'files'
return handler.getNamesByIDs(req, res) if req.route.method in ['get', 'post'] and parts[1] is 'names'
return handler.getByRelationship(req, res, parts[1..]...) if parts.length > 2
return handler.getById(req, res, parts[1]) if req.route.method is 'get' and parts[1]?
return handler.patch(req, res, parts[1]) if req.route.method is 'patch' and parts[1]?
handler[req.route.method](req, res, parts[1..]...)
catch error
errorMessage = "Error trying db method #{req?.route?.method} route #{parts} from #{name}: #{error}"
if req.user?
userInfo = req.user.getUserInfo()
errorMessage += "\n-- User Info Id: #{userInfo.id} #{if req.user.isAnonymous() then '' else 'Email:'} #{userInfo.email}"
log.error(errorMessage)
log.error(error)
log.error(error.stack)
# TODO: Generally ignore this error: error: Error trying db method get route analytics.log.event from undefined: Error: Cannot find module '../undefined'
unless "#{parts}" in ['analytics.users.active']
hipchat.sendHipChatMessage errorMessage, ['tower'], papertrail: true
errors.notFound(res, "Route #{req?.path} not found.")
app.all '/db/' + moduleName + '/*', routeHandler
app.all '/db/' + moduleName, routeHandler
app.get '/db/:module/schema', (req, res) ->
res.setHeader('Content-Type', 'application/json')
module = req.path[4..]
getSchema req, res, req.params.module
parts = module.split('/')
module = parts[0]
return getSchema(req, res, module) if parts[1] is 'schema'
if (not req.user) and req.route.method isnt 'get'
return errors.unauthorized(res, 'Must have an identity to do anything with the db. Do you have cookies enabled?')
for moduleNameUnder of handlers
name = handlers[moduleNameUnder]
moduleName = handlerUrlOverrides[moduleNameUnder] or moduleNameUnder
bindHandler name, moduleName
try
moduleName = module.replace new RegExp('\\.', 'g'), '_'
name = handlers[moduleName]
handler = require('../' + name)
return handler.getLatestVersion(req, res, parts[1], parts[3]) if parts[2] is 'version'
return handler.versions(req, res, parts[1]) if parts[2] is 'versions'
return handler.files(req, res, parts[1]) if parts[2] is 'files'
return handler.getNamesByIDs(req, res) if req.route.method in ['get', 'post'] and parts[1] is 'names'
return handler.getByRelationship(req, res, parts[1..]...) if parts.length > 2
return handler.getById(req, res, parts[1]) if req.route.method is 'get' and parts[1]?
return handler.patch(req, res, parts[1]) if req.route.method is 'patch' and parts[1]?
handler[req.route.method](req, res, parts[1..]...)
catch error
errorMessage = "Error trying db method #{req?.route?.method} route #{parts} from #{name}: #{error}"
if req.user?
userInfo = req.user.getUserInfo()
errorMessage += "\n-- User Info Id: #{userInfo.id} #{if req.user.isAnonymous() then '' else 'Email:'} #{userInfo.email}"
log.error(errorMessage)
log.error(error)
log.error(error.stack)
# TODO: Generally ignore this error: error: Error trying db method get route analytics.log.event from undefined: Error: Cannot find module '../undefined'
unless "#{parts}" in ['analytics.users.active']
hipchat.sendHipChatMessage errorMessage, ['tower'], papertrail: true
errors.notFound(res, "Route #{req?.path} not found.")
# Fall back for URLs that start with /db/ but dont map to a handler.
app.all '/db/*', (req, res) ->
res.setHeader('Content-Type', 'text/plain')
errors.notFound(res, "Route #{req?.path} not found.")
getSchema = (req, res, moduleName) ->
try

View file

@ -24,6 +24,8 @@ UserRemark = require './remarks/UserRemark'
hipchat = require '../hipchat'
sendwithus = require '../sendwithus'
Prepaid = require '../prepaids/Prepaid'
UserPollsRecord = require '../polls/UserPollsRecord'
EarnedAchievement = require '../achievements/EarnedAchievement'
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset', 'lastIP']
candidateProperties = [
@ -326,6 +328,7 @@ UserHandler = class UserHandler extends Handler
return @getSubSponsor(req, res) if args[1] is 'sub_sponsor'
return @getSubSponsors(req, res) if args[1] is 'sub_sponsors'
return @sendOneTimeEmail(req, res, args[0]) if args[1] is 'send_one_time_email'
return @resetProgress(req, res, args[0]) if args[1] is 'reset_progress'
return @sendNotFoundError(res)
super(arguments...)
@ -697,6 +700,19 @@ UserHandler = class UserHandler extends Handler
return @sendDatabaseError res, err if err
@sendSuccess res, users
resetProgress: (req, res, userID) ->
return @sendMethodNotAllowed res unless req.method is 'POST'
return @sendForbiddenError res unless userID and userID is req.user?._id + '' # Only you can reset your own progress
return @sendForbiddenError res if req.user?.isAdmin() # Protect admins from resetting their progress
async.parallel [
(cb) -> LevelSession.remove {creator: req.user._id + ''}, cb
(cb) -> EarnedAchievement.remove {user: req.user._id + ''}, cb
(cb) -> UserPollsRecord.remove {user: req.user._id + ''}, cb
(cb) -> req.user.update {points: 0, 'stats.gamesCompleted': 0, 'stats.concepts': {}, 'earned.gems': 0, 'earned.levels': [], 'earned.items': [], 'earned.heroes': [], 'purchased.items': [], 'purchased.heroes': [], spent: 0}, cb
], (err, results) =>
return @sendDatabaseError res, err if err
@sendSuccess res, result: 'success'
countEdits = (model, done) ->
statKey = User.statsMapping.edits[model.modelName]