This commit is contained in:
Nick Winter 2014-04-22 11:11:10 -07:00
commit ab77176ed0
23 changed files with 298 additions and 97 deletions

View file

@ -323,7 +323,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
if not action and @thang?.actionActivated and not @stopLogging if not action and @thang?.actionActivated and not @stopLogging
console.error "action is", action, "for", @thang?.id, "from", @currentRootAction, @thang.action, @thang.getActionName?() console.error "action is", action, "for", @thang?.id, "from", @currentRootAction, @thang.action, @thang.getActionName?()
@stopLogging = true @stopLogging = true
@queueAction(action) if isDifferent or (@thang?.actionActivated and action.name isnt 'move') @queueAction(action) if action and (isDifferent or (@thang?.actionActivated and action.name isnt 'move'))
@updateActionDirection() @updateActionDirection()
determineAction: -> determineAction: ->
@ -332,8 +332,11 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
action = thang.action if thang?.acts action = thang.action if thang?.acts
action ?= @currentRootAction.name if @currentRootAction? action ?= @currentRootAction.name if @currentRootAction?
action ?= 'idle' action ?= 'idle'
action = null unless @actions[action]? unless @actions[action]?
return null unless action @warnedFor ?= {}
console.warn 'Cannot show action', action, 'for', @thangType.get('name'), 'because it DNE' unless @warnedFor[action]
@warnedFor[action] = true
return null
action = 'break' if @actions.break? and @thang?.erroredOut action = 'break' if @actions.break? and @thang?.erroredOut
action = 'die' if @actions.die? and thang?.health? and thang.health <= 0 action = 'die' if @actions.die? and thang?.health? and thang.health <= 0
@actions[action] @actions[action]

View file

@ -119,6 +119,7 @@ class CocoModel extends Backbone.Model
@set prop, defaultValue @set prop, defaultValue
for prop, sch of @constructor.schema.properties or {} for prop, sch of @constructor.schema.properties or {}
continue if @get(prop)? continue if @get(prop)?
continue if prop is 'emails' # hack, defaults are handled through User.coffee's email-specific methods.
#console.log "setting", prop, "to", sch.default, "from sch.default" if sch.default? #console.log "setting", prop, "to", sch.default, "from sch.default" if sch.default?
@set prop, sch.default if sch.default? @set prop, sch.default if sch.default?
if @loaded if @loaded

View file

@ -9,6 +9,7 @@ module.exports = class User extends CocoModel
initialize: -> initialize: ->
super() super()
@migrateEmails()
isAdmin: -> isAdmin: ->
permissions = @attributes['permissions'] or [] permissions = @attributes['permissions'] or []
@ -43,3 +44,33 @@ module.exports = class User extends CocoModel
) )
cache[id] = user cache[id] = user
user user
getEnabledEmails: ->
@migrateEmails()
emails = _.clone(@get('emails')) or {}
emails = _.defaults emails, @schema().properties.emails.default
(emailName for emailName, emailDoc of emails when emailDoc.enabled)
setEmailSubscription: (name, enabled) ->
newSubs = _.clone(@get('emails')) or {}
(newSubs[name] ?= {}).enabled = enabled
@set 'emails', newSubs
emailMap:
announcement: 'generalNews'
developer: 'archmageNews'
tester: 'adventurerNews'
level_creator: 'artisanNews'
article_editor: 'scribeNews'
translator: 'diplomatNews'
support: 'ambassadorNews'
notification: 'anyNotes'
migrateEmails: ->
return if @attributes.emails or not @attributes.emailSubscriptions
oldSubs = @get('emailSubscriptions') or []
newSubs = {}
newSubs[newSubName] = { enabled: oldSubName in oldSubs } for oldSubName, newSubName of @emailMap
@set('emails', newSubs)
isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled

View file

@ -20,7 +20,20 @@ UserSchema = c.object {},
autocastDelay: {type: 'integer', 'default': 5000 } autocastDelay: {type: 'integer', 'default': 5000 }
lastLevel: { type: 'string' } lastLevel: { type: 'string' }
emailSubscriptions: c.array {uniqueItems: true, 'default': ['announcement', 'notification']}, {'enum': emailSubscriptions} emailSubscriptions: c.array {uniqueItems: true}, {'enum': emailSubscriptions}
emails: c.object {title: "Email Settings", default: {generalNews: {enabled:true}, anyNotes: {enabled:true}, recruitNotes: {enabled:true}}},
# newsletters
generalNews: { $ref: '#/definitions/emailSubscription' }
adventurerNews: { $ref: '#/definitions/emailSubscription' }
ambassadorNews: { $ref: '#/definitions/emailSubscription' }
archmageNews: { $ref: '#/definitions/emailSubscription' }
artisanNews: { $ref: '#/definitions/emailSubscription' }
diplomatNews: { $ref: '#/definitions/emailSubscription' }
scribeNews: { $ref: '#/definitions/emailSubscription' }
# notifications
anyNotes: { $ref: '#/definitions/emailSubscription' } # overrides any other notifications settings
recruitNotes: { $ref: '#/definitions/emailSubscription' }
# server controlled # server controlled
permissions: c.array {'default': []}, c.shortString() permissions: c.array {'default': []}, c.shortString()
@ -99,4 +112,10 @@ UserSchema = c.object {},
c.extendBasicProperties UserSchema, 'user' c.extendBasicProperties UserSchema, 'user'
c.definitions =
emailSubscription =
enabled: {type: 'boolean'}
lastSent: c.date()
count: {type: 'integer'}
module.exports = UserSchema module.exports = UserSchema

