Merge branch 'master' into production

This commit is contained in:
phoenixeliot 2016-07-18 10:42:54 -07:00
commit 96fa50afe4
37 changed files with 446 additions and 72 deletions

View file

@ -4,13 +4,18 @@ language: node_js
node_js: node_js:
- 5.1.1 - 5.1.1
env:
- CXX=g++-4.8
addons: addons:
apt: apt:
sources: sources:
- mongodb-upstart - mongodb-upstart
- ubuntu-toolchain-r-test
packages: packages:
- mongodb-org-server - mongodb-org-server
- g++-4.8
cache: cache:
directories: directories:

View file

@ -34,9 +34,10 @@ module.exports = class CocoRouter extends Backbone.Router
'admin/design-elements': go('admin/DesignElementsView') 'admin/design-elements': go('admin/DesignElementsView')
'admin/files': go('admin/FilesView') 'admin/files': go('admin/FilesView')
'admin/analytics': go('admin/AnalyticsView') 'admin/analytics': go('admin/AnalyticsView')
'admin/school-counts': go('admin/SchoolCountsView')
'admin/analytics/subscriptions': go('admin/AnalyticsSubscriptionsView') 'admin/analytics/subscriptions': go('admin/AnalyticsSubscriptionsView')
'admin/level-sessions': go('admin/LevelSessionsView') 'admin/level-sessions': go('admin/LevelSessionsView')
'admin/school-counts': go('admin/SchoolCountsView')
'admin/school-licenses': go('admin/SchoolLicensesView')
'admin/users': go('admin/UsersView') 'admin/users': go('admin/UsersView')
'admin/base': go('admin/BaseView') 'admin/base': go('admin/BaseView')
'admin/demo-requests': go('admin/DemoRequestsView') 'admin/demo-requests': go('admin/DemoRequestsView')

View file

@ -961,7 +961,7 @@
manage_subscription: "Click here to manage your subscription." manage_subscription: "Click here to manage your subscription."
new_password: "New Password" new_password: "New Password"
new_password_verify: "Verify" new_password_verify: "Verify"
type_in_email: "Type in your email to confirm account deletion." type_in_email: "Type in your email or username to confirm account deletion." # {change}
type_in_email_progress: "Type in your email to confirm deleting your progress." type_in_email_progress: "Type in your email to confirm deleting your progress."
type_in_password: "Also, type in your password." type_in_password: "Also, type in your password."
email_subscriptions: "Email Subscriptions" email_subscriptions: "Email Subscriptions"
@ -1333,7 +1333,7 @@
update_account_title: "Your account needs attention!" update_account_title: "Your account needs attention!"
update_account_blurb: "Before you can access your classes, choose how you want to use this account." update_account_blurb: "Before you can access your classes, choose how you want to use this account."
update_account_current_type: "Current Account Type:" update_account_current_type: "Current Account Type:"
update_account_account_email: "Account Email:" update_account_account_email: "Account Email/Username:" # {change}
update_account_am_teacher: "I am a teacher" update_account_am_teacher: "I am a teacher"
update_account_keep_access: "Keep access to classes I've created" update_account_keep_access: "Keep access to classes I've created"
update_account_teachers_can: "Teacher accounts can:" update_account_teachers_can: "Teacher accounts can:"

View file

@ -438,6 +438,8 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
tome_available_spells: "Доступные заклинания" tome_available_spells: "Доступные заклинания"
tome_your_skills: "Ваши навыки" tome_your_skills: "Ваши навыки"
tome_current_method: "Текущий метод" tome_current_method: "Текущий метод"
hints: "Советы"
hints_title: "Совет {{number}}"
code_saved: "Код сохранен" code_saved: "Код сохранен"
skip_tutorial: "Пропуск (Esc)" skip_tutorial: "Пропуск (Esc)"
keyboard_shortcuts: "Горячие клавиши" keyboard_shortcuts: "Горячие клавиши"

View file

@ -271,11 +271,11 @@ module.exports = class User extends CocoModel
window.location.reload() window.location.reload()
@fetch(options) @fetch(options)
signupWithPassword: (email, password, options={}) -> signupWithPassword: (name, email, password, options={}) ->
options.url = _.result(@, 'url') + '/signup-with-password' options.url = _.result(@, 'url') + '/signup-with-password'
options.type = 'POST' options.type = 'POST'
options.data ?= {} options.data ?= {}
_.extend(options.data, {email, password}) _.extend(options.data, {name, email, password})
jqxhr = @fetch(options) jqxhr = @fetch(options)
jqxhr.then -> jqxhr.then ->
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'CodeCombat' window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'CodeCombat'

View file

@ -0,0 +1,21 @@
#admin-school-licenses-view
table
td, th
padding: 0px
.range-container
position: relative
width: 100%
.range-background
position: absolute
height: 100%
left: 0px
top: 0px
background-color: green
opacity: 0.25
.range-dates
position: absolute
height: 100%
left: 0px
top: 0px

View file

@ -35,6 +35,9 @@
.help-block .help-block
margin: 0 margin: 0
.optional-help-block
font-style: italic
.form-container .form-container
width: 800px width: 800px

View file

@ -10,7 +10,7 @@ else
.panel-body .panel-body
.form .form
- var name = me.get('name') || ''; - var name = me.get('name') || '';
- var email = me.get('email'); - var email = me.get('email') || '';
- var admin = me.get('permissions', true).indexOf('admin') != -1; - var admin = me.get('permissions', true).indexOf('admin') != -1;
- var godmode = me.get('permissions', true).indexOf('godmode') != -1; - var godmode = me.get('permissions', true).indexOf('godmode') != -1;
.form-group .form-group
@ -71,11 +71,11 @@ else
.panel-body .panel-body
.form#delete-account-form .form#delete-account-form
.form-group .form-group
label.control-label(for="email1", data-i18n="account_settings.type_in_email") label.control-label(for="delete-account-email-or-username", data-i18n="account_settings.type_in_email")
input#email1.form-control(name="email", type="email") input#delete-account-email-or-username.form-control(name="emailOrUsername")
.form-group .form-group
label.control-label(for="password1", data-i18n="account_settings.type_in_password") label.control-label(for="delete-account-password", data-i18n="account_settings.type_in_password")
input#password1.form-control(name="password", type="password") input#delete-account-password.form-control(name="password", type="password")
button#delete-account-btn.btn.form-control.btn-primary(data-i18n="account_settings.delete_this_account") button#delete-account-btn.btn.form-control.btn-primary(data-i18n="account_settings.delete_this_account")
.col-md-6 .col-md-6

View file

@ -10,10 +10,10 @@ block content
.col-sm-1 .col-sm-1
button.btn.btn-primary.btn-large#enter-espionage-mode 007 button.btn.btn-primary.btn-large#enter-espionage-mode 007
label.control-label.col-sm-5(for="espionage-name-or-email") label.control-label.col-sm-5(for="espionage-name-or-email")
em you are currently #{me.get('name')} at #{me.get('email')} em you are currently #{me.get('name') || '(no username)'} at #{me.get('email') || '(no email)'}
if view.amActually if view.amActually
br br
em but you are actually #{view.amActually.get('name')} at #{view.amActually.get('email')} em but you are actually #{view.amActually.get('name') || '(no username)'} at #{view.amActually.get('email') || '(no email)'}
br br
button#stop-spying-btn.btn.btn-xs Stop Spying button#stop-spying-btn.btn.btn-xs Stop Spying
form#user-search-form.form-group form#user-search-form.form-group
@ -47,6 +47,8 @@ block content
input.classroom-progress-class-code(type=text value="<class code>") input.classroom-progress-class-code(type=text value="<class code>")
li li
a(href="/admin/analytics") Dashboard a(href="/admin/analytics") Dashboard
li
a(href="/admin/school-licenses") School Active Licenses
li li
a(href="/admin/school-counts") School Counts a(href="/admin/school-counts") School Counts
li li

