mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 00:40:56 -05:00
Course enroll page
Will add a prepaid purchase once the prepaid-v2 branch is merged into master.
This commit is contained in:
parent
46ee12ff9d
commit
9131d8668f
17 changed files with 628 additions and 7 deletions
|
@ -58,11 +58,12 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'contribute/diplomat': go('contribute/DiplomatView')
|
||||
'contribute/scribe': go('contribute/ScribeView')
|
||||
|
||||
'courses': go('courses/CoursesView')
|
||||
'courses/mock1': go('courses/mock1/CoursesView')
|
||||
'courses/mock1/enroll/:courseID': go('courses/mock1/CourseEnrollView')
|
||||
'courses/mock1/:courseID': go('courses/mock1/CourseDetailsView')
|
||||
'courses/mock1/:courseID/info': go('courses/mock1/CourseInfoView')
|
||||
'courses': go('courses/CoursesView')
|
||||
'courses/enroll(/:courseID)': go('courses/CourseEnrollView')
|
||||
'courses/:courseID': go('courses/CourseDetailsView')
|
||||
|
||||
'db/*path': 'routeToServer'
|
||||
'demo(/*subpath)': go('DemoView')
|
||||
|
|
|
@ -198,3 +198,11 @@ module.exports.getSponsoredSubsAmount = getSponsoredSubsAmount = (price=999, sub
|
|||
Math.round((1 - offset) * price + (subCount - 1 + offset) * price * 0.8)
|
||||
else
|
||||
Math.round((1 - offset) * price + 10 * price * 0.8 + (subCount - 11 + offset) * price * 0.6)
|
||||
|
||||
module.exports.getCoursesPrice = getSponsoredSubsAmount = (courses, seats=20) ->
|
||||
totalPricePerSeat = courses.reduce ((a, b) -> a + b.get('pricePerSeat')), 0
|
||||
if courses.length > 2
|
||||
pricePerSeat = Math.round(totalPricePerSeat / 2.0)
|
||||
else
|
||||
pricePerSeat = parseInt(totalPricePerSeat)
|
||||
seats * pricePerSeat
|
||||
|
|
|
@ -7,6 +7,7 @@ _.extend CourseSchema.properties,
|
|||
campaignID: c.objectId()
|
||||
concepts: c.array {title: 'Programming Concepts', uniqueItems: true}, c.concept
|
||||
description: {type: 'string'}
|
||||
pricePerSeat: {type: 'number', description: 'Price per seat in USD cents.'}
|
||||
screenshot: c.url {title: 'URL', description: 'Link to course screenshot.'}
|
||||
|
||||
c.extendBasicProperties CourseSchema, 'Course'
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
c = require './../schemas'
|
||||
|
||||
CourseInstanceSchema = c.object {title: 'Course Instance'}
|
||||
c.extendNamedProperties CourseInstanceSchema # name first
|
||||
|
||||
_.extend CourseInstanceSchema.properties,
|
||||
courseID: c.objectId()
|
||||
description: {type: 'string'}
|
||||
members: c.array {title: 'Members'}, c.objectId()
|
||||
name: {type: 'string'}
|
||||
ownerID: c.objectId()
|
||||
prepaidID: c.objectId()
|
||||
|
||||
|
|
7
app/styles/courses/course-details.sass
Normal file
7
app/styles/courses/course-details.sass
Normal file
|
@ -0,0 +1,7 @@
|
|||
#course-details-view
|
||||
|
||||
.edit-description-input
|
||||
width: 100%
|
||||
|
||||
.edit-name-input
|
||||
width: 50%
|
14
app/styles/courses/course-enroll.sass
Normal file
14
app/styles/courses/course-enroll.sass
Normal file
|
@ -0,0 +1,14 @@
|
|||
#course-enroll-view
|
||||
|
||||
.btn-buy
|
||||
margin: 20px 0px
|
||||
|
||||
.center
|
||||
text-align: center
|
||||
|
||||
.enroll-container
|
||||
margin: 5% 20%
|
||||
width: 60%
|
||||
|
||||
.class-name
|
||||
width: 300px
|
31
app/templates/courses/course-details.jade
Normal file
31
app/templates/courses/course-details.jade
Normal file
|
@ -0,0 +1,31 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
|
||||
//- DO NOT localize / i18n
|
||||
|
||||
div
|
||||
span *UNDER CONSTRUCTION, send feedback to
|
||||
a.spl(href='mailto:team@codecombat.com') team@codecombat.com
|
||||
div(style='border-bottom: 1px solid black;')
|
||||
|
||||
h1(style='text-align: center;') Course
|
||||
if course
|
||||
div= course.get('name')
|
||||
div= course.get('description')
|
||||
div= course.get('campaignID')
|
||||
div= course.get('concepts')
|
||||
else
|
||||
div No course found.
|
||||
|
||||
h1(style='text-align: center;') Class
|
||||
if courseInstance
|
||||
p
|
||||
div= courseInstance.get('name') || 'Class Name'
|
||||
div= courseInstance.get('description')
|
||||
div= courseInstance.get('courseID')
|
||||
div= courseInstance.get('ownerID')
|
||||
div= courseInstance.get('members')
|
||||
div= courseInstance.get('prepaidID')
|
||||
else
|
||||
p No classes found.
|
77
app/templates/courses/course-enroll.jade
Normal file
77
app/templates/courses/course-enroll.jade
Normal file
|
@ -0,0 +1,77 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
|
||||
//- DO NOT localize / i18n
|
||||
|
||||
div
|
||||
span *UNDER CONSTRUCTION, send feedback to
|
||||
a.spl(href='mailto:team@codecombat.com') team@codecombat.com
|
||||
div(style='border-bottom: 1px solid black')
|
||||
|
||||
if state === 'declined' || state === 'unknown_error'
|
||||
p
|
||||
.alert.alert-danger ERROR #{stateMessage}
|
||||
|
||||
if state === 'creating'
|
||||
p
|
||||
.alert.alert-info Creating class...
|
||||
else if state === 'purchasing'
|
||||
p
|
||||
.alert.alert-info Purchasing course...
|
||||
else
|
||||
.well.well-lg.enroll-container
|
||||
if price > 0
|
||||
h1.center Buy Course
|
||||
else
|
||||
h1.center Create Class
|
||||
h3 1. Course
|
||||
if courses.length > 2
|
||||
p Select 'All Courses' for a 50% discount!
|
||||
.form-group
|
||||
select.form-control.course-select
|
||||
each course in courses
|
||||
option(value="#{course.id}")= course.get('name')
|
||||
if courses.length > 1
|
||||
option(value="All Courses") All Courses
|
||||
|
||||
h3 2. Number of students
|
||||
p Enter the number of students you need for this class.
|
||||
input.input-seats(type='text', value="#{seats}")
|
||||
|
||||
h3 3. Name your class
|
||||
p This will be displayed on the course page for you and your students. It can be changed later.
|
||||
input.class-name(type='text', placeholder="Mrs. Smith's 4th Period", value="#{className ? className : ''}")
|
||||
|
||||
if price > 0
|
||||
h3 4. Buy
|
||||
else
|
||||
h3 4. Create Class
|
||||
p
|
||||
if price > 0
|
||||
span.spr You are purchasing a license for
|
||||
else
|
||||
span.spr You are creating a class for
|
||||
strong.spr #{selectedCourseTitle}
|
||||
span.spr for
|
||||
strong #{seats} students
|
||||
| .
|
||||
p Afterwards you will receive an unlock code to distribute to your students, which they can use to enroll in your class.
|
||||
p.center
|
||||
if price > 0
|
||||
button.btn.btn-success.btn-lg.btn-buy $#{(price / 100.0).toFixed(2)}
|
||||
else
|
||||
button.btn.btn-success.btn-lg.btn-buy Create Class
|
||||
+trial-and-questions
|
||||
|
||||
mixin trial-and-questions
|
||||
h3 Free trial for teachers!
|
||||
p
|
||||
span.spr Please fill out our
|
||||
a(href='/teachers/freetrial', data-i18n="teachers.teacher_subs_2")
|
||||
span.spl to get individual access to all courses for evalutaion purposes.
|
||||
|
||||
h3 Questions?
|
||||
p
|
||||
span Please contact
|
||||
a.spl(href='mailto:team@codecombat.com') team@codecombat.com
|
38
app/views/courses/CourseDetailsView.coffee
Normal file
38
app/views/courses/CourseDetailsView.coffee
Normal file
|
@ -0,0 +1,38 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/courses/course-details'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Course = require 'models/Course'
|
||||
CourseInstance = require 'models/CourseInstance'
|
||||
|
||||
# TODO: logged out experience
|
||||
# TODO: no course instances
|
||||
# TODO: no course instance selected
|
||||
|
||||
module.exports = class CourseDetailsView extends RootView
|
||||
id: 'course-details-view'
|
||||
template: template
|
||||
|
||||
constructor: (options, @courseID) ->
|
||||
super options
|
||||
@courseInstanceID = options.courseInstanceID
|
||||
@course = new Course _id: @courseID
|
||||
@supermodel.loadModel @course, 'course', cache: false
|
||||
if @courseInstanceID
|
||||
@courseInstance = new CourseInstance _id: @courseInstanceID
|
||||
@supermodel.loadModel @courseInstance, 'course_instance', cache: false
|
||||
else if !me.isAnonymous()
|
||||
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
||||
@listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded
|
||||
@supermodel.loadCollection(@courseInstances, 'course_instances')
|
||||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
context.course = @course
|
||||
context.courseInstance = @courseInstance
|
||||
context
|
||||
|
||||
onCourseInstancesLoaded: ->
|
||||
if @courseInstances.models.length is 1
|
||||
@courseInstance = @courseInstances.models[0]
|
||||
else if @courseInstances.models.length > 0
|
||||
@courseInstance = @courseInstances.models[0]
|
133
app/views/courses/CourseEnrollView.coffee
Normal file
133
app/views/courses/CourseEnrollView.coffee
Normal file
|
@ -0,0 +1,133 @@
|
|||
app = require 'core/application'
|
||||
AuthModal = require 'views/core/AuthModal'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Course = require 'models/Course'
|
||||
{getCoursesPrice} = require 'core/utils'
|
||||
RootView = require 'views/core/RootView'
|
||||
stripeHandler = require 'core/services/stripe'
|
||||
template = require 'templates/courses/course-enroll'
|
||||
|
||||
module.exports = class CourseEnrollView extends RootView
|
||||
id: 'course-enroll-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click .btn-buy': 'onClickBuy'
|
||||
'change .class-name': 'onNameChange'
|
||||
'change .course-select': 'onChangeCourse'
|
||||
'change .input-seats': 'onSeatsChange'
|
||||
|
||||
subscriptions:
|
||||
'stripe:received-token': 'onStripeReceivedToken'
|
||||
|
||||
constructor: (options, @courseID=0) ->
|
||||
super options
|
||||
@courseID ?= options.courseID
|
||||
@seats = 20
|
||||
|
||||
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
||||
@listenTo @courses, 'sync', @onCoursesLoaded
|
||||
@supermodel.loadCollection(@courses, 'courses')
|
||||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
context.className = @className
|
||||
context.courses = @courses.models
|
||||
context.price = @price ? 0
|
||||
context.seats = @seats
|
||||
context.selectedCourse = @selectedCourse
|
||||
context.selectedCourseTitle = @selectedCourse?.get('name') ? 'All Courses'
|
||||
context.state = @state
|
||||
context.stateMessage = @stateMessage
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
if @selectedCourse
|
||||
@$el.find('.course-select').val(@selectedCourse.id)
|
||||
else
|
||||
@$el.find('.course-select').val('All Courses')
|
||||
|
||||
onCoursesLoaded: ->
|
||||
if @courseID
|
||||
@selectedCourse = _.find @courses.models, (a) => a.id is @courseID
|
||||
else if @courses.models.length > 0
|
||||
@selectedCourse = @courses.models[0]
|
||||
@renderNewPrice()
|
||||
|
||||
onClickBuy: (e) ->
|
||||
return @openModalView new AuthModal() if me.isAnonymous()
|
||||
|
||||
if @seats < 1 or not _.isFinite(@seats)
|
||||
alert("Please enter the maximum number of students needed for your class.")
|
||||
return
|
||||
|
||||
if @price is 0
|
||||
@state = 'creating'
|
||||
@createClass()
|
||||
return
|
||||
|
||||
@state = undefined
|
||||
@stateMessage = undefined
|
||||
@render()
|
||||
|
||||
# Show Stripe handler
|
||||
courseTitle = @selectedCourse?.get('name') ? 'All Courses'
|
||||
application.tracker?.trackEvent 'Started course purchase', {course: courseTitle, price: @price, seats: @seats}
|
||||
stripeHandler.open
|
||||
amount: @price
|
||||
description: "#{courseTitle} for #{@seats} students"
|
||||
bitcoin: true
|
||||
alipay: if me.get('chinaVersion') or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
||||
|
||||
onStripeReceivedToken: (e) ->
|
||||
@state = 'purchasing'
|
||||
@render?()
|
||||
@createClass(e.token.id)
|
||||
|
||||
onChangeCourse: (e) ->
|
||||
@selectedCourse = _.find @courses.models, (a) -> a.id is $(e.target).val()
|
||||
@renderNewPrice()
|
||||
|
||||
onNameChange: (e) ->
|
||||
@className = $('.class-name').val()
|
||||
|
||||
onSeatsChange: (e) ->
|
||||
@seats = $(e.target).val()
|
||||
@seats = 20 if @seats < 1 or not _.isFinite(@seats)
|
||||
@renderNewPrice()
|
||||
|
||||
createClass: (token) ->
|
||||
data =
|
||||
name: $('.class-name').val()
|
||||
seats: @seats
|
||||
token: token
|
||||
data.courseID = @selectedCourse.id if @selectedCourse
|
||||
jqxhr = $.post('/db/course_instance/-/create', data)
|
||||
jqxhr.done (data, textStatus, jqXHR) =>
|
||||
application.tracker?.trackEvent 'Finished course purchase', {course: @selectedCourse?.get('name') ? 'All Courses', price: @price, seats: @seats}
|
||||
# TODO: handle fetch errors
|
||||
me.fetch(cache: false).always =>
|
||||
courseID = @selectedCourse?.id ? @courses.models[0]?.id
|
||||
viewArgs = if data?.length > 0 then [courseInstanceID: data[0]._id, courseID] else [{}, courseID]
|
||||
Backbone.Mediator.publish 'router:navigate',
|
||||
route: "/courses/#{courseID}"
|
||||
viewClass: 'views/courses/CourseDetailsView'
|
||||
viewArgs: viewArgs
|
||||
jqxhr.fail (xhr, textStatus, errorThrown) =>
|
||||
console.error 'Got an error purchasing a course:', textStatus, errorThrown
|
||||
application.tracker?.trackEvent 'Failed course purchase', status: textStatus
|
||||
if xhr.status is 402
|
||||
@state = 'declined'
|
||||
@stateMessage = arguments[2]
|
||||
else
|
||||
@state = 'unknown_error'
|
||||
@stateMessage = "#{xhr.status}: #{xhr.responseText}"
|
||||
@render?()
|
||||
|
||||
renderNewPrice: ->
|
||||
if @selectedCourse
|
||||
@price = getCoursesPrice([@selectedCourse], @seats)
|
||||
else
|
||||
@price = getCoursesPrice(@courses.models, @seats)
|
||||
@render?()
|
|
@ -4,6 +4,8 @@
|
|||
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
|
||||
|
||||
// NOTE: uses name as unique identifier, so changing the name will insert a new course
|
||||
// NOTE: concepts should match actual campaign levels
|
||||
// NOTE: pricePerSeat in USD cents
|
||||
|
||||
var documents =
|
||||
[
|
||||
|
@ -13,7 +15,26 @@ var documents =
|
|||
campaignID: ObjectId("55b29efd1cd6abe8ce07db0d"),
|
||||
concepts: ['basic_syntax', 'arguments', 'while_loops', 'strings', 'variables'],
|
||||
description: "Learn basic syntax, while loops, and the CodeCombat environment.",
|
||||
pricePerSeat: NumberInt(0),
|
||||
screenshot: "/images/pages/courses/101_info.png"
|
||||
},
|
||||
{
|
||||
name: "Computer Science 2",
|
||||
slug: "computer-science-2",
|
||||
campaignID: ObjectId("55b29efd1cd6abe8ce07db0d"),
|
||||
concepts: ['basic_syntax', 'arguments', 'while_loops', 'strings', 'variables', 'if_statements'],
|
||||
description: "Introduce Arguments, Variables, If Statements, and Arithmetic.",
|
||||
pricePerSeat: NumberInt(400),
|
||||
screenshot: "/images/pages/courses/102_info.png"
|
||||
},
|
||||
{
|
||||
name: "Computer Science 3",
|
||||
slug: "computer-science-3",
|
||||
campaignID: ObjectId("55b29efd1cd6abe8ce07db0d"),
|
||||
concepts: ['if_statements', 'arithmetic'],
|
||||
description: "Learn how to handle input.",
|
||||
pricePerSeat: NumberInt(400),
|
||||
screenshot: "/images/pages/courses/103_info.png"
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -75,6 +75,7 @@ module.exports = class Handler
|
|||
props
|
||||
|
||||
# sending functions
|
||||
sendUnauthorizedError: (res) -> errors.unauthorized(res)
|
||||
sendForbiddenError: (res) -> errors.forbidden(res)
|
||||
sendNotFoundError: (res, message) -> errors.notFound(res, message)
|
||||
sendMethodNotAllowed: (res, message) -> errors.badMethod(res, @allowedMethods, message)
|
||||
|
|
|
@ -5,9 +5,6 @@ jsonSchema = require '../../app/schemas/models/course_instance.schema'
|
|||
|
||||
CourseInstanceSchema = new mongoose.Schema {}, {strict: false, minimize: false, read:config.mongo.readpref}
|
||||
|
||||
CourseInstanceSchema.plugin plugins.NamedPlugin
|
||||
CourseInstanceSchema.plugin plugins.SearchablePlugin, {searchable: ['name', 'description']}
|
||||
|
||||
CourseInstanceSchema.statics.privateProperties = []
|
||||
CourseInstanceSchema.statics.editableProperties = [
|
||||
'description'
|
||||
|
|
|
@ -1,13 +1,98 @@
|
|||
mongoose = require 'mongoose'
|
||||
async = require 'async'
|
||||
Handler = require '../commons/Handler'
|
||||
{getCoursesPrice} = require '../../app/core/utils'
|
||||
Course = require './Course'
|
||||
CourseInstance = require './CourseInstance'
|
||||
Prepaid = require '../prepaids/Prepaid'
|
||||
|
||||
CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||
modelClass: CourseInstance
|
||||
jsonSchema: require '../../app/schemas/models/course_instance.schema'
|
||||
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE']
|
||||
|
||||
logError: (user, msg) ->
|
||||
console.warn "Course error: #{user.get('slug')} (#{user._id}): '#{msg}'"
|
||||
|
||||
hasAccess: (req) ->
|
||||
req.method is 'GET' or req.user?.isAdmin()
|
||||
|
||||
hasAccessToDocument: (req, document, method=null) ->
|
||||
return true if _.find document?.get('members'), (a) -> a.equals(req.user?.get('_id'))
|
||||
req.user?.isAdmin()
|
||||
|
||||
getByRelationship: (req, res, args...) ->
|
||||
relationship = args[1]
|
||||
return @createAPI(req, res) if relationship is 'create'
|
||||
super arguments...
|
||||
|
||||
createAPI: (req, res) ->
|
||||
return @sendUnauthorizedError(res) unless req.user?
|
||||
|
||||
# Required Input
|
||||
seats = req.body.seats
|
||||
unless seats > 0
|
||||
@logError(req.user, 'Course create API missing required seats count')
|
||||
return @sendBadInputError(res, 'Missing required seats count')
|
||||
# Optional - unspecified means create instances for all courses
|
||||
courseID = req.body.courseID
|
||||
# Optional
|
||||
name = req.body.name
|
||||
# Optional - as long as course(s) are all free
|
||||
stripeToken = req.body.token
|
||||
|
||||
@getCourses courseID, (err, courses) =>
|
||||
if err
|
||||
@logError(req.user, err)
|
||||
return @sendDatabaseError(res, err)
|
||||
|
||||
price = getCoursesPrice(courses, seats)
|
||||
if price > 0 and not stripeToken
|
||||
@logError(req.user, 'Course create API missing required Stripe token')
|
||||
return @sendBadInputError(res, 'Missing required Stripe token')
|
||||
|
||||
# TODO: purchase prepaid for courses, price, and seats
|
||||
Prepaid.generateNewCode (code) =>
|
||||
return @sendDatabaseError(res, 'Database error.') unless code
|
||||
prepaid = new Prepaid
|
||||
creator: req.user.get('_id')
|
||||
type: 'course'
|
||||
code: code
|
||||
properties:
|
||||
courseIDs: (course.get('_id') for course in courses)
|
||||
prepaid.set('maxRedeemers', seats) if seats
|
||||
prepaid.save (err) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
|
||||
courseInstances = []
|
||||
makeCreateInstanceFn = (course, name, prepaid) =>
|
||||
(done) =>
|
||||
@createInstance req, course, name, prepaid, (err, newInstance)=>
|
||||
courseInstances.push newInstance unless err
|
||||
done(err)
|
||||
# tasks = []
|
||||
# tasks.push(makeCreateInstanceFn(course, name, prepaid)) for course in courses
|
||||
tasks = (makeCreateInstanceFn(course, name, prepaid) for course in courses)
|
||||
async.parallel tasks, (err, results) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@sendCreated(res, courseInstances)
|
||||
|
||||
createInstance: (req, course, name, prepaid, done) =>
|
||||
courseInstance = new CourseInstance
|
||||
courseID: course.get('_id')
|
||||
members: [req.user.get('_id')]
|
||||
name: name
|
||||
ownerID: req.user.get('_id')
|
||||
prepaidID: prepaid.get('_id')
|
||||
courseInstance.save (err, newInstance) =>
|
||||
done(err, newInstance)
|
||||
|
||||
getCourses: (courseID, done) =>
|
||||
if courseID
|
||||
Course.findById courseID, (err, document) =>
|
||||
done(err, [document])
|
||||
else
|
||||
Course.find {}, (err, documents) =>
|
||||
done(err, documents)
|
||||
|
||||
|
||||
module.exports = new CourseInstanceHandler()
|
||||
|
|
|
@ -11,6 +11,7 @@ log = require 'winston'
|
|||
moment = require 'moment'
|
||||
AnalyticsLogEvent = require '../analytics/AnalyticsLogEvent'
|
||||
Clan = require '../clans/Clan'
|
||||
CourseInstance = require '../courses/CourseInstance'
|
||||
LevelSession = require '../levels/sessions/LevelSession'
|
||||
LevelSessionHandler = require '../levels/sessions/level_session_handler'
|
||||
Payment = require '../payments/Payment'
|
||||
|
@ -309,6 +310,7 @@ UserHandler = class UserHandler extends Handler
|
|||
return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions'
|
||||
return @getCandidates(req, res) if args[1] is 'candidates'
|
||||
return @getClans(req, res, args[0]) if args[1] is 'clans'
|
||||
return @getCourseInstances(req, res, args[0]) if args[1] is 'course_instances'
|
||||
return @getEmployers(req, res) if args[1] is 'employers'
|
||||
return @getSimulatorLeaderboard(req, res, args[0]) if args[1] is 'simulatorLeaderboard'
|
||||
return @getMySimulatorLeaderboardRank(req, res, args[0]) if args[1] is 'simulator_leaderboard_rank'
|
||||
|
@ -605,6 +607,13 @@ UserHandler = class UserHandler extends Handler
|
|||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, documents)
|
||||
|
||||
getCourseInstances: (req, res, userIDOrSlug) ->
|
||||
@getDocumentForIdOrSlug userIDOrSlug, (err, user) =>
|
||||
return @sendNotFoundError(res) unless user
|
||||
CourseInstance.find {members: {$in: [user.get('_id')]}}, (err, documents) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, documents)
|
||||
|
||||
formatCandidate: (authorized, document) ->
|
||||
fields = if authorized then ['name', 'jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['_id','jobProfile', 'jobProfileApproved']
|
||||
obj = _.pick document.toObject(), fields
|
||||
|
|
|
@ -30,6 +30,8 @@ models_path = [
|
|||
'../../server/articles/Article'
|
||||
'../../server/campaigns/Campaign'
|
||||
'../../server/clans/Clan'
|
||||
'../../server/courses/Course'
|
||||
'../../server/courses/CourseInstance'
|
||||
'../../server/levels/Level'
|
||||
'../../server/levels/components/LevelComponent'
|
||||
'../../server/levels/systems/LevelSystem'
|
||||
|
|
195
test/server/functional/course_instance.spec.coffee
Normal file
195
test/server/functional/course_instance.spec.coffee
Normal file
|
@ -0,0 +1,195 @@
|
|||
async = require 'async'
|
||||
config = require '../../../server_config'
|
||||
require '../common'
|
||||
stripe = require('stripe')(config.stripe.secretKey)
|
||||
|
||||
# TODO: add permissiosn tests
|
||||
|
||||
describe 'CourseInstance', ->
|
||||
courseInstanceURL = getURL('/db/course_instance/-/create')
|
||||
userURL = getURL('/db/user')
|
||||
|
||||
nameCount = 0
|
||||
createName = (name) -> name + nameCount++
|
||||
|
||||
createCourse = (pricePerSeat, done) ->
|
||||
name = createName 'course '
|
||||
course = new Course
|
||||
name: name
|
||||
campaignID: ObjectId("55b29efd1cd6abe8ce07db0d")
|
||||
concepts: ['basic_syntax', 'arguments', 'while_loops', 'strings', 'variables']
|
||||
description: "Learn basic syntax, while loops, and the CodeCombat environment."
|
||||
pricePerSeat: pricePerSeat
|
||||
screenshot: "/images/pages/courses/101_info.png"
|
||||
course.save (err, course) ->
|
||||
return done(err) if err
|
||||
done(err, course)
|
||||
|
||||
createCourseInstances = (user, courseID, seats, token, done) ->
|
||||
name = createName 'course instance '
|
||||
requestBody =
|
||||
courseID: courseID
|
||||
name: name
|
||||
seats: seats
|
||||
token: token
|
||||
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
|
||||
expect(err).toBeNull()
|
||||
expect(res.statusCode).toBe(201)
|
||||
CourseInstance.find {name: name}, (err, courseInstances) ->
|
||||
expect(err).toBeNull()
|
||||
|
||||
makeCourseInstanceVerifyFn = (courseInstance) ->
|
||||
(done) ->
|
||||
expect(courseInstance.get('name')).toEqual(name)
|
||||
expect(courseInstance.get('ownerID')).toEqual(user.get('_id'))
|
||||
expect(courseInstance.get('members')).toContain(user.get('_id'))
|
||||
query = {$and: [{creator: user.get('_id')}]}
|
||||
query.$and.push {'properties.courseIDs': {$in: [courseID]}} if courseID
|
||||
Prepaid.find query, (err, prepaids) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
expect(prepaids?.length).toEqual(1)
|
||||
return done() unless prepaids?.length > 0
|
||||
expect(prepaids[0].get('type')).toEqual('course')
|
||||
expect(prepaids[0].get('maxRedeemers')).toEqual(seats) if seats
|
||||
|
||||
# TODO: verify Payment
|
||||
|
||||
done(err)
|
||||
|
||||
tasks = []
|
||||
for courseInstance in courseInstances
|
||||
tasks.push makeCourseInstanceVerifyFn(courseInstance)
|
||||
async.parallel tasks, (err) =>
|
||||
return done(err) if err
|
||||
done(err, courseInstances)
|
||||
|
||||
it 'Clear database users and clans', (done) ->
|
||||
clearModels [User, Course, CourseInstance, Prepaid], (err) ->
|
||||
throw err if err
|
||||
done()
|
||||
|
||||
describe 'Single courses', ->
|
||||
it 'Create for free course 1 seat', (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)
|
||||
done()
|
||||
|
||||
it 'Create for free course no seats', (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
|
||||
name = createName 'course instance '
|
||||
requestBody =
|
||||
courseID: course.get('_id')
|
||||
name: createName('course instance ')
|
||||
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
|
||||
expect(err).toBeNull()
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
it 'Create for free course no token', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
createCourse 0, (err, course) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
createCourseInstances user1, course.get('_id'), 2, null, (err, courseInstances) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
expect(courseInstances.length).toEqual(1)
|
||||
done()
|
||||
|
||||
it 'Create for paid course 1 seat', (done) ->
|
||||
stripe.tokens.create {
|
||||
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||
}, (err, token) ->
|
||||
loginNewUser (user1) ->
|
||||
createCourse 7000, (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)
|
||||
done()
|
||||
|
||||
it 'Create for paid course 50 seats', (done) ->
|
||||
stripe.tokens.create {
|
||||
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||
}, (err, token) ->
|
||||
loginNewUser (user1) ->
|
||||
createCourse 7000, (err, course) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
createCourseInstances user1, course.get('_id'), 50, token.id, (err, courseInstances) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
expect(courseInstances.length).toEqual(1)
|
||||
done()
|
||||
|
||||
it 'Create for paid course no token', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
createCourse 7000, (err, course) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
name = createName 'course instance '
|
||||
requestBody =
|
||||
courseID: course.get('_id')
|
||||
name: createName('course instance ')
|
||||
seats: 1
|
||||
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
|
||||
expect(err).toBeNull()
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
it 'Create for paid course -1 seats', (done) ->
|
||||
stripe.tokens.create {
|
||||
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||
}, (err, token) ->
|
||||
loginNewUser (user1) ->
|
||||
createCourse 7000, (err, course) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
name = createName 'course instance '
|
||||
requestBody =
|
||||
courseID: course.get('_id')
|
||||
name: createName('course instance ')
|
||||
seats: -1
|
||||
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
|
||||
expect(err).toBeNull()
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
describe 'All Courses', ->
|
||||
it 'Create for 50 seats', (done) ->
|
||||
stripe.tokens.create {
|
||||
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||
}, (err, token) ->
|
||||
loginNewUser (user1) ->
|
||||
createCourse 7000, (err, course1) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
createCourse 7000, (err, course2) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
createCourseInstances user1, null, 50, token.id, (err, courseInstances) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
Course.find {}, (err, courses) ->
|
||||
expect(err).toBeNull()
|
||||
return done(err) if err
|
||||
expect(courseInstances.length).toEqual(courses.length)
|
||||
done()
|
Loading…
Reference in a new issue