View file

@ -44,6 +44,12 @@
.form .form
max-width: 600px max-width: 600px
#email-pane
#specific-notification-settings
padding-left: 20px
margin-left: 20px
border-left: 1px solid gray
#job-profile-view #job-profile-view
.profile-preview-button .profile-preview-button
&.bottom-preview &.bottom-preview

View file

@ -67,16 +67,28 @@ block content
p p
.form .form
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_announcement", data-i18n="account_settings.email_announcements") Announcements label.control-label(for="email_generalNews", data-i18n="account_settings.email_announcements") Announcements
input#email_announcement(name="email_announcement", type="checkbox", checked=subs.announcement) input#email_generalNews(name="email_generalNews", type="checkbox", checked=subs.generalNews)
span.help-block(data-i18n="account_settings.email_announcements_description") Get emails on the latest news and developments at CodeCombat. span.help-block(data-i18n="account_settings.email_announcements_description") Get emails on the latest news and developments at CodeCombat.
hr
h4(data-i18n="account_settings.email_notifications") Notifications
span Controls for transactional emails, ie emails specific to your account.
.form .form
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_notification", data-i18n="account_settings.email_notifications") Notifications label.control-label(for="email_anyNotes", data-i18n="account_settings.any_notifications") Any Notifications
input#email_notification(name="email_notification", type="checkbox", checked=subs.notification) input#email_anyNotes(name="email_anyNotes", type="checkbox", checked=subs.anyNotes)
span.help-block(data-i18n="account_settings.email_notifications_description") Get periodic notifications for your account. span.help-block(data-i18n="account_settings.email_any_notes_description") Disable to universally stop ALL notifications for this account.
hr
fieldset#specific-notification-settings
.form-group.checkbox
label.control-label(for="email_recruitNotes", data-i18n="account_settings.recruit_notes") Recruitment Opportunities
input#email_recruitNotes(name="email_recruitNotes", type="checkbox", checked=subs.recruitNotes)
span.help-block(data-i18n="account_settings.email_recruit_notes_description") If you play really well, we may contact you about getting you a (better) job.
hr
h4(data-i18n="account_settings.contributor_emails") Contributor Class Emails 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
@ -85,63 +97,63 @@ block content
.form .form
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_developer") label.control-label(for="email_archmageNews")
span(data-i18n="classes.archmage_title") span(data-i18n="classes.archmage_title")
| Archmage | Archmage
| |
span(data-i18n="classes.archmage_title_description") span(data-i18n="classes.archmage_title_description")
| (Coder) | (Coder)
input#email_developer(name="email_developer", type="checkbox", checked=subs.developer) input#email_archmageNews(name="email_archmageNews", type="checkbox", checked=subs.archmageNews)
span(data-i18n="contribute.archmage_subscribe_desc").help-block Get emails about general news and announcements about CodeCombat. span(data-i18n="contribute.archmage_subscribe_desc").help-block Get emails about general news and announcements about CodeCombat.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_level_creator") label.control-label(for="email_artisanNews")
span(data-i18n="classes.artisan_title") span(data-i18n="classes.artisan_title")
| Artisan | Artisan
| |
span(data-i18n="classes.artisan_title_description") span(data-i18n="classes.artisan_title_description")
| (Level Builder) | (Level Builder)
input#email_level_creator(name="email_level_creator", type="checkbox", checked=subs.level_creator) input#email_artisanNews(name="email_artisanNews", type="checkbox", checked=subs.artisanNews)
span(data-i18n="contribute.artisan_subscribe_desc").help-block Get emails on level editor updates and announcements. span(data-i18n="contribute.artisan_subscribe_desc").help-block Get emails on level editor updates and announcements.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_tester") label.control-label(for="email_adventurerNews")
span(data-i18n="classes.adventurer_title") span(data-i18n="classes.adventurer_title")
| Adventurer | Adventurer
| |
span(data-i18n="classes.adventurer_title_description") span(data-i18n="classes.adventurer_title_description")
| (Level Playtester) | (Level Playtester)
input#email_tester(name="email_tester", type="checkbox", checked=subs.tester) input#email_adventurerNews(name="email_adventurerNews", type="checkbox", checked=subs.adventurerNews)
span(data-i18n="contribute.adventurer_subscribe_desc").help-block Get emails when there are new levels to test. span(data-i18n="contribute.adventurer_subscribe_desc").help-block Get emails when there are new levels to test.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_article_editor") label.control-label(for="email_scribeNews")
span(data-i18n="classes.scribe_title") span(data-i18n="classes.scribe_title")
| Scribe | Scribe
| |
span(data-i18n="classes.scribe_title_description") span(data-i18n="classes.scribe_title_description")
| (Article Editor) | (Article Editor)
input#email_article_editor(name="email_article_editor", type="checkbox", checked=subs.article_editor) input#email_scribeNews(name="email_scribeNews", type="checkbox", checked=subs.scribeNews)
span(data-i18n="contribute.scribe_subscribe_desc").help-block Get emails about article writing announcements. span(data-i18n="contribute.scribe_subscribe_desc").help-block Get emails about article writing announcements.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_translator") label.control-label(for="email_diplomatNews")
span(data-i18n="classes.diplomat_title") span(data-i18n="classes.diplomat_title")
| Diplomat | Diplomat
| |
span(data-i18n="classes.diplomat_title_description") span(data-i18n="classes.diplomat_title_description")
| (Translator) | (Translator)
input#email_translator(name="email_translator", type="checkbox", checked=subs.translator) input#email_diplomatNews(name="email_diplomatNews", type="checkbox", checked=subs.diplomatNews)
span(data-i18n="contribute.diplomat_subscribe_desc").help-block Get emails about i18n developments and, eventually, levels to translate. span(data-i18n="contribute.diplomat_subscribe_desc").help-block Get emails about i18n developments and, eventually, levels to translate.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_support") label.control-label(for="email_ambassadorNews")
span(data-i18n="classes.ambassador_title") span(data-i18n="classes.ambassador_title")
| Ambassador | Ambassador
| |
span(data-i18n="classes.ambassador_title_description") span(data-i18n="classes.ambassador_title_description")
| (Support) | (Support)
input#email_support(name="email_support", type="checkbox", checked=subs.support) input#email_ambassadorNews(name="email_ambassadorNews", type="checkbox", checked=subs.ambassadorNews)
span(data-i18n="contribute.ambassador_subscribe_desc").help-block Get emails on support updates and multiplayer developments. span(data-i18n="contribute.ambassador_subscribe_desc").help-block Get emails on support updates and multiplayer developments.
button.btn#toggle-all-button(data-i18n="account_settings.email_toggle") Toggle All button.btn#toggle-all-button(data-i18n="account_settings.email_toggle") Toggle All