View file

@ -0,0 +1,39 @@
extends /templates/base-flat
//- DO NOT TRANSLATE
block content
if !me.isAdmin()
div You must be logged in as an admin to view this page.
else if !view.schools
h3 Loading...
else
h3 School Active Licenses
.small Max: total licenses
.small Used: licenses redeemed
.small Activity: level sessions created in last 30 days
table.table.table-condensed
thead
th School
th Max
th Used&nbsp;
th Activity
tr
td(style="height:26px;").range-container
each rangeKey in view.rangeKeys
span.range-background(style="left:#{rangeKey.startScale}%;width:#{rangeKey.width}%;background-color:#{rangeKey.color}")
span.range-dates(style="left:#{rangeKey.startScale}%;width:#{rangeKey.width}%;") #{rangeKey.name}
td(colspan=2)
each school in view.schools
each prepaid in school.prepaids
tr
td.range-container
span.range-background(style="left:#{prepaid.startScale}%;width:#{prepaid.rangeScale}%;")
span.range-dates(style="left:#{prepaid.startScale}%;")
span.spr #{prepaid.startDate.substring(0, 10)}
strong.spr #{school.name}
span #{prepaid.endDate.substring(0, 10)}
td #{prepaid.max}&nbsp;
td #{prepaid.used}
td= school.activity

View file

@ -36,6 +36,9 @@ form#basic-info-form.modal-body.basic-info
span(data-i18n="share_progress_modal.form_label") span(data-i18n="share_progress_modal.form_label")
.col-xs-5.col-xs-offset-3 .col-xs-5.col-xs-offset-3
input.form-control.input-lg#email-input(name="email" type="email") input.form-control.input-lg#email-input(name="email" type="email")
if view.signupState.get('path') === 'student'
.help-block.optional-help-block.pull-right
span(data-i18n="signup.optional")
.col-xs-4.email-check .col-xs-4.email-check
- var checkEmailState = view.state.get('checkEmailState'); - var checkEmailState = view.state.get('checkEmailState');
if checkEmailState === 'checking' if checkEmailState === 'checking'
@ -53,6 +56,7 @@ form#basic-info-form.modal-body.basic-info
span.text-forest.glyphicon.glyphicon-ok-circle span.text-forest.glyphicon.glyphicon-ok-circle
=" " =" "
span(data-i18n="signup.email_good") span(data-i18n="signup.email_good")
.form-group .form-group
.row .row
.col-xs-7.col-xs-offset-3 .col-xs-7.col-xs-offset-3
@ -74,6 +78,7 @@ form#basic-info-form.modal-body.basic-info
span.text-forest.glyphicon.glyphicon-ok-circle span.text-forest.glyphicon.glyphicon-ok-circle
=" " =" "
span(data-i18n="signup.name_available") span(data-i18n="signup.name_available")
.form-group .form-group
.row .row
.col-xs-7.col-xs-offset-3 .col-xs-7.col-xs-offset-3
@ -81,6 +86,7 @@ form#basic-info-form.modal-body.basic-info
span(data-i18n="general.password") span(data-i18n="general.password")
.col-xs-5.col-xs-offset-3 .col-xs-5.col-xs-offset-3
input.form-control.input-lg#password-input(name="password" type="password") input.form-control.input-lg#password-input(name="password" type="password")
.form-group.checkbox.subscribe .form-group.checkbox.subscribe
.row .row
.col-xs-7.col-xs-offset-3 .col-xs-7.col-xs-offset-3

View file

@ -27,7 +27,7 @@ block content
if view.accountType if view.accountType
div #{view.accountType} div #{view.accountType}
div div
span.spr #{me.get('email')} span.spr #{me.get('email') || me.get('name')}
span.not_you span.not_you
span.spr(data-i18n="courses.not_you") span.spr(data-i18n="courses.not_you")
a.logout-btn(data-i18n="login.log_out", href="#") a.logout-btn(data-i18n="login.log_out", href="#")

View file

@ -4,7 +4,10 @@ block modal-header-content
.text-center .text-center
h1.modal-title(data-i18n="courses.remove_student1") h1.modal-title(data-i18n="courses.remove_student1")
span.glyphicon.glyphicon-warning-sign.text-danger span.glyphicon.glyphicon-warning-sign.text-danger
p= view.user.get('name', true) + ' - ' + view.user.get('email') p
span= view.user.get('name', true)
if view.user.get('email')
span= " — " + view.user.get('email')
h2(data-i18n="courses.are_you_sure") h2(data-i18n="courses.are_you_sure")
block modal-body-content block modal-body-content

View file

@ -87,16 +87,16 @@ module.exports = class AccountSettingsView extends CocoView
validateCredentialsForDestruction: ($form, onSuccess) -> validateCredentialsForDestruction: ($form, onSuccess) ->
forms.clearFormAlerts($form) forms.clearFormAlerts($form)
enteredEmail = $form.find('input[type="email"]').val() enteredEmailOrUsername = $form.find('input[name="emailOrUsername"]').val()
enteredPassword = $form.find('input[type="password"]').val() enteredPassword = $form.find('input[name="password"]').val()
if enteredEmail and enteredEmail is me.get('email') if enteredEmailOrUsername and enteredEmailOrUsername in [me.get('email'), me.get('name')]
isPasswordCorrect = false isPasswordCorrect = false
toBeDelayed = true toBeDelayed = true
$.ajax $.ajax
url: '/auth/login' url: '/auth/login'
type: 'POST' type: 'POST'
data: data:
username: enteredEmail username: enteredEmailOrUsername
password: enteredPassword password: enteredPassword
parse: true parse: true
error: (error) -> error: (error) ->
@ -225,9 +225,16 @@ module.exports = class AccountSettingsView extends CocoView
return unless res return unless res
res.error => res.error =>
errors = JSON.parse(res.responseText) if res.responseJSON?.property
forms.applyErrorsToForm(@$el, errors) errors = res.responseJSON
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')}) forms.applyErrorsToForm(@$el, errors)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
else
noty
text: res.responseText
type: 'error'
layout: 'topCenter'
timeout: 5000
@trigger 'save-user-error' @trigger 'save-user-error'
res.success (model, response, options) => res.success (model, response, options) =>
@trigger 'save-user-success' @trigger 'save-user-success'

View file

