parent
ff69bb8c89
commit
9c7345fed0
6 changed files with 180 additions and 14 deletions
app
server
test/server/functional
|
@ -68,9 +68,9 @@ mixin student-dialog(course)
|
||||||
.container-fluid
|
.container-fluid
|
||||||
.row
|
.row
|
||||||
.col-md-8
|
.col-md-8
|
||||||
input.code-input(type='text', data-i18n="[placeholder]courses.enter_code1", placeholder="Enter unlock code")
|
input.code-input(type='text', data-course-id="#{course.id}", data-i18n="[placeholder]courses.enter_code1", placeholder="Enter unlock code")
|
||||||
.col-md-4
|
.col-md-4
|
||||||
button.btn.btn-success.btn-enroll(data-i18n="courses.enroll")
|
button.btn.btn-success.btn-enroll(data-course-id="#{course.id}", data-i18n="courses.enroll")
|
||||||
|
|
||||||
mixin teacher-dialog(course)
|
mixin teacher-dialog(course)
|
||||||
.modal.continue-dialog(id="continueModal#{course.id}")
|
.modal.continue-dialog(id="continueModal#{course.id}")
|
||||||
|
@ -104,7 +104,10 @@ mixin teacher-dialog(course)
|
||||||
div.or(data-i18n="courses.or")
|
div.or(data-i18n="courses.or")
|
||||||
.row.button-row.center
|
.row.button-row.center
|
||||||
.col-md-12
|
.col-md-12
|
||||||
button.btn.btn-success.btn-lg.btn-buy(data-course-id="#{course.id}", data-i18n="courses.buy_course1")
|
if course.get('pricePerSeat') === 0
|
||||||
|
button.btn.btn-success.btn-lg.btn-buy(data-course-id="#{course.id}") Start new class
|
||||||
|
else
|
||||||
|
button.btn.btn-success.btn-lg.btn-buy(data-course-id="#{course.id}", data-i18n="courses.buy_course1")
|
||||||
|
|
||||||
mixin course-block(course)
|
mixin course-block(course)
|
||||||
if studentMode
|
if studentMode
|
||||||
|
|
|
@ -283,17 +283,17 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
when "progressAsc"
|
when "progressAsc"
|
||||||
@sortedMembers.sort (a, b) =>
|
@sortedMembers.sort (a, b) =>
|
||||||
for levelID, level of @campaign.get('levels')
|
for levelID, level of @campaign.get('levels')
|
||||||
if @userLevelStateMap[a][levelID] isnt 'complete' and @userLevelStateMap[b][levelID] is 'complete'
|
if @userLevelStateMap[a]?[levelID] isnt 'complete' and @userLevelStateMap[b]?[levelID] is 'complete'
|
||||||
return -1
|
return -1
|
||||||
else if @userLevelStateMap[a][levelID] is 'complete' and @userLevelStateMap[b][levelID] isnt 'complete'
|
else if @userLevelStateMap[a]?[levelID] is 'complete' and @userLevelStateMap[b]?[levelID] isnt 'complete'
|
||||||
return 1
|
return 1
|
||||||
0
|
0
|
||||||
when "progressDesc"
|
when "progressDesc"
|
||||||
@sortedMembers.sort (a, b) =>
|
@sortedMembers.sort (a, b) =>
|
||||||
for levelID, level of @campaign.get('levels')
|
for levelID, level of @campaign.get('levels')
|
||||||
if @userLevelStateMap[a][levelID] isnt 'complete' and @userLevelStateMap[b][levelID] is 'complete'
|
if @userLevelStateMap[a]?[levelID] isnt 'complete' and @userLevelStateMap[b]?[levelID] is 'complete'
|
||||||
return 1
|
return 1
|
||||||
else if @userLevelStateMap[a][levelID] is 'complete' and @userLevelStateMap[b][levelID] isnt 'complete'
|
else if @userLevelStateMap[a]?[levelID] is 'complete' and @userLevelStateMap[b]?[levelID] isnt 'complete'
|
||||||
return -1
|
return -1
|
||||||
0
|
0
|
||||||
else
|
else
|
||||||
|
|
|
@ -70,7 +70,27 @@ module.exports = class CoursesView extends RootView
|
||||||
Backbone.Mediator.publish 'router:navigate', navigationEvent
|
Backbone.Mediator.publish 'router:navigate', navigationEvent
|
||||||
|
|
||||||
onClickEnroll: (e) ->
|
onClickEnroll: (e) ->
|
||||||
alert('TODO: redeem course prepaid and navigate to correct course instance')
|
$('.continue-dialog').modal('hide')
|
||||||
|
courseID = $(e.target).data('course-id')
|
||||||
|
prepaidCode = $(".code-input[data-course-id=#{courseID}]").val()
|
||||||
|
data = prepaidCode: prepaidCode
|
||||||
|
jqxhr = $.post('/db/course_instance/-/redeem_prepaid', data)
|
||||||
|
jqxhr.done (data, textStatus, jqXHR) =>
|
||||||
|
application.tracker?.trackEvent 'Redeemed course prepaid code', {courseID: courseID, prepaidCode: prepaidCode}
|
||||||
|
# TODO: handle fetch errors
|
||||||
|
me.fetch(cache: false).always =>
|
||||||
|
route = "/courses/#{courseID}"
|
||||||
|
viewArgs = [{}, courseID]
|
||||||
|
Backbone.Mediator.publish 'router:navigate',
|
||||||
|
route: route
|
||||||
|
viewClass: 'views/courses/CourseDetailsView'
|
||||||
|
viewArgs: viewArgs
|
||||||
|
jqxhr.fail (xhr, textStatus, errorThrown) =>
|
||||||
|
console.error 'Got an error redeeming a course prepaid code:', textStatus, errorThrown
|
||||||
|
application.tracker?.trackEvent 'Failed to redeem course prepaid code', status: textStatus
|
||||||
|
@state = 'unknown_error'
|
||||||
|
@stateMessage = "#{xhr.status}: #{xhr.responseText}"
|
||||||
|
@render?()
|
||||||
|
|
||||||
onClickEnter: (e) ->
|
onClickEnter: (e) ->
|
||||||
$('.continue-dialog').modal('hide')
|
$('.continue-dialog').modal('hide')
|
||||||
|
|
|
@ -33,6 +33,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||||
return @getLevelSessionsAPI(req, res, args[0]) if args[1] is 'level_sessions'
|
return @getLevelSessionsAPI(req, res, args[0]) if args[1] is 'level_sessions'
|
||||||
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
|
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
|
||||||
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
|
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
|
||||||
|
return @redeemPrepaidCodeAPI(req, res) if args[1] is 'redeem_prepaid'
|
||||||
super arguments...
|
super arguments...
|
||||||
|
|
||||||
createAPI: (req, res) ->
|
createAPI: (req, res) ->
|
||||||
|
@ -126,5 +127,39 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||||
sendwithus.api.send context, _.noop
|
sendwithus.api.send context, _.noop
|
||||||
return @sendSuccess(res, {})
|
return @sendSuccess(res, {})
|
||||||
|
|
||||||
|
redeemPrepaidCodeAPI: (req, res) ->
|
||||||
|
return @sendUnauthorizedError(res) if not req.user? or req.user?.isAnonymous()
|
||||||
|
return @sendBadInputError(res) unless req.body?.prepaidCode
|
||||||
|
|
||||||
|
prepaidCode = req.body?.prepaidCode
|
||||||
|
Prepaid.find code: prepaidCode, (err, prepaids) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendNotFoundError(res) if prepaids.length < 1
|
||||||
|
return @sendDatabaseError(res, "Multiple prepaid codes found for #{prepaidCode}") if prepaids.length > 1
|
||||||
|
prepaid = prepaids[0]
|
||||||
|
|
||||||
|
CourseInstance.find prepaidID: prepaid.get('_id'), (err, courseInstances) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendForbiddenError(res) if prepaid.get('redeemers')?.length >= prepaid.get('maxRedeemers')
|
||||||
|
|
||||||
|
# Add to prepaid redeemers
|
||||||
|
query =
|
||||||
|
_id: prepaid.get('_id')
|
||||||
|
'redeemers.userID': { $ne: req.user.get('_id') }
|
||||||
|
$where: "this.redeemers.length < #{prepaid.get('maxRedeemers')}"
|
||||||
|
update = { $push: { redeemers : { date: new Date(), userID: req.user.get('_id') } }}
|
||||||
|
Prepaid.update query, update, (err, nMatched) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
if nMatched is 0
|
||||||
|
@logError(req.user, "Course instance update prepaid lost race on maxRedeemers")
|
||||||
|
return @sendForbiddenError(res)
|
||||||
|
|
||||||
|
# Add to each course instance
|
||||||
|
makeAddMemberToCourseInstanceFn = (courseInstance) =>
|
||||||
|
(done) => courseInstance.update({$addToSet: { members: req.user.get('_id')}}, done)
|
||||||
|
tasks = (makeAddMemberToCourseInstanceFn(courseInstance) for courseInstance in courseInstances)
|
||||||
|
async.parallel tasks, (err, results) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
@sendSuccess(res)
|
||||||
|
|
||||||
module.exports = new CourseInstanceHandler()
|
module.exports = new CourseInstanceHandler()
|
||||||
|
|
|
@ -19,7 +19,7 @@ PrepaidHandler = class PrepaidHandler extends Handler
|
||||||
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
|
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
|
||||||
|
|
||||||
hasAccess: (req) ->
|
hasAccess: (req) ->
|
||||||
req.user?.isAdmin()
|
req.method is 'GET' || req.user?.isAdmin()
|
||||||
|
|
||||||
getByRelationship: (req, res, args...) ->
|
getByRelationship: (req, res, args...) ->
|
||||||
relationship = args[1]
|
relationship = args[1]
|
||||||
|
|
|
@ -6,7 +6,8 @@ stripe = require('stripe')(config.stripe.secretKey)
|
||||||
# TODO: add permissiosn tests
|
# TODO: add permissiosn tests
|
||||||
|
|
||||||
describe 'CourseInstance', ->
|
describe 'CourseInstance', ->
|
||||||
courseInstanceURL = getURL('/db/course_instance/-/create')
|
courseInstanceCreateURL = getURL('/db/course_instance/-/create')
|
||||||
|
courseInstanceRedeemURL = getURL('/db/course_instance/-/redeem_prepaid')
|
||||||
userURL = getURL('/db/user')
|
userURL = getURL('/db/user')
|
||||||
|
|
||||||
createCourseInstances = (user, courseID, seats, token, done) ->
|
createCourseInstances = (user, courseID, seats, token, done) ->
|
||||||
|
@ -17,7 +18,7 @@ describe 'CourseInstance', ->
|
||||||
seats: seats
|
seats: seats
|
||||||
stripe:
|
stripe:
|
||||||
token: token
|
token: token
|
||||||
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
|
request.post {uri: courseInstanceCreateURL, json: requestBody }, (err, res) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(res.statusCode).toBe(201)
|
expect(res.statusCode).toBe(201)
|
||||||
CourseInstance.find {name: name}, (err, courseInstances) ->
|
CourseInstance.find {name: name}, (err, courseInstances) ->
|
||||||
|
@ -81,7 +82,7 @@ describe 'CourseInstance', ->
|
||||||
requestBody =
|
requestBody =
|
||||||
courseID: course.get('_id')
|
courseID: course.get('_id')
|
||||||
name: createName('course instance ')
|
name: createName('course instance ')
|
||||||
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
|
request.post {uri: courseInstanceCreateURL, json: requestBody }, (err, res) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(res.statusCode).toBe(422)
|
expect(res.statusCode).toBe(422)
|
||||||
done()
|
done()
|
||||||
|
@ -145,7 +146,7 @@ describe 'CourseInstance', ->
|
||||||
courseID: course.get('_id')
|
courseID: course.get('_id')
|
||||||
name: createName('course instance ')
|
name: createName('course instance ')
|
||||||
seats: 1
|
seats: 1
|
||||||
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
|
request.post {uri: courseInstanceCreateURL, json: requestBody }, (err, res) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(res.statusCode).toBe(422)
|
expect(res.statusCode).toBe(422)
|
||||||
done()
|
done()
|
||||||
|
@ -163,7 +164,7 @@ describe 'CourseInstance', ->
|
||||||
courseID: course.get('_id')
|
courseID: course.get('_id')
|
||||||
name: createName('course instance ')
|
name: createName('course instance ')
|
||||||
seats: -1
|
seats: -1
|
||||||
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
|
request.post {uri: courseInstanceCreateURL, json: requestBody }, (err, res) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(res.statusCode).toBe(422)
|
expect(res.statusCode).toBe(422)
|
||||||
done()
|
done()
|
||||||
|
@ -218,3 +219,110 @@ describe 'CourseInstance', ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
describe 'Redeem prepaid code', ->
|
||||||
|
|
||||||
|
it 'Redeem prepaid code an instance of max 2', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
createCourse 0, (err, course) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
createCourseInstances user1, course.get('_id'), 2, token.id, (err, courseInstances) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
expect(courseInstances.length).toEqual(1)
|
||||||
|
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
loginNewUser (user2) ->
|
||||||
|
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
|
||||||
|
# Check prepaid
|
||||||
|
Prepaid.findById prepaid.id, (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
expect(prepaid.get('redeemers')?.length).toEqual(1)
|
||||||
|
expect(prepaid.get('redeemers')[0].date).toBeLessThan(new Date())
|
||||||
|
expect(prepaid.get('redeemers')[0].userID).toEqual(user2.get('_id'))
|
||||||
|
|
||||||
|
# Check course instance
|
||||||
|
CourseInstance.findById courseInstances[0].id, (err, courseInstance) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
members = courseInstance.get('members')
|
||||||
|
expect(members?.length).toEqual(2)
|
||||||
|
# TODO: must be a better way to check membership
|
||||||
|
usersFound = 0
|
||||||
|
for memberID in members
|
||||||
|
usersFound++ if memberID.equals(user1.get('_id'))
|
||||||
|
usersFound++ if memberID.equals(user2.get('_id'))
|
||||||
|
expect(usersFound).toEqual(2)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Redeem full prepaid code on instance of max 1', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
createCourse 0, (err, course) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
createCourseInstances user1, course.get('_id'), 1, token.id, (err, courseInstances) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
expect(courseInstances.length).toEqual(1)
|
||||||
|
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
loginNewUser (user2) ->
|
||||||
|
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
loginNewUser (user3) ->
|
||||||
|
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Redeem 50 count course prepaid codes 51 times, in parallel', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
seatCount = 50
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
createCourse 0, (err, course) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
createCourseInstances user1, course.get('_id'), seatCount, token.id, (err, courseInstances) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
expect(courseInstances.length).toEqual(1)
|
||||||
|
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
|
||||||
|
forbiddenResults = 0
|
||||||
|
makeRedeemCall = ->
|
||||||
|
(callback) ->
|
||||||
|
loginNewUser (user2) ->
|
||||||
|
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
if res.statusCode is 403
|
||||||
|
forbiddenResults++
|
||||||
|
else
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
callback err
|
||||||
|
tasks = (makeRedeemCall() for i in [1..seatCount+1])
|
||||||
|
async.parallel tasks, (err, results) ->
|
||||||
|
expect(err?).toEqual(false)
|
||||||
|
expect(forbiddenResults).toEqual(1)
|
||||||
|
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
return done(err) if err
|
||||||
|
expect(prepaid.get('redeemers')?.length).toEqual(prepaid.get('maxRedeemers'))
|
||||||
|
done()
|
||||||
|
|
Reference in a new issue