View file

@ -171,7 +171,7 @@ block content
| We are trying to build a community, and every community needs a support team when | We are trying to build a community, and every community needs a support team when
| there are troubles. We have got chats, emails, and social networks so that our users | there are troubles. We have got chats, emails, and social networks so that our users
| can get acquainted with the game. If you want to help people get involved, have fun, | can get acquainted with the game. If you want to help people get involved, have fun,
| and learn some programming, then this class is for you. | and learn some programming, then this c lass is for you.
a(href="/contribute/ambassador") a(href="/contribute/ambassador")
p.lead(data-i18n="contribute.more_about_ambassador") p.lead(data-i18n="contribute.more_about_ambassador")

View file

@ -1,5 +1,5 @@
label.checkbox(for=contributorClassID).well label.checkbox(for=contributorClassName).well
input(type='checkbox', name=contributorClassID, id=contributorClassID) input(type='checkbox', name=contributorClassName, id=contributorClassName)
span(data-i18n="contribute.#{contributorClassName}_subscribe_desc") span(data-i18n="contribute.#{contributorClassName}_subscribe_desc")
.saved-notification ✓ Saved .saved-notification ✓ Saved

View file

@ -65,20 +65,19 @@ module.exports = class SettingsView extends View
c = super() c = super()
return c unless me return c unless me
c.subs = {} c.subs = {}
c.subs[sub] = 1 for sub in c.me.get('emailSubscriptions') or ['announcement', 'notification', 'tester', 'level_creator', 'developer'] c.subs[sub] = 1 for sub in c.me.getEnabledEmails()
c.showsJobProfileTab = me.isAdmin() or me.get('jobProfile') or location.hash.search('job-profile-') isnt -1 c.showsJobProfileTab = me.isAdmin() or me.get('jobProfile') or location.hash.search('job-profile-') isnt -1
c c
getSubscriptions: -> getSubscriptions: ->
inputs = $('#email-pane input[type="checkbox"]', @$el) inputs = ($(i) for i in $('#email-pane input[type="checkbox"].changed', @$el))
inputs = ($(i) for i in inputs) emailNames = (i.attr('name').replace('email_', '') for i in inputs)
subs = (i.attr('name') for i in inputs when i.prop('checked')) enableds = (i.prop('checked') for i in inputs)
subs = (s.replace('email_', '') for s in subs) _.zipObject emailNames, enableds
subs
toggleEmailSubscriptions: => toggleEmailSubscriptions: =>
subs = @getSubscriptions() subs = @getSubscriptions()
$('#email-pane input[type="checkbox"]', @$el).prop('checked', not Boolean(subs.length)) $('#email-pane input[type="checkbox"]', @$el).prop('checked', not _.any(_.values(subs))).addClass('changed')
@save() @save()
buildPictureTreema: -> buildPictureTreema: ->
@ -102,7 +101,8 @@ module.exports = class SettingsView extends View
@trigger 'change' @trigger 'change'
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL' @$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
save: -> save: (e) ->
$(e.target).addClass('changed') if e
forms.clearFormAlerts(@$el) forms.clearFormAlerts(@$el)
@grabData() @grabData()
res = me.validate() res = me.validate()
@ -143,7 +143,8 @@ module.exports = class SettingsView extends View
grabOtherData: -> grabOtherData: ->
me.set 'name', $('#name', @$el).val() me.set 'name', $('#name', @$el).val()
me.set 'email', $('#email', @$el).val() me.set 'email', $('#email', @$el).val()
me.set 'emailSubscriptions', @getSubscriptions() for emailName, enabled of @getSubscriptions()
me.setEmailSubscription emailName, enabled
me.set 'photoURL', @pictureTreema.get('/photoURL') me.set 'photoURL', @pictureTreema.get('/photoURL')
adminCheckbox = @$el.find('#admin') adminCheckbox = @$el.find('#admin')