@ -0,0 +1,72 @@
RootView = require 'views/core/RootView'
CocoCollection = require 'collections/CocoCollection'
Prepaid = require 'models/Prepaid'
TrialRequests = require 'collections/TrialRequests'
# TODO: year ranges hard-coded
module.exports = class SchoolLicensesView extends RootView
id: 'admin-school-licenses-view'
template: require 'templates/admin/school-licenses'
initialize: ->
return super() unless me.isAdmin()
@startDateRange = new Date()
@endDateRange = new Date()
@endDateRange.setUTCFullYear(@endDateRange.getUTCFullYear() + 2)
@supermodel.addRequestResource({
url: '/db/prepaid/-/active-schools'
method: 'GET'
success: ({prepaidActivityMap, schoolPrepaidsMap}) =>
@updateSchools(prepaidActivityMap, schoolPrepaidsMap)
}, 0).load()
super()
updateSchools: (prepaidActivityMap, schoolPrepaidsMap) ->
timeStart = @startDateRange.getTime()
time2017 = new Date('2017').getTime()
time2018 = new Date('2018').getTime()
timeEnd = @endDateRange.getTime()
rangeMilliseconds = timeEnd - timeStart
@rangeKeys = [
{name :'Today', color: 'blue', startScale: 0, width: Math.round((time2017 - timeStart) / rangeMilliseconds * 100)}
{name: '2017', color: 'red', startScale: Math.round((time2017 - timeStart) / rangeMilliseconds * 100), width: Math.round((time2018 - time2017) / rangeMilliseconds * 100)}
{name: '2018', color: 'yellow', startScale: Math.round((time2018 - timeStart) / rangeMilliseconds * 100), width: Math.round((timeEnd - time2018) / rangeMilliseconds * 100)}
]
@schools = []
for school, prepaids of schoolPrepaidsMap
activity = 0
schoolMax = 0
schoolUsed = 0
collapsedPrepaids = []
for prepaid in prepaids
activity += prepaidActivityMap[prepaid._id] ? 0
startDate = prepaid.startDate
endDate = prepaid.endDate
max = parseInt(prepaid.maxRedeemers)
used = parseInt(prepaid.redeemers?.length ? 0)
schoolMax += max
schoolUsed += used
foundIdenticalDates = false
for collapsedPrepaid in collapsedPrepaids
if collapsedPrepaid.startDate.substring(0, 10) is startDate.substring(0, 10) and collapsedPrepaid.endDate.substring(0, 10) is endDate.substring(0, 10)
collapsedPrepaid.max += parseInt(prepaid.maxRedeemers)
collapsedPrepaid.used += parseInt(prepaid.redeemers?.length ? 0)
foundIdenticalDates = true
break
unless foundIdenticalDates
collapsedPrepaids.push({startDate, endDate, max, used})
for collapsedPrepaid in collapsedPrepaids
collapsedPrepaid.startScale = Math.round((new Date(collapsedPrepaid.startDate).getTime() - @startDateRange.getTime()) / rangeMilliseconds * 100)
collapsedPrepaid.startScale = 0 if collapsedPrepaid.startScale < 0
collapsedPrepaid.rangeScale = Math.round((new Date(collapsedPrepaid.endDate).getTime() - new Date(collapsedPrepaid.startDate).getTime()) / rangeMilliseconds * 100)
collapsedPrepaid.rangeScale = 100 - collapsedPrepaid.startScale if collapsedPrepaid.rangeScale + collapsedPrepaid.startScale > 100
@schools.push {name: school, activity, max: schoolMax, used: schoolUsed, prepaids: collapsedPrepaids, startDate: collapsedPrepaids[0].startDate, endDate: collapsedPrepaids[0].endDate}
@schools.sort (a, b) ->
b.activity - a.activity or new Date(a.endDate).getTime() - new Date(b.endDate).getTime() or b.max - a.max or b.used - a.used or b.prepaids.length - a.prepaids.length or b.name.localeCompare(a.name)
# console.log @schools
@render()

View file

@ -64,7 +64,8 @@ module.exports = class BasicInfoView extends CocoView
checkEmail: -> checkEmail: ->
email = @$('[name="email"]').val() email = @$('[name="email"]').val()
if email is @state.get('checkEmailValue')
if @signupState.get('path') isnt 'student' and (not _.isEmpty(email) and email is @state.get('checkEmailValue'))
return @state.get('checkEmailPromise') return @state.get('checkEmailPromise')
if not (email and forms.validateEmail(email)) if not (email and forms.validateEmail(email))
@ -155,7 +156,7 @@ module.exports = class BasicInfoView extends CocoView
email: User.schema.properties.email email: User.schema.properties.email
name: User.schema.properties.name name: User.schema.properties.name
password: User.schema.properties.password password: User.schema.properties.password
required: ['email', 'name', 'password'].concat (if @signupState.get('path') is 'student' then ['firstName', 'lastName'] else []) required: ['name', 'password'].concat (if @signupState.get('path') is 'student' then ['firstName', 'lastName'] else ['email'])
onClickBackButton: -> @trigger 'nav-back' onClickBackButton: -> @trigger 'nav-back'
@ -176,20 +177,20 @@ module.exports = class BasicInfoView extends CocoView
@checkEmail() @checkEmail()
.then @checkName() .then @checkName()
.then => .then =>
if not (@state.get('checkEmailState') is 'available' and @state.get('checkNameState') is 'available') if not (@state.get('checkEmailState') in ['available', 'standby'] and @state.get('checkNameState') is 'available')
throw AbortError throw AbortError
# update User # update User
emails = _.assign({}, me.get('emails')) emails = _.assign({}, me.get('emails'))
emails.generalNews ?= {} emails.generalNews ?= {}
emails.generalNews.enabled = @$('#subscribe-input').is(':checked') emails.generalNews.enabled = @$('#subscribe-input').is(':checked') and not _.isEmpty(@state.get('checkEmailValue'))
me.set('emails', emails) me.set('emails', emails)
unless _.isNaN(@signupState.get('birthday').getTime()) unless _.isNaN(@signupState.get('birthday').getTime())
me.set('birthday', @signupState.get('birthday').toISOString()) me.set('birthday', @signupState.get('birthday').toISOString())
me.set(_.omit(@signupState.get('ssoAttrs') or {}, 'email', 'facebookID', 'gplusID')) me.set(_.omit(@signupState.get('ssoAttrs') or {}, 'email', 'facebookID', 'gplusID'))
me.set('name', @$('input[name="name"]').val())
jqxhr = me.save() jqxhr = me.save()
if not jqxhr if not jqxhr
console.error(me.validationError) console.error(me.validationError)
@ -203,13 +204,15 @@ module.exports = class BasicInfoView extends CocoView
switch @signupState.get('ssoUsed') switch @signupState.get('ssoUsed')
when 'gplus' when 'gplus'
{ email, gplusID } = @signupState.get('ssoAttrs') { email, gplusID } = @signupState.get('ssoAttrs')
jqxhr = me.signupWithGPlus(email, gplusID) { name } = forms.formToObject(@$el)
jqxhr = me.signupWithGPlus(name, email, gplusID)
when 'facebook' when 'facebook'
{ email, facebookID } = @signupState.get('ssoAttrs') { email, facebookID } = @signupState.get('ssoAttrs')
jqxhr = me.signupWithFacebook(email, facebookID) { name } = forms.formToObject(@$el)
jqxhr = me.signupWithFacebook(name, email, facebookID)
else else
{ email, password } = forms.formToObject(@$el) { name, email, password } = forms.formToObject(@$el)
jqxhr = me.signupWithPassword(email, password) jqxhr = me.signupWithPassword(name, email, password)
return new Promise(jqxhr.then) return new Promise(jqxhr.then)

View file