View file

@ -21,30 +21,23 @@ module.exports = class ContributeClassView extends View
super() super()
@$el.find('.contributor-signup-anonymous').replaceWith(contributorSignupAnonymousTemplate(me: me)) @$el.find('.contributor-signup-anonymous').replaceWith(contributorSignupAnonymousTemplate(me: me))
@$el.find('.contributor-signup').each -> @$el.find('.contributor-signup').each ->
context = me: me, contributorClassID: $(@).data('contributor-class-id'), contributorClassName: $(@).data('contributor-class-name') context = me: me, contributorClassName: $(@).data('contributor-class-name')
$(@).replaceWith(contributorSignupTemplate(context)) $(@).replaceWith(contributorSignupTemplate(context))
@$el.find('#contributor-list').replaceWith(contributorListTemplate(contributors: @contributors, contributorClassName: @contributorClassName)) @$el.find('#contributor-list').replaceWith(contributorListTemplate(contributors: @contributors, contributorClassName: @contributorClassName))
checkboxes = @$el.find('input[type="checkbox"]').toArray() checkboxes = @$el.find('input[type="checkbox"]').toArray()
_.forEach checkboxes, (el) -> _.forEach checkboxes, (el) ->
el = $(el) el = $(el)
if el.attr('name') in me.get('emailSubscriptions') el.prop('checked', true) if me.isEmailSubscriptionEnabled(el.attr('name')+'News')
el.prop('checked', true)
onCheckboxChanged: (e) -> onCheckboxChanged: (e) ->
el = $(e.target) el = $(e.target)
checked = el.prop('checked') checked = el.prop('checked')
subscription = el.attr('name') subscription = el.attr('name')
subscriptions = me.get('emailSubscriptions') ? []
if checked and not (subscription in subscriptions) me.setEmailSubscription subscription+'News', checked
subscriptions.push(subscription) me.save()
if me.get 'anonymous' @openModalView new SignupModalView() if me.get 'anonymous'
@openModalView new SignupModalView()
if not checked
subscriptions = _.without subscriptions, subscription
el.parent().find('.saved-notification').finish().show('fast').delay(3000).fadeOut(2000) el.parent().find('.saved-notification').finish().show('fast').delay(3000).fadeOut(2000)
me.set('emailSubscriptions', subscriptions)
me.save()
contributors: [] contributors: []

View file

@ -32,13 +32,6 @@ module.exports = class EditorLevelView extends View
'click #level-patch-button': 'startPatchingLevel' 'click #level-patch-button': 'startPatchingLevel'
'click #level-watch-button': 'toggleWatchLevel' 'click #level-watch-button': 'toggleWatchLevel'
subscriptions:
'refresh-level-editor': 'rerenderAllViews'
rerenderAllViews: ->
for view in [@thangsTab, @settingsTab, @scriptsTab, @componentsTab, @systemsTab, @patchesView]
view.render()
constructor: (options, @levelID) -> constructor: (options, @levelID) ->
super options super options
@listenToOnce(@supermodel, 'loaded-all', @onAllLoaded) @listenToOnce(@supermodel, 'loaded-all', @onAllLoaded)

View file

@ -11,8 +11,7 @@ module.exports = class DiplomatSuggestionView extends View
"click #subscribe-button": "subscribeAsDiplomat" "click #subscribe-button": "subscribeAsDiplomat"
subscribeAsDiplomat: -> subscribeAsDiplomat: ->
currentSubscriptions = me.get("emailSubscriptions") me.setEmailSubscription 'diplomatNews', true
me.set("emailSubscriptions", currentSubscriptions.concat ["translator"]) if "translator" not in currentSubscriptions
me.save() me.save()
$("#email_translator").prop("checked", 1) $("#email_translator").prop("checked", 1)
@hide() @hide()

View file

@ -48,15 +48,12 @@ module.exports = class SignupModalView extends View
userObject = forms.formToObject @$el userObject = forms.formToObject @$el
delete userObject.subscribe delete userObject.subscribe
delete userObject["confirm-age"] delete userObject["confirm-age"]
for key, val of me.attributes when key in ["preferredLanguage", "testGroupNumber", "dateCreated", "wizardColor1", "name", "music", "volume", "emailSubscriptions"] for key, val of me.attributes when key in ["preferredLanguage", "testGroupNumber", "dateCreated", "wizardColor1", "name", "music", "volume", "emails"]
userObject[key] ?= val userObject[key] ?= val
subscribe = @$el.find('#signup-subscribe').prop('checked') subscribe = @$el.find('#signup-subscribe').prop('checked')
userObject.emailSubscriptions ?= [] userObject.emails ?= {}
if subscribe userObject.emails.generalNews ?= {}
userObject.emailSubscriptions.push 'announcement' unless 'announcement' in userObject.emailSubscriptions userObject.emails.generalNews.enabled = subscribe
userObject.emailSubscriptions.push 'notification' unless 'notification' in userObject.emailSubscriptions
else
userObject.emailSubscriptions = _.without (userObject.emailSubscriptions ? []), 'announcement', 'notification'
res = tv4.validateMultiple userObject, User.schema res = tv4.validateMultiple userObject, User.schema
return forms.applyErrorsToForm(@$el, res.errors) unless res.valid return forms.applyErrorsToForm(@$el, res.errors) unless res.valid
window.tracker?.trackEvent 'Finished Signup' window.tracker?.trackEvent 'Finished Signup'

View file

@ -65,7 +65,7 @@ collapseSessions = (sessionLists) ->
grabUser = (session, callback) -> grabUser = (session, callback) ->
findParameters = _id: session.creator findParameters = _id: session.creator
selectString = 'email emailSubscriptions name jobProfile' selectString = 'email emailSubscriptions emails name jobProfile'
query = User query = User
.findOne(findParameters) .findOne(findParameters)
.select(selectString) .select(selectString)

View file

@ -0,0 +1,35 @@
// Did not migrate anonymous users because they get their properties setup on signup.
// migrate the most common subscription configs with mass update commands
db.users.update({anonymous:false, emailSubscriptions:['announcement', 'notification'], emails:{$exists:false}}, {$set:{emails:{}}}, {multi:true});
db.users.update({anonymous:false, emailSubscriptions:[], emails:{$exists:false}}, {$set:{emails:{anyNotes:{enabled:false}, generalNews:{enabled:false}}}}, {multi:true});
// migrate the rest one by one
emailMap = {
announcement: 'generalNews',
developer: 'archmageNews',
tester: 'adventurerNews',
level_creator: 'artisanNews',
article_editor: 'scribeNews',
translator: 'diplomatNews',
support: 'ambassadorNews',
notification: 'anyNotes'
};
db.users.find({anonymous:false, emails:{$exists:false}}).forEach(function(u) {
emails = {anyNotes:{enabled:false}, generalNews:{enabled:false}};
var oldEmailSubs = u.emailSubscriptions || ['notification', 'announcement'];
for(var email in oldEmailSubs) {
var oldEmailName = oldEmailSubs[email];
var newEmailName = emailMap[oldEmailName];
if(!newEmailName) {
print('STOP, COULD NOT FIND EMAIL NAME', oldEmailName);
return false;
}
emails[newEmailName] = {enabled:true};
}
u.emails = emails;
db.users.save(u);
});
// Done. No STOP error when this was run.

View file