@ -373,7 +373,7 @@ module.exports = class TeacherClassView extends RootView
coursePlaytimesString += "0," coursePlaytimesString += "0,"
else else
coursePlaytimesString += "#{moment.duration(coursePlaytime.playtime, 'seconds').humanize()}," coursePlaytimesString += "#{moment.duration(coursePlaytime.playtime, 'seconds').humanize()},"
csvContent += "#{student.get('name')},#{student.get('email')},#{playtimeString},#{coursePlaytimesString}\"#{conceptsString}\"\n" csvContent += "#{student.get('name')},#{student.get('email') or ''},#{playtimeString},#{coursePlaytimesString}\"#{conceptsString}\"\n"
csvContent = csvContent.substring(0, csvContent.length - 1) csvContent = csvContent.substring(0, csvContent.length - 1)
encodedUri = encodeURI(csvContent) encodedUri = encodeURI(csvContent)
window.open(encodedUri) window.open(encodedUri)

View file

@ -81,6 +81,7 @@ grabUser = (session, callback) ->
totalEmailsSent = 0 totalEmailsSent = 0
emailUserInitialRecruiting = (user, callback) -> emailUserInitialRecruiting = (user, callback) ->
return callback null, false if not user.email
#return callback null, false if user.emails?.anyNotes?.enabled is false # TODO: later, uncomment to obey also 'anyNotes' when that's untangled #return callback null, false if user.emails?.anyNotes?.enabled is false # TODO: later, uncomment to obey also 'anyNotes' when that's untangled
return callback null, false if user.emails?.recruitNotes?.enabled is false return callback null, false if user.emails?.recruitNotes?.enabled is false
return callback null, false if user.email in alreadyEmailed return callback null, false if user.email in alreadyEmailed
@ -129,6 +130,7 @@ grabEmail = (winner, callback) ->
callback null, winner callback null, winner
emailUserTournamentResults = (winner, callback) -> emailUserTournamentResults = (winner, callback) ->
return callback null, false if not winner.email
return callback null, false if DEBUGGING and (winner.team is 'humans' or totalEmailsSent > 1) return callback null, false if DEBUGGING and (winner.team is 'humans' or totalEmailsSent > 1)
++totalEmailsSent ++totalEmailsSent
name = winner.name name = winner.name

View file

@ -52,6 +52,36 @@ var courses =
duration: NumberInt(5), duration: NumberInt(5),
free: false, free: false,
screenshot: "/images/pages/courses/105_info.png" screenshot: "/images/pages/courses/105_info.png"
},
{
name: "CS: Game Development 1",
slug: "game-dev-1",
campaignID: ObjectId("5789236960deed1f00ec2ab8"),
description: "Learn to create your owns games which you can share with your friends.",
duration: NumberInt(5),
free: false,
//screenshot: "/images/pages/courses/105_info.png",
adminOnly: true
},
{
name: "CS: Web Development 1",
slug: "web-dev-1",
campaignID: ObjectId("578913f2c8871ac2326fa3e4"),
description: "Learn the basics of web development in this introductory HTML & CSS course.",
duration: NumberInt(5),
free: false,
//screenshot: "/images/pages/courses/105_info.png",
adminOnly: true
},
{
name: "CS: Web Development 2",
slug: "web-dev-2",
campaignID: ObjectId("57891570c8871ac2326fa3f8"),
description: "Learn more advanced web development, including scripting to make interactive webpages.",
duration: NumberInt(5),
free: false,
//screenshot: "/images/pages/courses/105_info.png",
adminOnly: true
} }
]; ];
@ -62,7 +92,7 @@ for (var i = 0; i < courses.length; i++) {
if (cursor.hasNext()) { if (cursor.hasNext()) {
var doc = cursor.next(); var doc = cursor.next();
for (var levelID in doc.levels) { for (var levelID in doc.levels) {
for (var j = 0; j < doc.levels[levelID].concepts.length; j++) { for (var j = 0; j < (doc.levels[levelID].concepts || []).length; j++) {
concepts[doc.levels[levelID].concepts[j]] = true; concepts[doc.levels[levelID].concepts[j]] = true;
} }
} }

View file

@ -82,6 +82,8 @@ module.exports = class Handler
sendBadInputError: (res, message) -> errors.badInput(res, message) sendBadInputError: (res, message) -> errors.badInput(res, message)
sendPaymentRequiredError: (res, message) -> errors.paymentRequired(res, message) sendPaymentRequiredError: (res, message) -> errors.paymentRequired(res, message)
sendDatabaseError: (res, err) -> sendDatabaseError: (res, err) ->
if err instanceof errors.NetworkError
return res.status(err.code).send(err.toJSON())
return @sendError(res, err.code, err.response) if err?.response and err?.code return @sendError(res, err.code, err.response) if err?.response and err?.code
log.error "Database error, #{err}" log.error "Database error, #{err}"
errors.serverError(res, 'Database error, ' + err) errors.serverError(res, 'Database error, ' + err)
@ -467,6 +469,7 @@ module.exports = class Handler
@notifyWatcherOfChange editor, watcher, changedDocument, editPath @notifyWatcherOfChange editor, watcher, changedDocument, editPath
notifyWatcherOfChange: (editor, watcher, changedDocument, editPath) -> notifyWatcherOfChange: (editor, watcher, changedDocument, editPath) ->
return if not watcher.get('email')
context = context =
email_id: sendwithus.templates.change_made_notify_watcher email_id: sendwithus.templates.change_made_notify_watcher
recipient: recipient:

View file

@ -100,7 +100,7 @@ errorResponseSchema = {
} }
errorProps = _.keys(errorResponseSchema.properties) errorProps = _.keys(errorResponseSchema.properties)
class NetworkError class NetworkError extends Error
code: 0 code: 0
constructor: (@message, options) -> constructor: (@message, options) ->

View file

@ -98,7 +98,8 @@ PatchHandler = class PatchHandler extends Handler
@sendPatchCreatedEmail req.user, watcher, doc, doc.targetLoaded, docLink @sendPatchCreatedEmail req.user, watcher, doc, doc.targetLoaded, docLink
sendPatchCreatedEmail: (patchCreator, watcher, patch, target, docLink) -> sendPatchCreatedEmail: (patchCreator, watcher, patch, target, docLink) ->
# return if watcher._id is patchCreator._id return if not watcher.get('email')
# return if watcher._id is patchCreator._id
context = context =
email_id: sendwithus.templates.patch_created email_id: sendwithus.templates.patch_created
recipient: recipient:

View file

@ -270,6 +270,9 @@ class SubscriptionHandler extends Handler
if (not req.user) or req.user.isAnonymous() or user.isAnonymous() if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
return done({res: 'You must be signed in to subscribe.', code: 403}) return done({res: 'You must be signed in to subscribe.', code: 403})
if not req.user.get('email')
return done({res: 'Your account needs an email address to subscribe.', code: 403})
token = req.body.stripe.token token = req.body.stripe.token
prepaidCode = req.body.stripe.prepaidCode prepaidCode = req.body.stripe.prepaidCode
customerID = user.get('stripe')?.customerID customerID = user.get('stripe')?.customerID

View file

@ -110,9 +110,9 @@ UserHandler = class UserHandler extends Handler
# Name setting # Name setting
(req, user, callback) -> (req, user, callback) ->
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 user.get 'anonymous' # anonymous users can have any name 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') 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) ->

View file

@ -115,7 +115,7 @@ module.exports =
_type: "lead" _type: "lead"
lead_id: leadID lead_id: leadID
assigned_to: userID assigned_to: userID
text: "Call #{teacherEmail}" text: "Call license inquiry #{teacherEmail}"
is_complete: false is_complete: false
options = options =
uri: "https://#{apiKey}:X@app.close.io/api/v1/task/" uri: "https://#{apiKey}:X@app.close.io/api/v1/task/"

View file

@ -1,9 +1,11 @@
wrap = require 'co-express' wrap = require 'co-express'
errors = require '../commons/errors' errors = require '../commons/errors'
database = require '../commons/database' database = require '../commons/database'
Prepaid = require '../models/Prepaid'
User = require '../models/User'
mongoose = require 'mongoose' mongoose = require 'mongoose'
LevelSession = require '../models/LevelSession'
Prepaid = require '../models/Prepaid'
TrialRequest = require '../models/TrialRequest'
User = require '../models/User'
cutoffDate = new Date(2015,11,11) cutoffDate = new Date(2015,11,11)
cutoffID = mongoose.Types.ObjectId(Math.floor(cutoffDate/1000).toString(16)+'0000000000000000') cutoffID = mongoose.Types.ObjectId(Math.floor(cutoffDate/1000).toString(16)+'0000000000000000')
@ -11,14 +13,14 @@ cutoffID = mongoose.Types.ObjectId(Math.floor(cutoffDate/1000).toString(16)+'000
module.exports = module.exports =
logError: (user, msg) -> logError: (user, msg) ->
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'" console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
post: wrap (req, res) -> post: wrap (req, res) ->
validTypes = ['course'] validTypes = ['course']
unless req.body.type in validTypes unless req.body.type in validTypes
throw new errors.UnprocessableEntity("type must be on of: #{validTypes}.") throw new errors.UnprocessableEntity("type must be on of: #{validTypes}.")
# TODO: deprecate or refactor other prepaid types # TODO: deprecate or refactor other prepaid types
if req.body.creator if req.body.creator
user = yield User.search(req.body.creator) user = yield User.search(req.body.creator)
if not user if not user
@ -32,16 +34,16 @@ module.exports =
database.validateDoc(prepaid) database.validateDoc(prepaid)
yield prepaid.save() yield prepaid.save()
res.status(201).send(prepaid.toObject()) res.status(201).send(prepaid.toObject())
redeem: wrap (req, res) -> redeem: wrap (req, res) ->
if not req.user?.isTeacher() if not req.user?.isTeacher()
throw new errors.Forbidden('Must be a teacher to use licenses') throw new errors.Forbidden('Must be a teacher to use licenses')
prepaid = yield database.getDocFromHandle(req, Prepaid) prepaid = yield database.getDocFromHandle(req, Prepaid)
if not prepaid if not prepaid
throw new errors.NotFound('Prepaid not found.') throw new errors.NotFound('Prepaid not found.')
if prepaid._id.getTimestamp().getTime() < cutoffDate.getTime() if prepaid._id.getTimestamp().getTime() < cutoffDate.getTime()
throw new errors.Forbidden('Cannot redeem from prepaids older than November 11, 2015') throw new errors.Forbidden('Cannot redeem from prepaids older than November 11, 2015')
unless prepaid.get('creator').equals(req.user._id) unless prepaid.get('creator').equals(req.user._id)
@ -61,7 +63,7 @@ module.exports =
return res.status(200).send(prepaid.toObject({req: req})) return res.status(200).send(prepaid.toObject({req: req}))
if user.isTeacher() if user.isTeacher()
throw new errors.Forbidden('Teachers may not be enrolled') throw new errors.Forbidden('Teachers may not be enrolled')
query = query =
_id: prepaid._id _id: prepaid._id
'redeemers.userID': { $ne: user._id } 'redeemers.userID': { $ne: user._id }
@ -71,7 +73,7 @@ module.exports =
if result.nModified is 0 if result.nModified is 0
@logError(req.user, "POST prepaid redeemer lost race on maxRedeemers") @logError(req.user, "POST prepaid redeemer lost race on maxRedeemers")
throw new errors.Forbidden('This prepaid is exhausted') throw new errors.Forbidden('This prepaid is exhausted')
update = { update = {
$set: { $set: {
coursePrepaid: { coursePrepaid: {
@ -84,7 +86,7 @@ module.exports =
if not user.get('role') if not user.get('role')
update.$set.role = 'student' update.$set.role = 'student'
yield user.update(update) yield user.update(update)
# return prepaid with new redeemer added locally # return prepaid with new redeemer added locally
redeemers = _.clone(prepaid.get('redeemers') or []) redeemers = _.clone(prepaid.get('redeemers') or [])
redeemers.push({ date: new Date(), userID: user._id }) redeemers.push({ date: new Date(), userID: user._id })
@ -94,12 +96,12 @@ module.exports =
fetchByCreator: wrap (req, res, next) -> fetchByCreator: wrap (req, res, next) ->
creator = req.query.creator creator = req.query.creator
return next() if not creator return next() if not creator
unless req.user.isAdmin() or creator is req.user.id unless req.user.isAdmin() or creator is req.user.id
throw new errors.Forbidden('Must be logged in as given creator') throw new errors.Forbidden('Must be logged in as given creator')
unless database.isID(creator) unless database.isID(creator)
throw new errors.UnprocessableEntity('Invalid creator') throw new errors.UnprocessableEntity('Invalid creator')
q = { q = {
_id: { $gt: cutoffID } _id: { $gt: cutoffID }
creator: mongoose.Types.ObjectId(creator) creator: mongoose.Types.ObjectId(creator)
@ -108,3 +110,43 @@ module.exports =
prepaids = yield Prepaid.find(q) prepaids = yield Prepaid.find(q)
res.send((prepaid.toObject({req: req}) for prepaid in prepaids)) res.send((prepaid.toObject({req: req}) for prepaid in prepaids))
fetchActiveSchools: wrap (req, res) ->
unless req.user.isAdmin() or creator is req.user.id
throw new errors.Forbidden('Must be logged in as given creator')
prepaids = yield Prepaid.find({type: 'course'}, {creator: 1, properties: 1, startDate: 1, endDate: 1, maxRedeemers: 1, redeemers: 1}).lean()
userPrepaidsMap = {}
today = new Date()
userIDs = []
redeemerIDs = []
redeemerPrepaidMap = {}
for prepaid in prepaids
continue if new Date(prepaid.endDate ? prepaid.properties?.endDate ? '2000') < today
continue if new Date(prepaid.endDate) < new Date(prepaid.startDate)
userPrepaidsMap[prepaid.creator.valueOf()] ?= []
userPrepaidsMap[prepaid.creator.valueOf()].push(prepaid)
userIDs.push prepaid.creator
for redeemer in prepaid.redeemers ? []
redeemerIDs.push redeemer.userID + ""
redeemerPrepaidMap[redeemer.userID + ""] = prepaid._id.valueOf()
# Find recently created level sessions for redeemers
lastMonth = new Date()
lastMonth.setUTCDate(lastMonth.getUTCDate() - 30)
levelSessions = yield LevelSession.find({$and: [{created: {$gte: lastMonth}}, {creator: {$in: redeemerIDs}}]}, {creator: 1}).lean()
prepaidActivityMap = {}
for levelSession in levelSessions
prepaidActivityMap[redeemerPrepaidMap[levelSession.creator.valueOf()]] ?= 0
prepaidActivityMap[redeemerPrepaidMap[levelSession.creator.valueOf()]]++
trialRequests = yield TrialRequest.find({$and: [{type: 'course'}, {applicant: {$in: userIDs}}]}, {applicant: 1, properties: 1}).lean()
schoolPrepaidsMap = {}
for trialRequest in trialRequests
school = trialRequest.properties?.organization ? trialRequest.properties?.school
continue unless school
if userPrepaidsMap[trialRequest.applicant.valueOf()]?.length > 0
schoolPrepaidsMap[school] ?= []
for prepaid in userPrepaidsMap[trialRequest.applicant.valueOf()]
schoolPrepaidsMap[school].push prepaid
res.send({prepaidActivityMap, schoolPrepaidsMap})

View file

@ -89,6 +89,8 @@ module.exports =
timestamp = (new Date).getTime() timestamp = (new Date).getTime()
if not user if not user
throw new errors.NotFound('User not found') throw new errors.NotFound('User not found')
if not user.get('email')
throw new errors.UnprocessableEntity('User must have an email address to receive a verification email')
context = context =
email_id: sendwithus.templates.verify_email email_id: sendwithus.templates.verify_email
recipient: recipient:
@ -127,14 +129,18 @@ module.exports =
unless req.user.isAnonymous() unless req.user.isAnonymous()
throw new errors.Forbidden('You are already signed in.') throw new errors.Forbidden('You are already signed in.')
{ password, email } = req.body { name, email, password } = req.body
unless _.all([password, email]) unless password
throw new errors.UnprocessableEntity('Requires password and email') throw new errors.UnprocessableEntity('Requires password')
unless name or email
throw new errors.UnprocessableEntity('Requires username or email')
if yield User.findByEmail(email) if not _.isEmpty(email) and yield User.findByEmail(email)
throw new errors.Conflict('Email already taken') throw new errors.Conflict('Email already taken')
if not _.isEmpty(name) and yield User.findByName(name)
throw new errors.Conflict('Name already taken')
req.user.set({ password, email, anonymous: false }) req.user.set({ name, email, password, anonymous: false })
yield module.exports.finishSignup(req, res) yield module.exports.finishSignup(req, res)
signupWithFacebook: wrap (req, res) -> signupWithFacebook: wrap (req, res) ->

View file

@ -105,6 +105,7 @@ module.exports =
if watchers.length if 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
continue if not watcher.get('email')
context = context =
email_id: sendwithus.templates.change_made_notify_watcher email_id: sendwithus.templates.change_made_notify_watcher
recipient: recipient:

View file

@ -122,6 +122,10 @@ UserSchema.statics.findByEmail = (email, done=_.noop) ->
emailLower = email.toLowerCase() emailLower = email.toLowerCase()
User.findOne({emailLower: emailLower}).exec(done) User.findOne({emailLower: emailLower}).exec(done)
UserSchema.statics.findByName = (name, done=_.noop) ->
nameLower = name.toLowerCase()
User.findOne({nameLower: nameLower}).exec(done)
emailNameMap = emailNameMap =
generalNews: 'announcement' generalNews: 'announcement'
adventurerNews: 'tester' adventurerNews: 'tester'
@ -267,6 +271,7 @@ UserSchema.statics.unconflictName = unconflictName = (name, done) ->
unconflictName name + suffix, done unconflictName name + suffix, done
UserSchema.methods.sendWelcomeEmail = -> UserSchema.methods.sendWelcomeEmail = ->
return if not @get('email')
{ welcome_email_student, welcome_email_user } = sendwithus.templates { welcome_email_student, welcome_email_user } = sendwithus.templates
timestamp = (new Date).getTime() timestamp = (new Date).getTime()
data = data =
@ -345,14 +350,25 @@ UserSchema.methods.saveActiveUser = (event, done=null) ->
UserSchema.pre('save', (next) -> UserSchema.pre('save', (next) ->
if _.isNaN(@get('purchased')?.gems) if _.isNaN(@get('purchased')?.gems)
return next(new errors.InternalServerError('Attempting to save NaN to user')) return next(new errors.InternalServerError('Attempting to save NaN to user'))
Classroom = require './Classroom' Classroom = require './Classroom'
if @isTeacher() and not @wasTeacher if @isTeacher() and not @wasTeacher
Classroom.update({members: @_id}, {$pull: {members: @_id}}, {multi: true}).exec (err, res) -> Classroom.update({members: @_id}, {$pull: {members: @_id}}, {multi: true}).exec (err, res) ->
if email = @get('email') if email = @get('email')
@set('emailLower', email.toLowerCase()) @set('emailLower', email.toLowerCase())
else
@set('email', undefined)
@set('emailLower', undefined)
if name = @get('name') if name = @get('name')
@set('nameLower', name.toLowerCase()) @set('nameLower', name.toLowerCase())
else
@set('name', undefined)
@set('nameLower', undefined)
unless email or name or @get('anonymous') or @get('deleted')
return next(new errors.UnprocessableEntity('User needs a username or email address'))
pwd = @get('password') pwd = @get('password')
if @get('password') if @get('password')
@set('passwordHash', User.hashPassword(pwd)) @set('passwordHash', User.hashPassword(pwd))

View file

@ -26,7 +26,7 @@ module.exports = createNewTask = (req, res) ->
validatePermissions = (req, sessionID, callback) -> validatePermissions = (req, sessionID, callback) ->
return callback 'You are unauthorized to submit that game to the simulator.' unless req.user?.get('email') return callback 'You are unauthorized to submit that game to the simulator.' if (not req.user) or req.user.isAnonymous()
return callback null if req.user?.isAdmin() return callback null if req.user?.isAdmin()
findParameters = _id: sessionID findParameters = _id: sessionID

View file

@ -26,7 +26,7 @@ module.exports = dispatchTaskToConsumer = (req, res) ->
checkSimulationPermissions = (req, cb) -> checkSimulationPermissions = (req, cb) ->
if req.user?.get('email') if req.user and not req.user.isAnonymous()
cb null cb null
else else
cb 'You need to be logged in to simulate games' cb 'You need to be logged in to simulate games'

View file

@ -104,6 +104,7 @@ module.exports.setup = (app) ->
app.post('/db/user/:handle/signup-with-password', mw.users.signupWithPassword) app.post('/db/user/:handle/signup-with-password', mw.users.signupWithPassword)
app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator) app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator)
app.get('/db/prepaid/-/active-schools', mw.auth.checkHasPermission(['admin']), mw.prepaids.fetchActiveSchools)
app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post) app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post)
app.post('/db/prepaid/:handle/redeemers', mw.prepaids.redeem) app.post('/db/prepaid/:handle/redeemers', mw.prepaids.redeem)

View file

@ -471,6 +471,7 @@ taskReminderAlreadySentThisWeekFilter = (task, cb) ->
sendUserRemarkTaskEmail = (task, cb) -> sendUserRemarkTaskEmail = (task, cb) ->
mailTaskName = @mailTaskName mailTaskName = @mailTaskName
User.findOne("_id":task.contact).select("email").lean().exec (err, contact) -> User.findOne("_id":task.contact).select("email").lean().exec (err, contact) ->
return if not contact.email
if err? then return cb err if err? then return cb err
User.findOne("_id":task.user).select("jobProfile.name").lean().exec (err, user) -> User.findOne("_id":task.user).select("jobProfile.name").lean().exec (err, user) ->
if err? then return cb err if err? then return cb err
@ -567,6 +568,7 @@ handleLadderUpdate = (req, res) ->
sendLadderUpdateEmail = (session, now, daysAgo) -> sendLadderUpdateEmail = (session, now, daysAgo) ->
User.findOne({_id: session.creator}).select('name email firstName lastName emailSubscriptions emails preferredLanguage').exec (err, user) -> User.findOne({_id: session.creator}).select('name email firstName lastName emailSubscriptions emails preferredLanguage').exec (err, user) ->
return if not user.get('email')
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
@ -686,13 +688,14 @@ handleNextSteps = (req, res) ->
log.info "Found #{results.length} next-steps users to email updates about for #{daysAgo} day(s) ago." if DEBUGGING log.info "Found #{results.length} next-steps users to email updates about for #{daysAgo} day(s) ago." if DEBUGGING
sendNextStepsEmail result, now, daysAgo for result in results sendNextStepsEmail result, now, daysAgo for result in results
sendNextStepsEmail = (user, now, daysAgo) -> module.exports.sendNextStepsEmail = sendNextStepsEmail = (user, now, daysAgo) ->
return log.info "Not sending next steps email to user with no email address" if not user.get('email')
unless user.isEmailSubscriptionEnabled('generalNews') and user.isEmailSubscriptionEnabled('anyNotes') unless user.isEmailSubscriptionEnabled('generalNews') and user.isEmailSubscriptionEnabled('anyNotes')
log.info "Not sending email to #{user.get('email')} #{user.get('name')} because they only want emails about #{JSON.stringify(user.get('emails'))}" if DEBUGGING log.info "Not sending email to #{user.get('email')} #{user.get('name')} because they only want emails about #{JSON.stringify(user.get('emails'))}" if DEBUGGING
return return
LevelSession.find({creator: user.get('_id') + ''}).select('levelName levelID changed state.complete playtime').lean().exec (err, sessions) -> LevelSession.find({creator: user.get('_id') + ''}).select('levelName levelID changed state.complete playtime').lean().exec (err, sessions) ->
return log.error "Couldn't find sessions for #{user.get('email')}: #{err}" if err return log.error "Couldn't find sessions for #{user.get('email')} #{user.get('name')}: #{err}" if err
complete = (s for s in sessions when s.state?.complete) complete = (s for s in sessions when s.state?.complete)
incomplete = (s for s in sessions when not s.state?.complete) incomplete = (s for s in sessions when not s.state?.complete)
return if complete.length < 2 return if complete.length < 2
@ -704,7 +707,7 @@ sendNextStepsEmail = (user, now, daysAgo) ->
nextLevel = null nextLevel = null
err = null err = null
do (err, nextLevel) -> do (err, nextLevel) ->
return log.error "Couldn't find next level for #{user.get('email')}: #{err}" if err return log.error "Couldn't find next level for #{user.get('email')} #{user.get('name')}: #{err}" if err
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name') name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
name = 'Hero' if not name or name in ['Anoner', 'Anonymous'] name = 'Hero' if not name or name in ['Anoner', 'Anonymous']
#secretLevel = switch user.get('testGroupNumber') % 8 #secretLevel = switch user.get('testGroupNumber') % 8

View file

@ -1,7 +1,10 @@
require '../common' require '../common'
utils = require '../utils'
mail = require '../../../server/routes/mail' mail = require '../../../server/routes/mail'
sendwithus = require '../../../server/sendwithus'
User = require '../../../server/models/User' User = require '../../../server/models/User'
request = require '../request' request = require '../request'
LevelSession = require '../../../server/models/LevelSession'
testPost = testPost =
data: data:
@ -37,3 +40,30 @@ describe 'handleUnsubscribe', ->
expect(u.isEmailSubscriptionEnabled('ambassadorNews')).toBeFalsy() expect(u.isEmailSubscriptionEnabled('ambassadorNews')).toBeFalsy()
expect(u.isEmailSubscriptionEnabled('artisanNews')).toBeFalsy() expect(u.isEmailSubscriptionEnabled('artisanNews')).toBeFalsy()
done() done()
# This can be re-enabled on demand to test it, but for some async reason this
# crashes jasmine soon afterward.
describe 'sendNextStepsEmail', ->
xit 'Sends the email', utils.wrap (done) ->
user = yield utils.initUser({generalNews: {enabled: true}, anyNotes: {enabled: true}})
expect(user.id).toBeDefined()
yield new LevelSession({
creator: user.id
permissions: simplePermissions
level: original: 'dungeon-arena'
state: complete: true
}).save()
yield new LevelSession({
creator: user.id
permissions: simplePermissions
level: original: 'dungeon-arena-2'
state: complete: true
}).save()
spyOn(sendwithus.api, 'send').and.callFake (options, cb) ->
expect(options.recipient.address).toBe(user.get('email'))
cb()
done()
mail.sendNextStepsEmail(user, new Date, 5)
.pend('Breaks other tests — must be run alone')

View file

@ -1,7 +1,8 @@
async = require 'async' async = require 'async'
config = require '../../../server_config' config = require '../../../server_config'
require '../common' require '../common'
utils = require '../../../app/core/utils' # Must come after require /common appUtils = require '../../../app/core/utils' # Must come after require /common
utils = require '../utils'
mongoose = require 'mongoose' mongoose = require 'mongoose'
TRAVIS = process.env.COCO_TRAVIS_TEST TRAVIS = process.env.COCO_TRAVIS_TEST
nockUtils = require '../nock-utils' nockUtils = require '../nock-utils'
@ -113,6 +114,13 @@ describe '/db/user, editing stripe property', ->
request.put {uri: userURL, json: body, headers: headers}, (err, res, body) -> request.put {uri: userURL, json: body, headers: headers}, (err, res, body) ->
expect(res.statusCode).toBe 403 expect(res.statusCode).toBe 403
done() done()
it 'denies username-only users trying to subscribe', utils.wrap (done) ->
user = yield utils.initUser({ email: undefined, })
yield utils.loginUser(user)
[res, body] = yield request.putAsync(getURL("/db/user/#{user.id}"), { headers, json: { stripe: { planID: 'basic', token: '12345' } } })
expect(res.statusCode).toBe(403)
done()
#- shared data between tests #- shared data between tests
joeData = null joeData = null
@ -327,7 +335,7 @@ describe 'Subscriptions', ->
return done() unless subscription? return done() unless subscription?
expect(subscription.plan.amount).toEqual(1) expect(subscription.plan.amount).toEqual(1)
expect(subscription.customer).toEqual(sponsorCustomerID) expect(subscription.customer).toEqual(sponsorCustomerID)
expect(subscription.quantity).toEqual(utils.getSponsoredSubsAmount(subPrice, numSponsored, sponsorStripe.subscriptionID?)) expect(subscription.quantity).toEqual(appUtils.getSponsoredSubsAmount(subPrice, numSponsored, sponsorStripe.subscriptionID?))
# Verify sponsor payment # Verify sponsor payment
# May be greater than expected amount due to multiple subscribes and unsubscribes # May be greater than expected amount due to multiple subscribes and unsubscribes
@ -336,7 +344,7 @@ describe 'Subscriptions', ->
recipient: mongoose.Types.ObjectId(sponsorUserID) recipient: mongoose.Types.ObjectId(sponsorUserID)
"stripe.customerID": sponsorCustomerID "stripe.customerID": sponsorCustomerID
"stripe.subscriptionID": sponsorStripe.sponsorSubscriptionID "stripe.subscriptionID": sponsorStripe.sponsorSubscriptionID
expectedAmount = utils.getSponsoredSubsAmount(subPrice, numSponsored, sponsorStripe.subscriptionID?) expectedAmount = appUtils.getSponsoredSubsAmount(subPrice, numSponsored, sponsorStripe.subscriptionID?)
Payment.find paymentQuery, (err, payments) -> Payment.find paymentQuery, (err, payments) ->
expect(err).toBeNull() expect(err).toBeNull()
expect(payments).not.toBeNull() expect(payments).not.toBeNull()
@ -1192,7 +1200,7 @@ describe 'Subscriptions', ->
for invoice in invoices.data for invoice in invoices.data
line = invoice.lines.data[0] line = invoice.lines.data[0]
if line.type is 'invoiceitem' and line.proration if line.type is 'invoiceitem' and line.proration
totalAmount = utils.getSponsoredSubsAmount(subPrice, 2, false) totalAmount = appUtils.getSponsoredSubsAmount(subPrice, 2, false)
expect(invoice.total).toBeLessThan(totalAmount) expect(invoice.total).toBeLessThan(totalAmount)
expect(invoice.total).toEqual(totalAmount - subPrice) expect(invoice.total).toEqual(totalAmount - subPrice)
Payment.findOne "stripe.invoiceID": invoice.id, (err, payment) -> Payment.findOne "stripe.invoiceID": invoice.id, (err, payment) ->

View file

@ -234,6 +234,14 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl
expect(body.role).toBe('advisor') expect(body.role).toBe('advisor')
done() done()
it 'returns 422 if both email and name would be unset for a registered user', utils.wrap (done) ->
user = yield utils.initUser()
yield utils.loginUser(user)
[res, body] = yield request.putAsync { uri: getURL('/db/user/'+user.id), json: { email: '', name: '' }}
expect(body.code).toBe(422)
expect(body.message).toEqual('User needs a username or email address')
done()
describe 'PUT /db/user/-/become-student', -> describe 'PUT /db/user/-/become-student', ->
beforeEach utils.wrap (done) -> beforeEach utils.wrap (done) ->
@url = getURL('/db/user/-/become-student') @url = getURL('/db/user/-/become-student')
@ -697,6 +705,50 @@ describe 'POST /db/user/:handle/signup-with-password', ->
expect(sendwithus.api.send).toHaveBeenCalled() expect(sendwithus.api.send).toHaveBeenCalled()
done() done()
it 'signs up the user with just a name and password', utils.wrap (done) ->
user = yield utils.becomeAnonymous()
url = getURL("/db/user/#{user.id}/signup-with-password")
name = 'someusername'
json = { name, password: '12345' }
[res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(200)
updatedUser = yield User.findById(user.id)
expect(updatedUser.get('name')).toBe(name)
expect(updatedUser.get('nameLower')).toBe(name.toLowerCase())
expect(updatedUser.get('slug')).toBe(name.toLowerCase())
expect(updatedUser.get('passwordHash')).toBeDefined()
expect(updatedUser.get('email')).toBeUndefined()
expect(updatedUser.get('emailLower')).toBeUndefined()
done()
it 'signs up the user with a username, email, and password', utils.wrap (done) ->
user = yield utils.becomeAnonymous()
url = getURL("/db/user/#{user.id}/signup-with-password")
name = 'someusername'
email = 'user@example.com'
json = { name, email, password: '12345' }
[res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(200)
updatedUser = yield User.findById(user.id)
expect(updatedUser.get('name')).toBe(name)
expect(updatedUser.get('nameLower')).toBe(name.toLowerCase())
expect(updatedUser.get('slug')).toBe(name.toLowerCase())
expect(updatedUser.get('email')).toBe(email)
expect(updatedUser.get('emailLower')).toBe(email.toLowerCase())
expect(updatedUser.get('passwordHash')).toBeDefined()
done()
it 'returns 422 if neither username or email were provided', utils.wrap (done) ->
user = yield utils.becomeAnonymous()
url = getURL("/db/user/#{user.id}/signup-with-password")
json = { password: '12345' }
[res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(422)
updatedUser = yield User.findById(user.id)
expect(updatedUser.get('anonymous')).toBe(true)
expect(updatedUser.get('passwordHash')).toBeUndefined()
done()
it 'returns 409 if there is already a user with the given email', utils.wrap (done) -> it 'returns 409 if there is already a user with the given email', utils.wrap (done) ->
email = 'some@email.com' email = 'some@email.com'
initialUser = yield utils.initUser({email}) initialUser = yield utils.initUser({email})
@ -707,6 +759,17 @@ describe 'POST /db/user/:handle/signup-with-password', ->
[res, body] = yield request.postAsync({url, json}) [res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(409) expect(res.statusCode).toBe(409)
done() done()
it 'returns 409 if there is already a user with the given username', utils.wrap (done) ->
name = 'someusername'
initialUser = yield utils.initUser({name})
expect(initialUser.get('nameLower')).toBeDefined()
user = yield utils.becomeAnonymous()
url = getURL("/db/user/#{user.id}/signup-with-password")
json = { name, password: '12345' }
[res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(409)
done()
it 'disassociates the user from their trial request if the trial request email and signup email do not match', utils.wrap (done) -> it 'disassociates the user from their trial request if the trial request email and signup email do not match', utils.wrap (done) ->
user = yield utils.becomeAnonymous() user = yield utils.becomeAnonymous()
@ -739,7 +802,7 @@ describe 'POST /db/user/:handle/signup-with-facebook', ->
facebookID = '12345' facebookID = '12345'
facebookEmail = 'some@email.com' facebookEmail = 'some@email.com'
validFacebookResponse = new Promise((resolve) -> resolve({ validFacebookResponse = new Promise((resolve) -> resolve({
id: facebookID, id: facebookID,
email: facebookEmail, email: facebookEmail,
first_name: 'Some', first_name: 'Some',
@ -753,12 +816,12 @@ describe 'POST /db/user/:handle/signup-with-facebook', ->
verified: true verified: true
})) }))
invalidFacebookResponse = new Promise((resolve) -> resolve({ invalidFacebookResponse = new Promise((resolve) -> resolve({
error: { error: {
message: 'Invalid OAuth access token.', message: 'Invalid OAuth access token.',
type: 'OAuthException', type: 'OAuthException',
code: 190, code: 190,
fbtrace_id: 'EC4dEdeKHBH' fbtrace_id: 'EC4dEdeKHBH'
} }
})) }))

View file

@ -36,7 +36,8 @@ module.exports = mw =
options = {} options = {}
options = _.extend({ options = _.extend({
permissions: [] permissions: []
email: 'user'+_.uniqueId()+'@gmail.com' name: 'Name Nameyname '+_.uniqueId()
email: 'user'+_.uniqueId()+'@example.com'
password: 'password' password: 'password'
anonymous: false anonymous: false
}, options) }, options)
@ -49,7 +50,7 @@ module.exports = mw =
done = options done = options
options = {} options = {}
form = { form = {
username: user.get('email') username: user.get('email') or user.get('name')
password: 'password' password: 'password'
} }
(options.request or request).post mw.getURL('/auth/login'), { form: form }, (err, res) -> (options.request or request).post mw.getURL('/auth/login'), { form: form }, (err, res) ->
@ -89,7 +90,7 @@ module.exports = mw =
args = Array.from(arguments) args = Array.from(arguments)
[done, [data, sources]] = [args.pop(), args] [done, [data, sources]] = [args.pop(), args]
data = _.extend({}, { data = _.extend({}, {
name: _.uniqueId('Level ') name: _.uniqueId('Level ')
permissions: [{target: mw.lastLogin.id, access: 'owner'}] permissions: [{target: mw.lastLogin.id, access: 'owner'}]
}, data) }, data)