@ -2,14 +2,10 @@ config = require '../../server_config'
module.exports.MAILCHIMP_LIST_ID = 'e9851239eb' module.exports.MAILCHIMP_LIST_ID = 'e9851239eb'
module.exports.MAILCHIMP_GROUP_ID = '4529' module.exports.MAILCHIMP_GROUP_ID = '4529'
module.exports.MAILCHIMP_GROUP_MAP =
announcement: 'Announcements' # these two need to be parallel
tester: 'Adventurers' module.exports.MAILCHIMP_GROUPS = ['Announcements', 'Adventurers', 'Artisans', 'Archmages', 'Scribes', 'Diplomats', 'Ambassadors']
level_creator: 'Artisans' module.exports.NEWS_GROUPS = ['generalNews', 'adventurerNews', 'artisanNews', 'archmageNews', 'scribeNews', 'diplomatNews', 'ambassadorNews']
developer: 'Archmages'
article_editor: 'Scribes'
translator: 'Diplomats'
support: 'Ambassadors'
nodemailer = require 'nodemailer' nodemailer = require 'nodemailer'
module.exports.transport = nodemailer.createTransport "SMTP", module.exports.transport = nodemailer.createTransport "SMTP",

View file

@ -58,7 +58,7 @@ PatchHandler = class PatchHandler extends Handler
log.error "Error sending patch created: could not find the loaded target on the patch object." unless doc.targetLoaded log.error "Error sending patch created: could not find the loaded target on the patch object." unless doc.targetLoaded
return unless doc.targetLoaded return unless doc.targetLoaded
watchers = doc.targetLoaded.get('watchers') or [] watchers = doc.targetLoaded.get('watchers') or []
watchers = (w for w in watchers when not w.equals(editor.get('_id'))) watchers = (w for w in watchers when not w.equals(req.user.get('_id')))
return unless watchers?.length return unless watchers?.length
User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) => User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) =>
for watcher in watchers for watcher in watchers

View file

@ -131,10 +131,14 @@ module.exports.setup = (app) ->
if not user if not user
return errors.notFound res, "No user found with email '#{req.query.email}'" return errors.notFound res, "No user found with email '#{req.query.email}'"
user.set('emailSubscriptions', []) emails = _.clone(user.get('emails')) or {}
user.save (err) => emailSettings.enabled = false for emailSettings in _.values(emails)
emails.generalNews ?= {}
emails.generalNews.enabled = false
emails.anyNotes ?= {}
emails.anyNotes.enabled = false
user.update {$set: {emails: emails, emailSubscriptions: []}}, {}, =>
return errors.serverError res, 'Database failure.' if err return errors.serverError res, 'Database failure.' if err
res.send "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go! <p><a href='/account/settings'>Account settings</a></p>" res.send "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go! <p><a href='/account/settings'>Account settings</a></p>"
res.end() res.end()

View file

@ -1,5 +1,4 @@
mail = require '../commons/mail' mail = require '../commons/mail'
map = _.invert mail.MAILCHIMP_GROUP_MAP
User = require '../users/User.coffee' User = require '../users/User.coffee'
errors = require '../commons/errors' errors = require '../commons/errors'
#request = require 'request' #request = require 'request'
@ -77,12 +76,13 @@ handleLadderUpdate = (req, res) ->
sendLadderUpdateEmail result, now, daysAgo for result in results sendLadderUpdateEmail result, now, daysAgo for result in results
sendLadderUpdateEmail = (session, now, daysAgo) -> sendLadderUpdateEmail = (session, now, daysAgo) ->
User.findOne({_id: session.creator}).select("name email firstName lastName emailSubscriptions preferredLanguage").lean().exec (err, user) -> User.findOne({_id: session.creator}).select("name email firstName lastName emailSubscriptions emails preferredLanguage").lean().exec (err, user) ->
if err if err
log.error "Couldn't find user for #{session.creator} from session #{session._id}" log.error "Couldn't find user for #{session.creator} from session #{session._id}"
return return
unless user.email and ('notification' in user.emailSubscriptions) and not session.unsubscribed allowNotes = user.isEmailSubscriptionEnabled 'anyNotes'
log.info "Not sending email to #{user.email} #{user.name} because they only want emails about #{user.emailSubscriptions} - session unsubscribed: #{session.unsubscribed}" unless user.email and allowNotes and not session.unsubscribed
log.info "Not sending email to #{user.email} #{user.name} because they only want emails about #{user.emailSubscriptions}, #{user.emails} - session unsubscribed: #{session.unsubscribed}"
return return
unless session.levelName unless session.levelName
log.info "Not sending email to #{user.email} #{user.name} because the session had no levelName in it." log.info "Not sending email to #{user.email} #{user.name} because the session had no levelName in it."
@ -198,13 +198,11 @@ handleMailchimpWebHook = (req, res) ->
return errors.serverError(res) if err return errors.serverError(res) if err
res.end('Success') res.end('Success')
module.exports.handleProfileUpdate = handleProfileUpdate = (user, post) ->
mailchimpSubs = post.data.merges.INTERESTS.split(', ')
handleProfileUpdate = (user, post) -> for [mailchimpEmailGroup, emailGroup] in _.zip(mail.MAILCHIMP_GROUPS, mail.NEWS_GROUPS)
groups = post.data.merges.INTERESTS.split(', ') user.setEmailSubscription emailGroup, mailchimpEmailGroup in mailchimpSubs
groups = (map[g] for g in groups when map[g])
otherSubscriptions = (g for g in user.get('emailSubscriptions') when not mail.MAILCHIMP_GROUP_MAP[g])
groups = groups.concat otherSubscriptions
user.set 'emailSubscriptions', groups
fname = post.data.merges.FNAME fname = post.data.merges.FNAME
user.set('firstName', fname) if fname user.set('firstName', fname) if fname
@ -217,7 +215,9 @@ handleProfileUpdate = (user, post) ->
# badLog("Updating user object to: #{JSON.stringify(user.toObject(), null, '\t')}") # badLog("Updating user object to: #{JSON.stringify(user.toObject(), null, '\t')}")
handleUnsubscribe = (user) -> module.exports.handleUnsubscribe = handleUnsubscribe = (user) ->
user.set 'emailSubscriptions', [] user.set 'emailSubscriptions', []
for emailGroup in mail.NEWS_GROUPS
user.setEmailSubscription emailGroup, false
# badLog("Unsubscribing user object to: #{JSON.stringify(user.toObject(), null, '\t')}") # badLog("Unsubscribing user object to: #{JSON.stringify(user.toObject(), null, '\t')}")

View file

@ -16,6 +16,7 @@ UserSchema = new mongoose.Schema({
UserSchema.pre('init', (next) -> UserSchema.pre('init', (next) ->
return next() unless jsonschema.properties? return next() unless jsonschema.properties?
for prop, sch of jsonschema.properties for prop, sch of jsonschema.properties
continue if prop is 'emails' # defaults may change, so don't carry them over just yet
@set(prop, sch.default) if sch.default? @set(prop, sch.default) if sch.default?
@set('permissions', ['admin']) if not isProduction @set('permissions', ['admin']) if not isProduction
next() next()
@ -23,26 +24,59 @@ UserSchema.pre('init', (next) ->
UserSchema.post('init', -> UserSchema.post('init', ->
@set('anonymous', false) if @get('email') @set('anonymous', false) if @get('email')
@currentSubscriptions = JSON.stringify(@get('emailSubscriptions'))
) )
UserSchema.methods.isAdmin = -> UserSchema.methods.isAdmin = ->
p = @get('permissions') p = @get('permissions')
return p and 'admin' in p return p and 'admin' in p
emailNameMap =
generalNews: 'announcement'
adventurerNews: 'tester'
artisanNews: 'level_creator'
archmageNews: 'developer'
scribeNews: 'article_editor'
diplomatNews: 'translator'
ambassadorNews: 'support'
anyNotes: 'notification'
UserSchema.methods.setEmailSubscription = (newName, enabled) ->
oldSubs = _.clone @get('emailSubscriptions')
if oldSubs and oldName = emailNameMap[newName]
oldSubs = (s for s in oldSubs when s isnt oldName)
oldSubs.push(oldName) if enabled
@set('emailSubscriptions', oldSubs)
newSubs = _.clone(@get('emails') or jsonschema.properties.emails.default)
newSubs[newName] ?= {}
newSubs[newName].enabled = enabled
@set('emails', newSubs)
@newsSubsChanged = true if newName in mail.NEWS_GROUPS
UserSchema.methods.isEmailSubscriptionEnabled = (newName) ->
emails = @get 'emails'
if not emails
oldSubs = @get('emailSubscriptions')
oldName = emailNameMap[newName]
return oldName and oldName in oldSubs if oldSubs
emails ?= {}
_.defaults emails, jsonschema.properties.emails.default
return emails[newName]?.enabled
UserSchema.statics.updateMailChimp = (doc, callback) -> UserSchema.statics.updateMailChimp = (doc, callback) ->
return callback?() unless isProduction return callback?() unless isProduction or GLOBAL.testing
return callback?() if doc.updatedMailChimp return callback?() if doc.updatedMailChimp
return callback?() unless doc.get('email') return callback?() unless doc.get('email')
existingProps = doc.get('mailChimp') existingProps = doc.get('mailChimp')
emailChanged = (not existingProps) or existingProps?.email isnt doc.get('email') emailChanged = (not existingProps) or existingProps?.email isnt doc.get('email')
emailSubs = doc.get('emailSubscriptions') return callback?() unless emailChanged or doc.newsSubsChanged
gm = mail.MAILCHIMP_GROUP_MAP
newGroups = (gm[name] for name in emailSubs when gm[name]?) newGroups = []
for [mailchimpEmailGroup, emailGroup] in _.zip(mail.MAILCHIMP_GROUPS, mail.NEWS_GROUPS)
newGroups.push(mailchimpEmailGroup) if doc.isEmailSubscriptionEnabled(emailGroup)
if (not existingProps) and newGroups.length is 0 if (not existingProps) and newGroups.length is 0
return callback?() # don't add totally unsubscribed people to the list return callback?() # don't add totally unsubscribed people to the list
subsChanged = doc.currentSubscriptions isnt JSON.stringify(emailSubs)
return callback?() unless emailChanged or subsChanged
params = {} params = {}
params.id = mail.MAILCHIMP_LIST_ID params.id = mail.MAILCHIMP_LIST_ID

View file

@ -25,7 +25,7 @@ UserHandler = class UserHandler extends Handler
editableProperties: [ editableProperties: [
'name', 'photoURL', 'password', 'anonymous', 'wizardColor1', 'volume', 'name', 'photoURL', 'password', 'anonymous', 'wizardColor1', 'volume',
'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'emailSubscriptions', 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'emails',
'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage', 'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage',
'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile' 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile'
] ]

View file

@ -0,0 +1,38 @@
require '../common'
mail = require '../../../server/routes/mail'
User = require '../../../server/users/User'
testPost =
data:
email: 'scott@codecombat.com'
id: '12345678'
merges:
INTERESTS: 'Announcements, Adventurers, Archmages, Scribes, Diplomats, Ambassadors, Artisans'
FNAME: 'Scott'
LNAME: 'Erickson'
describe 'handleProfileUpdate', ->
it 'updates emails from the data passed in', (done) ->
u = new User()
mail.handleProfileUpdate(u, testPost)
expect(u.isEmailSubscriptionEnabled('generalNews')).toBeTruthy()
expect(u.isEmailSubscriptionEnabled('adventurerNews')).toBeTruthy()
expect(u.isEmailSubscriptionEnabled('archmageNews')).toBeTruthy()
expect(u.isEmailSubscriptionEnabled('scribeNews')).toBeTruthy()
expect(u.isEmailSubscriptionEnabled('diplomatNews')).toBeTruthy()
expect(u.isEmailSubscriptionEnabled('ambassadorNews')).toBeTruthy()
expect(u.isEmailSubscriptionEnabled('artisanNews')).toBeTruthy()
done()
describe 'handleUnsubscribe', ->
it 'turns off all news and notifications', (done) ->
u = new User({generalNews: {enabled:true}, archmageNews: {enabled:true}, anyNotes: {enabled:true}})
mail.handleUnsubscribe(u)
expect(u.isEmailSubscriptionEnabled('generalNews')).toBeFalsy()
expect(u.isEmailSubscriptionEnabled('adventurerNews')).toBeFalsy()
expect(u.isEmailSubscriptionEnabled('archmageNews')).toBeFalsy()
expect(u.isEmailSubscriptionEnabled('scribeNews')).toBeFalsy()
expect(u.isEmailSubscriptionEnabled('diplomatNews')).toBeFalsy()
expect(u.isEmailSubscriptionEnabled('ambassadorNews')).toBeFalsy()
expect(u.isEmailSubscriptionEnabled('artisanNews')).toBeFalsy()
done()

View file

@ -1,8 +1,47 @@
require '../common' require '../common'
request = require 'request' request = require 'request'
User = require '../../../server/users/User'
urlUser = '/db/user' urlUser = '/db/user'
describe 'Server user object', ->
it 'uses the schema defaults to fill in email preferences', (done) ->
user = new User()
expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy()
expect(user.isEmailSubscriptionEnabled('anyNotes')).toBeTruthy()
expect(user.isEmailSubscriptionEnabled('recruitNotes')).toBeTruthy()
expect(user.isEmailSubscriptionEnabled('archmageNews')).toBeFalsy()
done()
it 'uses old subs if they\'re around', (done) ->
user = new User()
user.set 'emailSubscriptions', ['tester']
expect(user.isEmailSubscriptionEnabled('adventurerNews')).toBeTruthy()
expect(user.isEmailSubscriptionEnabled('generalNews')).toBeFalsy()
done()
it 'maintains the old subs list if it\'s around', (done) ->
user = new User()
user.set 'emailSubscriptions', ['tester']
user.setEmailSubscription('artisanNews', true)
expect(JSON.stringify(user.get('emailSubscriptions'))).toBe(JSON.stringify(['tester','level_creator']))
done()
describe 'User.updateMailChimp', ->
makeMC = (callback) ->
GLOBAL.mc =
lists:
subscribe: callback
it 'uses emails to determine what to send to MailChimp', (done) ->
makeMC (params) ->
expect(JSON.stringify(params.merge_vars.groupings[0].groups)).toBe(JSON.stringify(['Announcements']))
done()
user = new User({emailSubscriptions:['announcement'], email:'tester@gmail.com'})
User.updateMailChimp(user)
describe 'POST /db/user', -> describe 'POST /db/user', ->
it 'preparing test : clears the db first', (done) -> it 'preparing test : clears the db first', (done) ->