mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Add Enrollment start/end dates, remove self-serve
* Refactor several related endpoints and views * Redesign EnrollmentView, add TeacherContactModal * Add "Enrollment Status" tab to TeacherClassView * Delete PurchaseCoursesView and related files * Style-flatten RemoveStudentModal * Fix error handling in ActivateLicensesModal * TeacherCoursesView loads faster by only loading course campaigns, and not load prepaids
This commit is contained in:
parent
8496343a02
commit
f0fa88206d
65 changed files with 1975 additions and 1163 deletions
7
app/collections/StripeCoupons.coffee
Normal file
7
app/collections/StripeCoupons.coffee
Normal file
|
@ -0,0 +1,7 @@
|
|||
StripeCoupon = require 'models/StripeCoupon'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
|
||||
module.exports = class StripeCoupons extends CocoCollection
|
||||
model: StripeCoupon
|
||||
url: '/stripe/coupons'
|
||||
|
|
@ -29,3 +29,9 @@ module.exports = class Users extends CocoCollection
|
|||
@remove @filter (user) ->
|
||||
user.get('deleted')
|
||||
true
|
||||
|
||||
search: (term) ->
|
||||
return @slice() unless term
|
||||
term = term.toLowerCase()
|
||||
return @filter (user) ->
|
||||
user.broadName().toLowerCase().indexOf(term) > -1 or (user.get('email') ? '').indexOf(term) > -1
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
module.exports.sendContactMessage = (contactMessageObject, modal) ->
|
||||
modal?.find('.sending-indicator').show()
|
||||
jqxhr = $.post '/contact', contactMessageObject, (response) ->
|
||||
return unless modal
|
||||
modal.find('.sending-indicator').hide()
|
||||
modal.find('#contact-message').val('Thanks!')
|
||||
_.delay ->
|
||||
modal.find('#contact-message').val('')
|
||||
modal.modal 'hide'
|
||||
, 1000
|
||||
module.exports = {
|
||||
sendContactMessage: (contactMessageObject, modal) ->
|
||||
# deprecated
|
||||
modal?.find('.sending-indicator').show()
|
||||
return $.post '/contact', contactMessageObject, (response) ->
|
||||
return unless modal
|
||||
modal.find('.sending-indicator').hide()
|
||||
modal.find('#contact-message').val('Thanks!')
|
||||
_.delay ->
|
||||
modal.find('#contact-message').val('')
|
||||
modal.modal 'hide'
|
||||
, 1000
|
||||
|
||||
send: (options={}) ->
|
||||
options.type = 'POST'
|
||||
options.url = '/contact'
|
||||
$.ajax(options)
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module.exports = nativeDescription: "English", englishDescription: "English", translation:
|
||||
module.exports = nativeDescription: "English", englishDescription: "English", translation:
|
||||
home:
|
||||
slogan: "Learn to Code by Playing a Game"
|
||||
no_ie: "CodeCombat does not run in Internet Explorer 8 or older. Sorry!" # Warning that only shows up in IE8 and older
|
||||
|
@ -1120,7 +1120,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
|
|||
already_enrolled: "already enrolled"
|
||||
licenses_remaining: "licenses remaining:"
|
||||
insufficient_enrollments: "insufficient paid enrollments"
|
||||
get_enrollments: "Get More Enrollments"
|
||||
get_enrollments: "Get Enrollments" # {change}
|
||||
change_language: "Change Course Language"
|
||||
keep_using: "Keep Using"
|
||||
switch_to: "Switch To"
|
||||
|
@ -1364,6 +1364,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
|
|||
earliest_incomplete: "Earliest incomplete level"
|
||||
latest_complete: "Latest completed level"
|
||||
enroll_student: "Enroll student"
|
||||
revoke_enrollment: "Revoke Enrollment"
|
||||
adding_students: "Adding students"
|
||||
course_progress: "Course Progress"
|
||||
not_applicable: "N/A"
|
||||
|
@ -1399,8 +1400,8 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
|
|||
enrollments_blurb_1: "Students taking Computer Science" # Enrollments page
|
||||
enrollments_blurb_2: "require enrollments to access the courses."
|
||||
credits_available: "Credits Available"
|
||||
total_unique_students: "Total Unique Students"
|
||||
total_enrolled_students: "Total Enrolled Students"
|
||||
total_unique_students: "Total Students (unique)" # {change}
|
||||
total_enrolled_students: "Total Enrolled" # {change}
|
||||
unenrolled_students: "Unenrolled Students"
|
||||
add_enrollment_credits: "Add Enrollment Credits"
|
||||
purchasing: "Purchasing..."
|
||||
|
@ -1419,6 +1420,23 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
|
|||
send_recovery_email: "Send recovery email"
|
||||
change_password: "Change Password"
|
||||
changed: "Changed"
|
||||
available_credits: "Available Credits"
|
||||
pending_credits: "Pending Credits"
|
||||
credits: "credits"
|
||||
start_date: "start date:"
|
||||
end_date: "end date:"
|
||||
num_enrollments_needed: "Number of enrollments needed:"
|
||||
get_enrollments_blurb: " We'll help you build a solution that meets the needs of your class, school or district."
|
||||
enroll_request_sent_blurb1: "Thanks! Your request has been sent."
|
||||
enroll_request_sent_blurb2: "Our classroom success team will be in touch shortly to help you find the best solution for your students' needs!"
|
||||
enroll_request_sent_blurb3: "Please reach out to <a href='mailto:schools@codecombat.com'>schools@codecombat.com</a> if you have additional questions at this time."
|
||||
request_sent: "Request Sent!"
|
||||
enrollment_status: "Enrollment Status"
|
||||
status_expired: "Expired on {{date}}"
|
||||
status_not_enrolled: "Not Enrolled"
|
||||
status_enrolled: "Expires on {{date}}"
|
||||
revoke_confirm: "Are you sure you want to revoke enrollment from {{student_name}}?"
|
||||
revoking: "Revoking..."
|
||||
|
||||
classes:
|
||||
archmage_title: "Archmage"
|
||||
|
@ -1576,7 +1594,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
|
|||
scribe_introduction_url_mozilla: "Mozilla Developer Network"
|
||||
scribe_introduction_suf: " has built. If your idea of fun is articulating the concepts of programming in Markdown form, then this class might be for you."
|
||||
scribe_attribute_1: "Skill in words is pretty much all you need. Not only grammar and spelling, but able to convey complicated ideas to others."
|
||||
contact_us_url: "Contact us"
|
||||
contact_us_url: "Contact Us" # {change}
|
||||
scribe_join_description: "tell us a little about yourself, your experience with programming and what sort of things you'd like to write about. We'll go from there!"
|
||||
scribe_subscribe_desc: "Get emails about article writing announcements."
|
||||
diplomat_introduction_pref: "So, if there's one thing we learned from the "
|
||||
|
|
|
@ -19,3 +19,31 @@ module.exports = class Prepaid extends CocoModel
|
|||
maxRedeemers = @get('maxRedeemers')
|
||||
if _.isString(maxRedeemers)
|
||||
@set 'maxRedeemers', parseInt(maxRedeemers)
|
||||
|
||||
status: ->
|
||||
endDate = @get('endDate')
|
||||
if endDate and new Date(endDate) < new Date()
|
||||
return 'expired'
|
||||
|
||||
startDate = @get('startDate')
|
||||
if startDate and new Date(startDate) > new Date()
|
||||
return 'pending'
|
||||
|
||||
if @openSpots() <= 0
|
||||
return 'empty'
|
||||
|
||||
return 'available'
|
||||
|
||||
redeem: (user, options={}) ->
|
||||
options.url = _.result(@, 'url')+'/redeemers'
|
||||
options.type = 'POST'
|
||||
options.data ?= {}
|
||||
options.data.userID = user.id or user
|
||||
@fetch(options)
|
||||
|
||||
revoke: (user, options={}) ->
|
||||
options.url = _.result(@, 'url')+'/redeemers'
|
||||
options.type = 'DELETE'
|
||||
options.data ?= {}
|
||||
options.data.userID = user.id or user
|
||||
@fetch(options)
|
||||
|
|
19
app/models/StripeCoupon.coffee
Normal file
19
app/models/StripeCoupon.coffee
Normal file
|
@ -0,0 +1,19 @@
|
|||
CocoModel = require './CocoModel'
|
||||
|
||||
module.exports = class StripeCoupon extends CocoModel
|
||||
@className: 'StripeCoupon'
|
||||
@schema: {}
|
||||
urlRoot: '/stripe/coupons'
|
||||
idAttribute: 'id'
|
||||
|
||||
formatString: ->
|
||||
bits = [@id]
|
||||
if @get('percent_off')
|
||||
bits.push "(#{@get('percent_off')}% off)"
|
||||
else if @get('amount_off')
|
||||
bits.push "($#{@get('amount_off')} off)"
|
||||
if @get('duration')
|
||||
bits.push "(duration: #{@get('duration')})"
|
||||
if @redeem_by
|
||||
bits.push "(redeem by: #{moment(@get('redeem_by')).format('lll')}"
|
||||
return bits.join(' ')
|
|
@ -208,9 +208,6 @@ module.exports = class User extends CocoModel
|
|||
return true if me.hasSubscription()
|
||||
return false
|
||||
|
||||
isEnrolled: ->
|
||||
Boolean(@get('coursePrepaidID'))
|
||||
|
||||
isOnPremiumServer: ->
|
||||
me.get('country') in ['china', 'brazil']
|
||||
|
||||
|
@ -225,6 +222,14 @@ module.exports = class User extends CocoModel
|
|||
@trigger 'email-verify-error'
|
||||
})
|
||||
|
||||
isEnrolled: -> @prepaidStatus() is 'enrolled'
|
||||
|
||||
prepaidStatus: -> # 'not-enrolled', 'enrolled', 'expired'
|
||||
coursePrepaid = @get('coursePrepaid')
|
||||
return 'not-enrolled' unless coursePrepaid
|
||||
return 'enrolled' unless coursePrepaid.endDate
|
||||
return if coursePrepaid.endDate > new Date().toISOString() then 'enrolled' else 'expired'
|
||||
|
||||
# Function meant for "me"
|
||||
|
||||
spy: (user, options={}) ->
|
||||
|
@ -279,6 +284,12 @@ module.exports = class User extends CocoModel
|
|||
options.data.facebookID = facebookID
|
||||
options.data.facebookAccessToken = application.facebookHandler.token()
|
||||
@fetch(options)
|
||||
|
||||
makeCoursePrepaid: ->
|
||||
coursePrepaid = @get('coursePrepaid')
|
||||
return null unless coursePrepaid
|
||||
Prepaid = require 'models/Prepaid'
|
||||
return new Prepaid(coursePrepaid)
|
||||
|
||||
becomeStudent: (options={}) ->
|
||||
options.url = '/db/user/-/become-student'
|
||||
|
|
|
@ -6,11 +6,13 @@ PrepaidSchema = c.object({title: 'Prepaid', required: ['creator', 'type']}, {
|
|||
c.object {required: ['date', 'userID']},
|
||||
date: c.date {title: 'Redeemed date'}
|
||||
userID: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ])
|
||||
maxRedeemers: { type: 'integer'}
|
||||
maxRedeemers: { type: 'integer' }
|
||||
code: c.shortString(title: "Unique code to redeem")
|
||||
type: { type: 'string' }
|
||||
properties: {type: 'object'}
|
||||
properties: {type: 'object' }
|
||||
exhausted: { type: 'boolean' }
|
||||
startDate: c.stringDate()
|
||||
endDate: c.stringDate()
|
||||
})
|
||||
|
||||
c.extendBasicProperties(PrepaidSchema, 'prepaid')
|
||||
|
|
|
@ -328,6 +328,16 @@ _.extend UserSchema.properties,
|
|||
coursePrepaidID: c.objectId({
|
||||
description: 'Prepaid which has paid for this user\'s course access'
|
||||
})
|
||||
coursePrepaid: {
|
||||
type: 'object'
|
||||
properties: {
|
||||
_id: c.objectId()
|
||||
startDate: c.stringDate()
|
||||
endDate: c.stringDate()
|
||||
}
|
||||
}
|
||||
enrollmentRequestSent: { type: 'boolean' }
|
||||
|
||||
schoolName: {type: 'string'}
|
||||
role: {type: 'string', enum: ["God", "advisor", "parent", "principal", "student", "superintendent", "teacher", "technology coordinator"]}
|
||||
birthday: c.stringDate({title: "Birthday"})
|
||||
|
|
3
app/styles/admin/administer-user-modal.sass
Normal file
3
app/styles/admin/administer-user-modal.sass
Normal file
|
@ -0,0 +1,3 @@
|
|||
#administer-user-modal
|
||||
.modal-dialog
|
||||
width: 90%
|
|
@ -1,4 +1,28 @@
|
|||
@import "app/styles/bootstrap/variables"
|
||||
|
||||
#enrollments-view
|
||||
|
||||
@media (min-width: $screen-md-min)
|
||||
#prepaids-col
|
||||
padding-right: 40px
|
||||
|
||||
#actions-col
|
||||
border-left: 1px solid gray // Will tend to be the longer one
|
||||
padding-left: 40px
|
||||
|
||||
.prepaid-card
|
||||
border-radius: 10px
|
||||
|
||||
p
|
||||
color: white
|
||||
|
||||
h1
|
||||
font-size: 68px
|
||||
|
||||
&.pending-prepaid-card
|
||||
background: #6e939f
|
||||
|
||||
|
||||
.how-to-enroll
|
||||
padding: 10px
|
||||
ol
|
||||
|
@ -8,11 +32,20 @@
|
|||
border-radius: 5px
|
||||
|
||||
#students-input
|
||||
width: 100px
|
||||
height: 50px
|
||||
line-height: 30px
|
||||
font-size: 30px
|
||||
width: 180px
|
||||
height: 80px
|
||||
font-size: 60px
|
||||
|
||||
&::-webkit-inner-spin-button, &::-webkit-outer-spin-button
|
||||
-webkit-appearance: none
|
||||
margin: 0
|
||||
|
||||
#enrollment-stats-table
|
||||
td, th
|
||||
border: none
|
||||
|
||||
.classroom-name-td
|
||||
padding-left: 20px
|
||||
|
||||
th
|
||||
padding-bottom: 10px
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
#purchase-courses-view
|
||||
|
||||
.enrollment-count
|
||||
font-size: 30px
|
||||
width: 120px
|
||||
|
||||
.not-enrolled
|
||||
line-height: 16px
|
||||
|
||||
.uppercase
|
||||
text-transform: uppercase
|
|
@ -280,3 +280,30 @@
|
|||
|
||||
.export-student-progress-btn
|
||||
margin-top: 10px
|
||||
|
||||
// Enrollment Status Tab
|
||||
|
||||
#search-form-group
|
||||
position: relative
|
||||
input
|
||||
width: auto
|
||||
.glyphicon
|
||||
color: $gray
|
||||
position: absolute
|
||||
top: 8px
|
||||
right: 5px
|
||||
|
||||
#enrollment-status-table
|
||||
// These column widths are just to keep the cells from resizing on search
|
||||
.checkbox-col
|
||||
width: 75px
|
||||
.student-info-col
|
||||
width: 240px
|
||||
.status-col
|
||||
width: 300px
|
||||
.enroll-col
|
||||
width: 140px
|
||||
.revoke-col
|
||||
width: 170px
|
||||
td
|
||||
vertical-align: middle
|
||||
|
|
|
@ -248,6 +248,11 @@ body[lang='ru'], body[lang='uk'], body[lang='bg'], body[lang^='mk'], body[lang='
|
|||
background-color: $burgandy
|
||||
color: white
|
||||
|
||||
.btn-burgandy-alt
|
||||
background-color: white
|
||||
border: 1px solid $burgandy
|
||||
color: $burgandy
|
||||
|
||||
.btn-lg
|
||||
font-size: 18px
|
||||
|
||||
|
|
3
app/styles/teachers/teachers-contact-modal.sass
Normal file
3
app/styles/teachers/teachers-contact-modal.sass
Normal file
|
@ -0,0 +1,3 @@
|
|||
#teachers-contact-modal
|
||||
textarea
|
||||
height: 200px
|
|
@ -1,6 +1,5 @@
|
|||
#test-view
|
||||
background-color: #eee
|
||||
margin: 0 20px
|
||||
padding: 0
|
||||
|
||||
#test-h2
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
extends /templates/core/modal-base-flat
|
||||
|
||||
// DNT
|
||||
|
||||
block modal-header-content
|
||||
h3 Administer User
|
||||
h4 #{user.get('name') || 'Unnamed'} / #{user.get('email')}
|
||||
span= user.id
|
||||
h4 #{view.user.get('name') || 'Unnamed'} / #{view.user.get('email')}
|
||||
span= view.user.id
|
||||
|
||||
|
||||
block modal-body-content
|
||||
|
@ -13,27 +15,48 @@ block modal-body-content
|
|||
.form-group
|
||||
.radio
|
||||
label
|
||||
input(type="radio" name="stripe-benefit" value="" checked=none)
|
||||
input(type="radio" name="stripe-benefit" value="" checked=view.none)
|
||||
| None
|
||||
.radio
|
||||
label
|
||||
input(type="radio" name="stripe-benefit" value="free" checked=free)
|
||||
input(type="radio" name="stripe-benefit" value="free" checked=view.free)
|
||||
| Free
|
||||
.radio
|
||||
label
|
||||
input(type="radio" name="stripe-benefit" value="free-until" checked=FreeUntil)
|
||||
input(type="radio" name="stripe-benefit" value="free-until" checked=view.freeUntil)
|
||||
| Free Until
|
||||
input.form-control.spl(type="date" name="stripe-free-until" value=freeUntilDate)#free-until-date
|
||||
input.form-control.spl(type="date" name="stripe-free-until" value=view.freeUntilDate)#free-until-date
|
||||
.radio
|
||||
label
|
||||
input(type="radio" name="stripe-benefit" value="coupon" checked=coupon)
|
||||
input(type="radio" name="stripe-benefit" value="coupon" checked=view.coupon)
|
||||
| Coupon
|
||||
select.form-control#coupon-select
|
||||
for couponOption in coupons
|
||||
option(value=couponOption.id selected=coupon===couponOption.id)= couponOption.format
|
||||
for coupon in view.coupons.models
|
||||
option(value=coupon.id selected=coupon.id===view.currentCouponID)= coupon.formatString()
|
||||
button#save-changes.btn.btn-primary Save Changes
|
||||
|
||||
h3 Grant Prepaid for Courses
|
||||
if view.prepaids.size()
|
||||
h3.m-t-3 Existing Prepaids
|
||||
table.table.table-condensed
|
||||
tr
|
||||
th ID
|
||||
th Type
|
||||
th Start
|
||||
th End
|
||||
th Used
|
||||
for prepaid in view.prepaids.models
|
||||
tr
|
||||
td= prepaid.id
|
||||
td= prepaid.get('type')
|
||||
td
|
||||
if prepaid.get('startDate')
|
||||
= moment(prepaid.get('startDate')).utc().format('lll')
|
||||
td
|
||||
if prepaid.get('endDate')
|
||||
= moment(prepaid.get('endDate')).utc().format('lll')
|
||||
td #{(prepaid.get('redeemers') || []).length} / #{prepaid.get('maxRedeemers') || 0}
|
||||
|
||||
h3.m-t-3 Grant Prepaid for Courses
|
||||
#prepaid-form.form
|
||||
if view.state === 'creating-prepaid'
|
||||
.progress.progress-striped.active
|
||||
|
@ -45,10 +68,16 @@ block modal-body-content
|
|||
else
|
||||
.form-group
|
||||
label Seats
|
||||
input#seats-input.form-control(type="number")
|
||||
input#seats-input.form-control(type="number", name="maxRedeemers")
|
||||
.form-group
|
||||
label Start Date
|
||||
input.form-control(type="date" name="startDate" value=moment().format('YYYY-MM-DD'))
|
||||
.form-group
|
||||
label End Date
|
||||
input.form-control(type="date" name="endDate" value=moment().add(1, 'year').format('YYYY-MM-DD')))
|
||||
.form-group
|
||||
button#add-seats-btn.btn.btn-primary Add Seats
|
||||
|
||||
block modal-footer-content
|
||||
block modal-footer
|
||||
|
||||
|
||||
|
|
|
@ -57,8 +57,8 @@ block content
|
|||
.tab-pane#tab_active_classes
|
||||
h3 Active Classes 90 days
|
||||
.small Active class: 12+ students in a classroom, with 6+ who played in last 30 days. Played == 'Started Level' analytics event.
|
||||
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
|
||||
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
|
||||
.small Paid student: user.coursePrepaid set and prepaid.properties.trialRequestID NOT set
|
||||
.small Trial student: user.coursePrepaid set and prepaid.properties.trialRequestID set
|
||||
.small Paid class: at least one paid student in the classroom
|
||||
.small Trial class: not paid, at least one trial student in classroom
|
||||
.small Free class: not paid, not trial
|
||||
|
@ -135,8 +135,8 @@ block content
|
|||
|
||||
.tab-pane#tab_classroom
|
||||
h3 Classroom Daily Active Users 90 days
|
||||
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
|
||||
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
|
||||
.small Paid student: user.coursePrepaid set and prepaid.properties.trialRequestID NOT set
|
||||
.small Trial student: user.coursePrepaid set and prepaid.properties.trialRequestID set
|
||||
.small Free student: not paid, not trial
|
||||
.classroom-daily-active-users-chart-90.line-chart-container
|
||||
|
||||
|
@ -159,8 +159,8 @@ block content
|
|||
.small Student: member of a course instance (assigned to course)
|
||||
.small For course instances != Single Player, hourOfCode != true
|
||||
.small Counts are not summed. I.e. a student or teacher only contributes to the count of one course
|
||||
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
|
||||
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
|
||||
.small Paid student: user.coursePrepaid set and prepaid.properties.trialRequestID NOT set
|
||||
.small Trial student: user.coursePrepaid set and prepaid.properties.trialRequestID set
|
||||
.small Free student: not paid, not trial
|
||||
.small Paid teacher: at least one paid student in course instance
|
||||
.small Trial teacher: at least one trial student in course instance, and no paid students
|
||||
|
|
|
@ -21,16 +21,16 @@ block modal-body-content
|
|||
span.spr :
|
||||
select.classroom-select
|
||||
each classroom in view.classrooms.models
|
||||
option(selected=(classroom.id === view.classroom.id), value=classroom.id)
|
||||
option(selected=(view.classroom ? classroom.id === view.classroom.id : false), value=classroom.id)
|
||||
= classroom.get('name')
|
||||
option(selected=(view.classroom.id === 'all-students'), value='all-students' data-i18n='teacher.all_students')
|
||||
option(selected=(!view.classroom), value='' data-i18n='teacher.all_students')
|
||||
|
||||
form.form.m-t-3
|
||||
span(data-i18n="teacher.enroll_the_following_students")
|
||||
span :
|
||||
.well.form-group
|
||||
- var enrolledUsers = view.users.filter(function(user){ return Boolean(user.get('coursePrepaidID')) })
|
||||
- var unenrolledUsers = view.users.filter(function(user){ return !Boolean(user.get('coursePrepaidID')) })
|
||||
- var enrolledUsers = view.users.filter(function(user){ return user.isEnrolled() })
|
||||
- var unenrolledUsers = view.users.filter(function(user){ return !user.isEnrolled() })
|
||||
for user in unenrolledUsers
|
||||
- var selected = Boolean(paid || state.get('selectedUsers').get(user.id))
|
||||
.checkbox
|
||||
|
|
|
@ -83,7 +83,7 @@ block content
|
|||
span.spr :
|
||||
span= playtime
|
||||
|
||||
- var paidFor = user.get('coursePrepaidID');
|
||||
- var paidFor = user.isEnrolled();
|
||||
for courseInstance in view.courseInstances.models
|
||||
- var inCourse = _.contains(courseInstance.get('members'), user.id);
|
||||
if !(inCourse || view.teacherMode)
|
||||
|
|
|
@ -31,79 +31,105 @@ block content
|
|||
a.btn.btn-primary.btn-lg(href="/teachers/update-account") Upgrade to teacher account
|
||||
|
||||
.container.m-t-5
|
||||
.pull-right
|
||||
span.glyphicon.glyphicon-question-sign
|
||||
=' '
|
||||
a#how-to-enroll-link(data-i18n="teacher.how_to_enroll")
|
||||
h3(data-i18n='teacher.enrollments')
|
||||
h4
|
||||
h4#enrollments-blurb
|
||||
span(data-i18n='teacher.enrollments_blurb_1')
|
||||
span 2–8
|
||||
span 2–#{view.state.get('totalCourses')}
|
||||
span(data-i18n='teacher.enrollments_blurb_2')
|
||||
|
||||
- var available = view.state.get('prepaidGroups').available
|
||||
- var pending = view.state.get('prepaidGroups').pending
|
||||
- var anyPrepaids = available || pending
|
||||
|
||||
.row.m-t-3
|
||||
.col-xs-4
|
||||
+enrollmentStats
|
||||
.col-xs-4
|
||||
+addCredits
|
||||
.col-xs-3.col-xs-offset-1
|
||||
+howToEnroll
|
||||
+quoteSection
|
||||
if anyPrepaids
|
||||
#prepaids-col.col-md-9
|
||||
if available
|
||||
h5.m-b-1(data-i18n="teacher.available_credits")
|
||||
.row
|
||||
for prepaid in available
|
||||
.col-sm-4.col-xs-6
|
||||
+prepaidCard(prepaid)
|
||||
|
||||
if pending
|
||||
h5.m-b-1.m-t-3(data-i18n="teacher.pending_credits")
|
||||
.row
|
||||
for prepaid in pending
|
||||
.col-sm-4.col-xs-6
|
||||
+prepaidCard(prepaid, 'pending-prepaid-card')
|
||||
|
||||
#actions-col.col-md-3
|
||||
+addCredits
|
||||
+enrollmentStats
|
||||
|
||||
else
|
||||
// no prepaids
|
||||
.col-sm-6.col-lg-4.col-lg-offset-2
|
||||
+addCredits
|
||||
.col-sm-6.col-lg-4
|
||||
+enrollmentStats
|
||||
|
||||
mixin prepaidCard(prepaid, className)
|
||||
.prepaid-card.bg-navy.text-center.m-b-2.p-a-2(class=className)
|
||||
h1.m-t-2.m-b-0= prepaid.openSpots()
|
||||
div(data-i18n="teacher.credits")
|
||||
hr
|
||||
em.small-details
|
||||
.pull-left(data-i18n="teacher.start_date")
|
||||
.pull-right= moment(prepaid.get('startDate')).utc().format('l')
|
||||
.clearfix
|
||||
|
||||
.pull-left(data-i18n="teacher.end_date")
|
||||
.pull-right= moment(prepaid.get('endDate')).utc().format('l')
|
||||
.clearfix
|
||||
|
||||
|
||||
mixin addCredits
|
||||
.text-center.m-b-3.m-t-3
|
||||
h5(data-i18n="courses.get_enrollments")
|
||||
if me.get('enrollmentRequestSent')
|
||||
#enrollment-request-sent-blurb.small
|
||||
p(data-i18n="teacher.enroll_request_sent_blurb1")
|
||||
p(data-i18n="teacher.enroll_request_sent_blurb2")
|
||||
p(data-i18n="[html]teacher.enroll_request_sent_blurb3")
|
||||
button#request-sent-btn.btn-lg.btn.btn-forest(disabled=true, data-i18n="teacher.request_sent")
|
||||
else
|
||||
p(data-i18n="teacher.num_enrollments_needed")
|
||||
div.m-t-2
|
||||
input#students-input.enrollment-count.text-center(value=view.state.get('numberOfStudents') type='number')
|
||||
strong(data-i18n="teacher.credits")
|
||||
p.m-y-2(data-i18n="teacher.get_enrollments_blurb")
|
||||
button#contact-us-btn.btn-lg.btn.btn-forest(data-i18n="contribute.contact_us_url")
|
||||
|
||||
|
||||
mixin enrollmentStats
|
||||
h5
|
||||
span(data-i18n='teacher.credits_available')
|
||||
span.spr :
|
||||
= view.prepaids.totalAvailable()
|
||||
.small-details
|
||||
span(data-i18n='teacher.total_unique_students')
|
||||
span.spr :
|
||||
= view.totalEnrolled + view.totalNotEnrolled
|
||||
.small-details
|
||||
span(data-i18n='teacher.total_enrolled_students')
|
||||
span.spr :
|
||||
= view.totalEnrolled
|
||||
|
||||
h5.small-details.m-t-3
|
||||
span(data-i18n='teacher.unenrolled_students')
|
||||
each classroom in view.classrooms.models
|
||||
if classroom.get('members').length > 0 && view.classroomNotEnrolledMap && view.classroomNotEnrolledMap[classroom.id] > 0
|
||||
.small-details
|
||||
span= classroom.get('name')
|
||||
span.spr :
|
||||
span= view.classroomNotEnrolledMap[classroom.id]
|
||||
h5.text-center.m-t-3.m-b-2(data-i18n='teacher.enrollment_status')
|
||||
table#enrollment-stats-table.table-condensed.table.small-details
|
||||
tr
|
||||
td
|
||||
span(data-i18n='teacher.total_unique_students')
|
||||
span.spr :
|
||||
td= view.state.get('totalEnrolled') + view.state.get('totalNotEnrolled')
|
||||
tr
|
||||
td
|
||||
span(data-i18n='teacher.total_enrolled_students')
|
||||
span.spr :
|
||||
td= view.state.get('totalEnrolled')
|
||||
tr
|
||||
th(data-i18n='teacher.unenrolled_students')
|
||||
th= view.state.get('totalNotEnrolled')
|
||||
each classroom in view.classrooms.models
|
||||
if classroom.get('members').length > 0 && view.state.get('classroomNotEnrolledMap')[classroom.id] > 0
|
||||
tr
|
||||
td.classroom-name-td
|
||||
span= classroom.get('name')
|
||||
span.spr :
|
||||
td= view.state.get('classroomNotEnrolledMap')[classroom.id]
|
||||
|
||||
.small-details
|
||||
span(data-i18n='teacher.total_unenrolled')
|
||||
span.spr :
|
||||
= view.totalNotEnrolled
|
||||
|
||||
//- .enroll-students.btn.btn-lg.btn-navy
|
||||
//- span(data-i18n='teacher.enroll_students')
|
||||
|
||||
mixin addCredits
|
||||
.text-center
|
||||
h5(data-i18n='teacher.add_enrollment_credits')
|
||||
div.m-t-1
|
||||
input#students-input.text-center.enrollment-count(value=view.numberOfStudents type='number')
|
||||
div.m-t-1
|
||||
if view.state === 'purchasing'
|
||||
.purchase-now.btn.btn-lg.btn-forest.disabled
|
||||
span(data-i18n='teacher.purchasing')
|
||||
else if view.state === 'purchased'
|
||||
.purchase-now.btn.btn-lg.btn-forest
|
||||
span(data-i18n='teacher.purchased')
|
||||
else
|
||||
.purchase-now.btn.btn-lg.btn-forest
|
||||
span(data-i18n='teacher.purchase_now')
|
||||
|
||||
mixin howToEnroll
|
||||
.how-to-enroll.small-details
|
||||
.text-center
|
||||
b(data-i18n='teacher.how_to_enroll')
|
||||
ol
|
||||
li.m-t-1(data-i18n='teacher.how_to_enroll_blurb_1')
|
||||
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_2')
|
||||
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_3')
|
||||
|
||||
mixin quoteSection
|
||||
.text-center.m-t-5
|
||||
h4(data-i18n='teacher.bulk_pricing_blurb')
|
||||
a.request-quote.btn.btn-lg.btn-navy.m-t-2(href='/teachers/demo')
|
||||
span(data-i18n='teachers_quote.title')
|
||||
button#enroll-students-btn.btn.btn-lg.btn-navy
|
||||
span(data-i18n='teacher.enroll_students')
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
|
||||
if view.state === 'purchasing'
|
||||
p.text-center(data-i18n="buy_gems.purchasing")
|
||||
.progress.progress-striped.active
|
||||
.progress-bar(style="width: 100%")
|
||||
|
||||
else if view.state === 'purchased'
|
||||
p
|
||||
span.spr(data-i18n="courses.thank_you_pref")
|
||||
span= view.numberOfStudents
|
||||
span.spl(data-i18n="courses.thank_you_suff")
|
||||
|
||||
p.text-center
|
||||
if view.fromClassroom
|
||||
a(href="/courses/"+view.fromClassroom, data-i18n="courses.return_to_class")
|
||||
else
|
||||
a(href="/teachers/classes", data-i18n="courses.return_to_course_man")
|
||||
|
||||
else
|
||||
|
||||
h2.text-center(data-i18n="courses.purchase_enrollments")
|
||||
br
|
||||
|
||||
if view.state === 'error'
|
||||
.alert.alert-danger= view.stateMessage
|
||||
|
||||
- var usedEnrollments = view.prepaids.totalRedeemers();
|
||||
- var totalEnrollments = view.prepaids.totalMaxRedeemers();
|
||||
- var remainingEnrollments = totalEnrollments - usedEnrollments;
|
||||
.row
|
||||
.col-md-4
|
||||
.col-md-3
|
||||
strong.uppercase(data-i18n="courses.you_have2")
|
||||
.col-md-1
|
||||
strong= remainingEnrollments
|
||||
br
|
||||
.row
|
||||
.col-md-4
|
||||
.col-md-3
|
||||
strong.uppercase(data-i18n="courses.students_not_enrolled")
|
||||
.row
|
||||
.col-md-4
|
||||
.col-md-3
|
||||
each classroom in view.classrooms.models
|
||||
if classroom.get('members').length > 0
|
||||
.not-enrolled= classroom.get('name')
|
||||
.not-enrolled(data-i18n="courses.total_all_classes")
|
||||
.col-md-1
|
||||
- var totalNotEnrolled = 0
|
||||
each classroom in view.classrooms.models
|
||||
if classroom.get('members').length > 0 && view.classroomNotEnrolledMap
|
||||
.not-enrolled
|
||||
strong= view.classroomNotEnrolledMap[classroom.id]
|
||||
.not-enrolled
|
||||
strong= view.totalNotEnrolled
|
||||
|
||||
br
|
||||
br
|
||||
|
||||
p.text-center
|
||||
strong(data-i18n="courses.how_many_enrollments")
|
||||
br
|
||||
|
||||
p.text-center
|
||||
input#students-input.text-center.enrollment-count(
|
||||
value= view.numberOfStudents
|
||||
type='number'
|
||||
)
|
||||
br
|
||||
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-offset-3.col-md-6
|
||||
span(data-i18n="courses.each_student_access")
|
||||
br
|
||||
|
||||
p.text-center#price-form-group
|
||||
if view.numberOfStudentsIsValid()
|
||||
strong
|
||||
span(data-i18n="account_prepaid.purchase_total")
|
||||
span.spr : #{view.numberOfStudents}
|
||||
span(data-i18n="courses.enrollments")
|
||||
span.spl x $#{(view.pricePerStudent/100).toFixed(2)} = #{view.getPriceString()}
|
||||
else
|
||||
strong Invalid number of students
|
||||
|
||||
p.text-center
|
||||
button#purchase-btn.btn.btn-lg.btn-success.uppercase(data-i18n="courses.purchase_now" disabled=me.isAnonymous())
|
||||
|
||||
if me.isAnonymous()
|
||||
// DNT. Temporary redirect until teacher-dashboard is finished
|
||||
.alert.alert-danger.text-center
|
||||
h2 You must be signed up to purchase enrollments.
|
||||
p
|
||||
a.btn.btn-primary.btn-lg(href="/teachers/signup") Create a teacher account
|
|
@ -1,24 +1,24 @@
|
|||
extends /templates/core/modal-base
|
||||
extends /templates/core/modal-base-flat
|
||||
|
||||
block modal-header-content
|
||||
.text-center
|
||||
h3.modal-title(data-i18n="courses.remove_student1")
|
||||
h1.modal-title(data-i18n="courses.remove_student1")
|
||||
span.glyphicon.glyphicon-warning-sign.text-danger
|
||||
h3(data-i18n="courses.are_you_sure")
|
||||
h2(data-i18n="courses.are_you_sure")
|
||||
|
||||
block modal-body-content
|
||||
p.text-center
|
||||
span(data-i18n="courses.remove_description1")
|
||||
if view.user.get('coursePrepaidID')
|
||||
if view.user.isEnrolled()
|
||||
span(data-i18n="courses.remove_description2")
|
||||
|
||||
block modal-footer-content
|
||||
#remove-student-buttons.text-center
|
||||
p
|
||||
button.btn.btn-lg.btn-success.text-uppercase(data-dismiss="modal", data-i18n="courses.keep_student")
|
||||
button.btn.btn-lg.btn-forest.text-uppercase(data-dismiss="modal", data-i18n="courses.keep_student")
|
||||
p - OR -
|
||||
p
|
||||
button#remove-student-btn.btn.btn-lg.btn-default.text-uppercase(data-i18n="courses.remove_student1")
|
||||
button#remove-student-btn.btn.btn-lg.btn-burgandy.text-uppercase(data-i18n="courses.remove_student1")
|
||||
|
||||
#remove-student-progress.text-center.hide
|
||||
.progress
|
||||
|
|
|
@ -108,21 +108,28 @@ block content
|
|||
+copyCodes
|
||||
+addStudentsButton
|
||||
|
||||
ul#student-info-tabs.nav.nav-tabs.m-t-5(role='tablist')
|
||||
li(class=(state.get('activeTab')==="#students-tab" ? 'active' : ''))
|
||||
ul.nav.nav-tabs.m-t-5(role='tablist')
|
||||
- var activeTab = state.get('activeTab');
|
||||
li(class=(activeTab === "#students-tab" ? 'active' : ''))
|
||||
a.students-tab-btn(href='#students-tab')
|
||||
.small-details.text-center(data-i18n='teacher.students')
|
||||
.tab-spacer
|
||||
li(class=(state.get('activeTab')==="#course-progress-tab" ? 'active' : ''))
|
||||
li(class=(activeTab === "#course-progress-tab" ? 'active' : ''))
|
||||
a.course-progress-tab-btn(href='#course-progress-tab')
|
||||
.small-details.text-center(data-i18n='teacher.course_progress')
|
||||
.tab-spacer
|
||||
li(class=(activeTab === "#enrollment-status-tab" ? 'active' : ''))
|
||||
a.course-progress-tab-btn(href='#enrollment-status-tab')
|
||||
.small-details.text-center(data-i18n='teacher.enrollment_status')
|
||||
.tab-filler
|
||||
|
||||
.tab-content
|
||||
if state.get('activeTab')=='#students-tab'
|
||||
if activeTab === '#students-tab'
|
||||
+studentsTab
|
||||
else
|
||||
else if activeTab === '#course-progress-tab'
|
||||
+courseProgressTab
|
||||
else
|
||||
+enrollmentStatusTab
|
||||
|
||||
mixin breadcrumbs
|
||||
.breadcrumbs
|
||||
|
@ -179,8 +186,8 @@ mixin sortButtons
|
|||
.sort-buttons.small
|
||||
span(data-i18n='teacher.sort_by')
|
||||
span.spr :
|
||||
button.sort-button.sort-by-name(data-i18n='general.name')
|
||||
button.sort-button.sort-by-progress(data-i18n='teacher.progress')
|
||||
button.sort-button.sort-by-name(data-i18n='general.name', value='name')
|
||||
button.sort-button.sort-by-progress(data-i18n='teacher.progress', value='progress')
|
||||
|
||||
mixin studentRow(student)
|
||||
tr.student-row.alternating-background
|
||||
|
@ -369,3 +376,46 @@ mixin bulkAssignControls
|
|||
span(data-i18n='teacher.assign_to_selected_students')
|
||||
button.btn.btn-primary-alt.enroll-selected-students
|
||||
span(data-i18n='teacher.enroll_selected_students')
|
||||
|
||||
mixin enrollmentStatusTab
|
||||
form.form-inline.text-center.m-t-3
|
||||
#search-form-group.form-group
|
||||
label(for="student-search") Search for student:
|
||||
input#student-search.form-control.m-l-1(type="search")
|
||||
span.glyphicon.glyphicon-search.form-control-feedback
|
||||
|
||||
table.table#enrollment-status-table.table-condensed
|
||||
thead
|
||||
//th.checkbox-col.select-all
|
||||
.checkbox-flat
|
||||
input(type='checkbox' id='checkbox-all-students')
|
||||
label.checkmark(for='checkbox-all-students')
|
||||
th
|
||||
.sort-buttons.small
|
||||
span(data-i18n='teacher.sort_by')
|
||||
span.spr :
|
||||
button.sort-button.sort-by-name(data-i18n='general.name', value='name')
|
||||
button.sort-button.sort-by-status(data-i18n='user.status', value='status')
|
||||
tbody
|
||||
- var searchTerm = view.state.get('searchTerm');
|
||||
each student in state.get('students').search(searchTerm)
|
||||
- var status = student.prepaidStatus()
|
||||
tr.student-row.alternating-background
|
||||
//td.checkbox-col.student-checkbox
|
||||
.checkbox-flat
|
||||
input(type='checkbox' id='checkbox-student-' + student.id, data-student-id=student.id)
|
||||
label.checkmark(for='checkbox-student-' + student.id)
|
||||
td.student-info-col
|
||||
.student-info
|
||||
div.student-name= student.broadName()
|
||||
div.student-email.small-details= student.get('email')
|
||||
td.status-col
|
||||
span(data-i18n='user.status')
|
||||
span.spr :
|
||||
strong(class= status === 'expired' ? 'text-danger' : '')= view.studentStatusString(student)
|
||||
td.enroll-col
|
||||
if status !== 'enrolled'
|
||||
button.enroll-student-button.btn.btn-navy(data-i18n="teacher.enroll_student", data-user-id=student.id)
|
||||
td.revoke-col
|
||||
if status === 'enrolled'
|
||||
button.revoke-student-button.btn.btn-burgandy-alt(data-i18n="teacher.revoke_enrollment", data-user-id=student.id)
|
||||
|
|
12
app/templates/teachers/how-to-enroll-modal.jade
Normal file
12
app/templates/teachers/how-to-enroll-modal.jade
Normal file
|
@ -0,0 +1,12 @@
|
|||
extends /templates/core/modal-base-flat
|
||||
|
||||
block modal-header-content
|
||||
.text-center
|
||||
h3(data-i18n='teacher.how_to_enroll')
|
||||
|
||||
block modal-body
|
||||
|
||||
ol
|
||||
li.m-t-1(data-i18n='teacher.how_to_enroll_blurb_1')
|
||||
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_2')
|
||||
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_3')
|
42
app/templates/teachers/teachers-contact-modal.jade
Normal file
42
app/templates/teachers/teachers-contact-modal.jade
Normal file
|
@ -0,0 +1,42 @@
|
|||
extends /templates/core/modal-base-flat
|
||||
|
||||
block modal-header-content
|
||||
.text-center
|
||||
h3 Contact Our Classroom Team
|
||||
|
||||
block modal-body-content
|
||||
p Send us a message and our classroom success team will be in touch to help find the best solution for your students' needs!
|
||||
form
|
||||
- var sending = view.state.get('sendingState') === 'sending';
|
||||
- var values = view.state.get('formValues');
|
||||
- var errors = view.state.get('formErrors');
|
||||
|
||||
.form-group(class=errors.email ? 'has-error' : '')
|
||||
label.control-label(for="email" data-i18n="general.email")
|
||||
+formErrors(errors.email)
|
||||
input.form-control(name="email", type="email", value=values.email || '', tabindex=1, disabled=sending)
|
||||
|
||||
.form-group(class=errors.message ? 'has-error' : '')
|
||||
label.control-label(for="message" data-i18n="general.message")
|
||||
+formErrors(errors.message)
|
||||
textarea.form-control(name="message", tabindex=1 disabled=sending)= values.message
|
||||
|
||||
if view.state.get('sendingState') === 'error'
|
||||
.alert.alert-danger Could not send message.
|
||||
|
||||
if view.state.get('sendingState') === 'sent'
|
||||
.alert.alert-success Message sent!
|
||||
|
||||
.text-right
|
||||
- var sent = view.state.get('sendingState') === 'sent';
|
||||
button#submit-btn.btn.btn-navy.btn-lg(type='submit' disabled=sending || sent) Submit
|
||||
|
||||
block modal-footer
|
||||
|
||||
mixin formErrors(errors)
|
||||
if _.isString(errors)
|
||||
- errors = [errors]
|
||||
if _.size(errors)
|
||||
.help-block
|
||||
for error in errors
|
||||
div= error
|
|
@ -1,3 +1,5 @@
|
|||
#demo-area
|
||||
|
||||
h2#test-h2 Testing Page
|
||||
|
||||
ol.breadcrumb
|
||||
|
@ -10,7 +12,6 @@ ol.breadcrumb
|
|||
.row
|
||||
.col-md-8
|
||||
#test-wrapper.well
|
||||
#demo-area
|
||||
#testing-area
|
||||
|
||||
.col-md-4.hidden-sm.hidden-xs
|
||||
|
@ -31,4 +32,4 @@ ol.breadcrumb
|
|||
span.spl= child.name
|
||||
if child.type == 'folder'
|
||||
strong (#{child.size})
|
||||
|
||||
|
||||
|
|
|
@ -60,14 +60,10 @@ module.exports = TestView = class TestView extends RootView
|
|||
@specFiles = (f for f in @specFiles when _.string.startsWith f, prefix)
|
||||
|
||||
@runTests: (specFiles, demosOn=false) ->
|
||||
application.testing = true
|
||||
specFiles ?= @getAllSpecFiles()
|
||||
if demosOn
|
||||
jasmine.demoEl = ($el) ->
|
||||
$el.css({
|
||||
'border': '2px solid black'
|
||||
'background': 'white'
|
||||
'padding': '20px'
|
||||
})
|
||||
$('#demo-area').append($el)
|
||||
jasmine.demoModal = _.once (modal) ->
|
||||
currentView.openModalView(modal)
|
||||
|
|
|
@ -2,47 +2,37 @@ ModalView = require 'views/core/ModalView'
|
|||
template = require 'templates/admin/administer-user-modal'
|
||||
User = require 'models/User'
|
||||
Prepaid = require 'models/Prepaid'
|
||||
StripeCoupons = require 'collections/StripeCoupons'
|
||||
forms = require 'core/forms'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
|
||||
module.exports = class AdministerUserModal extends ModalView
|
||||
id: "administer-user-modal"
|
||||
id: 'administer-user-modal'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click #save-changes': 'onSaveChanges'
|
||||
'click #save-changes': 'onClickSaveChanges'
|
||||
'click #add-seats-btn': 'onClickAddSeatsButton'
|
||||
|
||||
constructor: (options, @userHandle) ->
|
||||
super(options)
|
||||
@user = @supermodel.loadModel(new User({_id:@userHandle}), {cache: false}).model
|
||||
options = {cache: false, url: '/stripe/coupons'}
|
||||
options.success = (@coupons) =>
|
||||
@couponsResource = @supermodel.addRequestResource('coupon', options)
|
||||
@couponsResource.load()
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
initialize: (options, @userHandle) ->
|
||||
@user = new User({_id:@userHandle})
|
||||
@supermodel.trackRequest @user.fetch({cache: false})
|
||||
@coupons = new StripeCoupons()
|
||||
@supermodel.trackRequest @coupons.fetch({cache: false})
|
||||
@prepaids = new Prepaids()
|
||||
@supermodel.trackRequest @prepaids.fetchByCreator(@userHandle)
|
||||
|
||||
onLoaded: ->
|
||||
# TODO: Figure out a better way to expose this info, perhaps User methods?
|
||||
stripe = @user.get('stripe') or {}
|
||||
c.free = stripe.free is true
|
||||
c.freeUntil = _.isString(stripe.free)
|
||||
c.freeUntilDate = if c.freeUntil then stripe.free else new Date().toISOString()[...10]
|
||||
c.coupon = stripe.couponID
|
||||
c.coupons = @coupons or []
|
||||
for coupon in c.coupons
|
||||
bits = [coupon.id]
|
||||
if coupon.percent_off
|
||||
bits.push "(#{coupon.percent_off}% off)"
|
||||
else if coupon.amount_off
|
||||
bits.push "($#{coupon.amount_off} off)"
|
||||
if coupon.duration
|
||||
bits.push "(duration: #{coupon.duration})"
|
||||
if coupon.redeem_by
|
||||
bits.push "(redeem by: #{moment(coupon.redeem_by).format('lll')}"
|
||||
coupon.format = bits.join(' ')
|
||||
c.none = not (c.free or c.freeUntil or c.coupon)
|
||||
c.user = @user
|
||||
c
|
||||
|
||||
onSaveChanges: ->
|
||||
@free = stripe.free is true
|
||||
@freeUntil = _.isString(stripe.free)
|
||||
@freeUntilDate = if @freeUntil then stripe.free else new Date().toISOString()[...10]
|
||||
@currentCouponID = stripe.couponID
|
||||
@none = not (@free or @freeUntil or @coupon)
|
||||
super()
|
||||
|
||||
onClickSaveChanges: ->
|
||||
stripe = _.clone(@user.get('stripe') or {})
|
||||
delete stripe.free
|
||||
delete stripe.couponID
|
||||
|
@ -61,15 +51,20 @@ module.exports = class AdministerUserModal extends ModalView
|
|||
@user.patch(options)
|
||||
|
||||
onClickAddSeatsButton: ->
|
||||
maxRedeemers = parseInt(@$('#seats-input').val())
|
||||
return unless maxRedeemers and maxRedeemers > 0
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: maxRedeemers
|
||||
attrs = forms.formToObject(@$('#prepaid-form'))
|
||||
attrs.maxRedeemers = parseInt(attrs.maxRedeemers)
|
||||
return unless _.all(_.values(attrs))
|
||||
return unless attrs.maxRedeemers > 0
|
||||
return unless attrs.endDate and attrs.startDate and attrs.endDate > attrs.startDate
|
||||
attrs.startDate = new Date(attrs.startDate).toISOString()
|
||||
attrs.endDate = new Date(attrs.endDate).toISOString()
|
||||
_.extend(attrs, {
|
||||
type: 'course'
|
||||
creator: @user.id
|
||||
properties:
|
||||
adminAdded: me.id
|
||||
})
|
||||
prepaid = new Prepaid(attrs)
|
||||
prepaid.save()
|
||||
@state = 'creating-prepaid'
|
||||
@renderSelectors('#prepaid-form')
|
||||
|
|
|
@ -290,7 +290,7 @@ module.exports = class AnalyticsView extends RootView
|
|||
prepaidUserMap = {}
|
||||
for user in data.students
|
||||
continue unless studentPaidStatusMap[user._id]
|
||||
if prepaidID = user.coursePrepaidID
|
||||
if prepaidID = user.coursePrepaidID or user.course.coursePrepaid?._id # TODO: make sure this works for coursePrepaid
|
||||
studentPaidStatusMap[user._id] = 'paid'
|
||||
prepaidUserMap[prepaidID] ?= []
|
||||
prepaidUserMap[prepaidID].push(user._id)
|
||||
|
|
|
@ -18,7 +18,8 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
'submit form': 'onSubmitForm'
|
||||
|
||||
getInitialState: (options) ->
|
||||
selectedUserModels = _.filter(options.selectedUsers.models, (user) -> not user.isEnrolled())
|
||||
selectedUsers = options.selectedUsers or options.users
|
||||
selectedUserModels = _.filter(selectedUsers.models, (user) -> not user.isEnrolled())
|
||||
{
|
||||
selectedUsers: new Users(selectedUserModels)
|
||||
visibleSelectedUsers: new Users(selectedUserModels)
|
||||
|
@ -31,11 +32,10 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
@users = options.users.clone()
|
||||
@users.comparator = (user) -> user.broadName().toLowerCase()
|
||||
@prepaids = new Prepaids()
|
||||
@prepaids.comparator = '_id'
|
||||
@prepaids.fetchByCreator(me.id)
|
||||
@supermodel.trackCollection(@prepaids)
|
||||
@prepaids.comparator = 'endDate' # use prepaids in order of expiration
|
||||
@supermodel.trackRequest @prepaids.fetchByCreator(me.id)
|
||||
@classrooms = new Classrooms()
|
||||
@classrooms.fetchMine({
|
||||
@supermodel.trackRequest @classrooms.fetchMine({
|
||||
data: {archived: false}
|
||||
success: =>
|
||||
@classrooms.each (classroom) =>
|
||||
|
@ -43,7 +43,6 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
jqxhrs = classroom.users.fetchForClassroom(classroom, { removeDeleted: true })
|
||||
@supermodel.trackRequests(jqxhrs)
|
||||
})
|
||||
@supermodel.trackCollection(@classrooms)
|
||||
|
||||
@listenTo @state, 'change', @render
|
||||
@listenTo @state.get('selectedUsers'), 'change add remove reset', ->
|
||||
|
@ -56,6 +55,10 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
@state.set {
|
||||
unusedEnrollments: @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers()
|
||||
}
|
||||
|
||||
onLoaded: ->
|
||||
@prepaids.reset(@prepaids.filter((prepaid) -> prepaid.status() is 'available'))
|
||||
super()
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
|
@ -73,8 +76,7 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
replaceStudentList: (e) ->
|
||||
selectedClassroomID = $(e.currentTarget).val()
|
||||
@classroom = @classrooms.get(selectedClassroomID)
|
||||
if selectedClassroomID is 'all-students'
|
||||
@classroom = new Classroom({ _id: 'all-students', name: 'All Students' }) # TODO: This is a horrible hack so the select shows the right option!
|
||||
if not @classroom
|
||||
users = _.uniq _.flatten @classrooms.map (classroom) -> classroom.users.models
|
||||
@users.reset(users)
|
||||
@users.sort()
|
||||
|
@ -96,26 +98,17 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
return
|
||||
|
||||
user = usersToRedeem.first()
|
||||
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots() > 0)
|
||||
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots() > 0) unless prepaid
|
||||
$.ajax({
|
||||
method: 'POST'
|
||||
url: _.result(prepaid, 'url') + '/redeemers'
|
||||
data: { userID: user.id }
|
||||
context: @
|
||||
success: (prepaid) ->
|
||||
user.set('coursePrepaidID', prepaid._id)
|
||||
prepaid = @prepaids.find((prepaid) -> prepaid.status() is 'available')
|
||||
prepaid.redeem(user, {
|
||||
success: (prepaid) =>
|
||||
user.set('coursePrepaid', prepaid.pick('_id', 'startDate', 'endDate'))
|
||||
usersToRedeem.remove(user)
|
||||
# pct = 100 * (usersToRedeem.originalSize - usersToRedeem.size() / usersToRedeem.originalSize)
|
||||
# @$('#progress-area .progress-bar').css('width', "#{pct.toFixed(1)}%")
|
||||
application.tracker?.trackEvent 'Enroll modal finished enroll student', category: 'Courses', userID: user.id
|
||||
@redeemUsers(usersToRedeem)
|
||||
error: (jqxhr, textStatus, errorThrown) ->
|
||||
if jqxhr.status is 402
|
||||
message = arguments[2]
|
||||
else
|
||||
message = "#{jqxhr.status}: #{jqxhr.responseText}"
|
||||
@state.set { error: message } # TODO: Test this! ("should" never happen. Only on server responding with an error.)
|
||||
error: (prepaid, jqxhr) =>
|
||||
@state.set { error: jqxhr.responseJSON.message }
|
||||
})
|
||||
|
||||
finishRedeemUsers: ->
|
||||
|
|
|
@ -118,8 +118,7 @@ module.exports = class ClassroomView extends RootView
|
|||
userID = $(e.target).closest('.btn').data('user-id')
|
||||
if @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() > 0
|
||||
# Have an unused enrollment, enroll student immediately instead of opening the enroll modal
|
||||
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots() > 0)
|
||||
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots() > 0) unless prepaid
|
||||
prepaid = @prepaids.find((prepaid) -> prepaid.status() is 'available')
|
||||
$.ajax({
|
||||
method: 'POST'
|
||||
url: _.result(prepaid, 'url') + '/redeemers'
|
||||
|
@ -182,7 +181,7 @@ module.exports = class ClassroomView extends RootView
|
|||
stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A' # '
|
||||
stats.totalLevelsComplete = _.size(completeSessions)
|
||||
|
||||
enrolledUsers = @users.filter (user) -> user.get('coursePrepaidID')
|
||||
enrolledUsers = @users.filter (user) -> user.isEnrolled()
|
||||
stats.enrolledUsers = _.size(enrolledUsers)
|
||||
return stats
|
||||
|
||||
|
|
|
@ -1,90 +1,79 @@
|
|||
app = require 'core/application'
|
||||
CreateAccountModal = require 'views/core/CreateAccountModal'
|
||||
Classroom = require 'models/Classroom'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Course = require 'models/Course'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
RootView = require 'views/core/RootView'
|
||||
stripeHandler = require 'core/services/stripe'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
State = require 'models/State'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
template = require 'templates/courses/enrollments-view'
|
||||
User = require 'models/User'
|
||||
Users = require 'collections/Users'
|
||||
utils = require 'core/utils'
|
||||
Products = require 'collections/Products'
|
||||
Courses = require 'collections/Courses'
|
||||
HowToEnrollModal = require 'views/teachers/HowToEnrollModal'
|
||||
TeachersContactModal = require 'views/teachers/TeachersContactModal'
|
||||
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
||||
|
||||
module.exports = class EnrollmentsView extends RootView
|
||||
id: 'enrollments-view'
|
||||
template: template
|
||||
numberOfStudents: 15
|
||||
pricePerStudent: 0
|
||||
|
||||
initialize: (options) ->
|
||||
@ownedClassrooms = new Classrooms()
|
||||
@ownedClassrooms.fetchMine({data: {project: '_id'}})
|
||||
@supermodel.trackCollection(@ownedClassrooms)
|
||||
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
|
||||
@fromClassroom = utils.getQueryVariable('from-classroom')
|
||||
@members = new Users()
|
||||
# @listenTo @members, 'sync add remove', @calculateEnrollmentStats
|
||||
@classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom })
|
||||
@classrooms.comparator = '_id'
|
||||
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
|
||||
@supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}})
|
||||
@prepaids = new Prepaids()
|
||||
@prepaids.comparator = '_id'
|
||||
@prepaids.fetchByCreator(me.id)
|
||||
@supermodel.loadCollection(@prepaids, 'prepaids')
|
||||
@products = new Products()
|
||||
@supermodel.loadCollection(@products, 'products')
|
||||
super(options)
|
||||
|
||||
events:
|
||||
'input #students-input': 'onInputStudentsInput'
|
||||
'click .purchase-now': 'onClickPurchaseButton'
|
||||
# 'click .enroll-students': 'onClickEnrollStudents'
|
||||
'click #enroll-students-btn': 'onClickEnrollStudentsButton'
|
||||
'click #how-to-enroll-link': 'onClickHowToEnrollLink'
|
||||
'click #contact-us-btn': 'onClickContactUsButton'
|
||||
|
||||
onLoaded: ->
|
||||
@calculateEnrollmentStats()
|
||||
@pricePerStudent = @products.findWhere({name: 'course'}).get('amount')
|
||||
super()
|
||||
initialize: ->
|
||||
@state = new State({
|
||||
totalEnrolled: 0
|
||||
totalNotEnrolled: 0
|
||||
classroomNotEnrolledMap: {}
|
||||
classroomEnrolledMap: {}
|
||||
numberOfStudents: 15
|
||||
totalCourses: 0
|
||||
prepaidGroups: {
|
||||
'available': []
|
||||
'pending': []
|
||||
}
|
||||
})
|
||||
|
||||
getPriceString: -> '$' + (@getPrice()/100).toFixed(2)
|
||||
getPrice: -> @pricePerStudent * @numberOfStudents
|
||||
@courses = new Courses()
|
||||
@supermodel.trackRequest @courses.fetch({data: { project: 'free' }})
|
||||
@members = new Users()
|
||||
@classrooms = new Classrooms()
|
||||
@classrooms.comparator = '_id'
|
||||
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
|
||||
@supermodel.trackRequest @classrooms.fetchMine()
|
||||
@prepaids = new Prepaids()
|
||||
@prepaids.comparator = '_id'
|
||||
@supermodel.trackRequest @prepaids.fetchByCreator(me.id)
|
||||
@debouncedRender = _.debounce @render, 0
|
||||
@listenTo @prepaids, 'all', -> @state.set('prepaidGroups', @prepaids.groupBy((p) -> p.status()))
|
||||
@listenTo(@state, 'all', @debouncedRender)
|
||||
|
||||
onceClassroomsSync: ->
|
||||
for classroom in @classrooms.models
|
||||
@supermodel.trackRequests @members.fetchForClassroom(classroom, {remove: false, removeDeleted: true})
|
||||
|
||||
onLoaded: ->
|
||||
@calculateEnrollmentStats()
|
||||
@state.set('totalCourses', @courses.size())
|
||||
super()
|
||||
|
||||
calculateEnrollmentStats: ->
|
||||
@removeDeletedStudents()
|
||||
@memberEnrolledMap = {}
|
||||
for user in @members.models
|
||||
@memberEnrolledMap[user.id] = user.get('coursePrepaidID')?
|
||||
|
||||
@totalEnrolled = _.reduce @members.models, ((sum, user) ->
|
||||
sum + (if user.get('coursePrepaidID') then 1 else 0)
|
||||
), 0
|
||||
|
||||
@numberOfStudents = @totalNotEnrolled = _.reduce @members.models, ((sum, user) ->
|
||||
sum + (if not user.get('coursePrepaidID') then 1 else 0)
|
||||
), 0
|
||||
# sort users into enrolled, not enrolled
|
||||
groups = @members.groupBy (m) -> m.isEnrolled()
|
||||
enrolledUsers = new Users(groups.true)
|
||||
@notEnrolledUsers = new Users(groups.false)
|
||||
|
||||
map = {}
|
||||
|
||||
@classroomEnrolledMap = _.reduce @classrooms.models, ((map, classroom) =>
|
||||
enrolled = _.reduce classroom.get('members'), ((sum, userID) =>
|
||||
sum + (if @members.get(userID).get('coursePrepaidID') then 1 else 0)
|
||||
), 0
|
||||
map[classroom.id] = enrolled
|
||||
map
|
||||
), {}
|
||||
for classroom in @classrooms.models
|
||||
map[classroom.id] = _.countBy(classroom.get('members'), (userID) -> enrolledUsers.get(userID)?).false
|
||||
|
||||
@classroomNotEnrolledMap = _.reduce @classrooms.models, ((map, classroom) =>
|
||||
enrolled = _.reduce classroom.get('members'), ((sum, userID) =>
|
||||
sum + (if not @members.get(userID).get('coursePrepaidID') then 1 else 0)
|
||||
), 0
|
||||
map[classroom.id] = enrolled
|
||||
map
|
||||
), {}
|
||||
@state.set({
|
||||
totalEnrolled: enrolledUsers.size()
|
||||
totalNotEnrolled: @notEnrolledUsers.size()
|
||||
classroomNotEnrolledMap: map
|
||||
})
|
||||
|
||||
true
|
||||
|
||||
|
@ -95,70 +84,21 @@ module.exports = class EnrollmentsView extends RootView
|
|||
)
|
||||
true
|
||||
|
||||
onClickHowToEnrollLink: ->
|
||||
@openModalView(new HowToEnrollModal())
|
||||
|
||||
onClickContactUsButton: ->
|
||||
@openModalView(new TeachersContactModal({ enrollmentsNeeded: @state.get('numberOfStudents') }))
|
||||
|
||||
onInputStudentsInput: ->
|
||||
input = @$('#students-input').val()
|
||||
if input isnt "" and (parseFloat(input) isnt parseInt(input) or _.isNaN parseInt(input))
|
||||
@$('#students-input').val(@numberOfStudents)
|
||||
@$('#students-input').val(@state.get('numberOfStudents'))
|
||||
else
|
||||
@numberOfStudents = Math.max(parseInt(@$('#students-input').val()) or 0, 0)
|
||||
@updatePrice()
|
||||
@state.set('numberOfStudents', Math.max(parseInt(@$('#students-input').val()) or 0, 0))
|
||||
|
||||
updatePrice: ->
|
||||
@renderSelectors '#price-form-group'
|
||||
numberOfStudentsIsValid: -> 0 < @get('numberOfStudents') < 100000
|
||||
|
||||
numberOfStudentsIsValid: -> 0 < @numberOfStudents < 100000
|
||||
|
||||
# onClickEnrollStudents: ->
|
||||
# TODO: Needs "All students" in modal dropdown
|
||||
|
||||
onClickPurchaseButton: ->
|
||||
return @openModalView new CreateAccountModal() if me.isAnonymous()
|
||||
unless @numberOfStudentsIsValid()
|
||||
alert("Please enter the maximum number of students needed for your class.")
|
||||
return
|
||||
|
||||
@state = undefined
|
||||
@stateMessage = undefined
|
||||
@render()
|
||||
|
||||
# Show Stripe handler
|
||||
application.tracker?.trackEvent 'Started course prepaid purchase', {
|
||||
price: @pricePerStudent, students: @numberOfStudents}
|
||||
stripeHandler.open
|
||||
amount: @numberOfStudents * @pricePerStudent
|
||||
description: "Full course access for #{@numberOfStudents} students"
|
||||
bitcoin: true
|
||||
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
||||
|
||||
onStripeReceivedToken: (e) ->
|
||||
@state = 'purchasing'
|
||||
@render?()
|
||||
|
||||
data =
|
||||
maxRedeemers: @numberOfStudents
|
||||
type: 'course'
|
||||
stripe:
|
||||
token: e.token.id
|
||||
timestamp: new Date().getTime()
|
||||
|
||||
$.ajax({
|
||||
url: '/db/prepaid/-/purchase',
|
||||
data: data,
|
||||
method: 'POST',
|
||||
context: @
|
||||
success: (prepaid) ->
|
||||
application.tracker?.trackEvent 'Finished course prepaid purchase', {price: @pricePerStudent, seats: @numberOfStudents}
|
||||
@state = 'purchased'
|
||||
@prepaids.add(prepaid)
|
||||
@render?()
|
||||
|
||||
error: (jqxhr, textStatus, errorThrown) ->
|
||||
application.tracker?.trackEvent 'Failed course prepaid purchase', status: textStatus
|
||||
if jqxhr.status is 402
|
||||
@state = 'error'
|
||||
@stateMessage = arguments[2]
|
||||
else
|
||||
@state = 'error'
|
||||
@stateMessage = "#{jqxhr.status}: #{jqxhr.responseText}"
|
||||
@render?()
|
||||
})
|
||||
onClickEnrollStudentsButton: ->
|
||||
modal = new ActivateLicensesModal({ selectedUsers: @notEnrolledUsers, users: @members })
|
||||
@openModalView(modal)
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
app = require 'core/application'
|
||||
Classroom = require 'models/Classroom'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Course = require 'models/Course'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
RootView = require 'views/core/RootView'
|
||||
stripeHandler = require 'core/services/stripe'
|
||||
template = require 'templates/courses/purchase-courses-view'
|
||||
User = require 'models/User'
|
||||
utils = require 'core/utils'
|
||||
Products = require 'collections/Products'
|
||||
|
||||
module.exports = class PurchaseCoursesView extends RootView
|
||||
id: 'purchase-courses-view'
|
||||
template: template
|
||||
numberOfStudents: 30
|
||||
pricePerStudent: 0
|
||||
|
||||
initialize: (options) ->
|
||||
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
|
||||
@fromClassroom = utils.getQueryVariable('from-classroom')
|
||||
@members = new CocoCollection([], { model: User })
|
||||
@listenTo @members, 'sync', @membersSync
|
||||
@classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom })
|
||||
@classrooms.comparator = '_id'
|
||||
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
|
||||
@supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}})
|
||||
@prepaids = new Prepaids()
|
||||
@prepaids.comparator = '_id'
|
||||
@prepaids.fetchByCreator(me.id)
|
||||
@supermodel.loadCollection(@prepaids, 'prepaids')
|
||||
@products = new Products()
|
||||
@supermodel.loadCollection(@products, 'products')
|
||||
super(options)
|
||||
|
||||
events:
|
||||
'input #students-input': 'onInputStudentsInput'
|
||||
'click #purchase-btn': 'onClickPurchaseButton'
|
||||
|
||||
onLoaded: ->
|
||||
@pricePerStudent = @products.findWhere({name: 'course'}).get('amount')
|
||||
super()
|
||||
|
||||
getPriceString: -> '$' + (@getPrice()/100).toFixed(2)
|
||||
getPrice: -> @pricePerStudent * @numberOfStudents
|
||||
|
||||
onceClassroomsSync: ->
|
||||
for classroom in @classrooms.models
|
||||
@members.fetch({
|
||||
remove: false
|
||||
url: "/db/classroom/#{classroom.id}/members"
|
||||
})
|
||||
|
||||
membersSync: ->
|
||||
@memberEnrolledMap = {}
|
||||
for user in @members.models
|
||||
@memberEnrolledMap[user.id] = user.get('coursePrepaidID')?
|
||||
@classroomNotEnrolledMap = {}
|
||||
@totalNotEnrolled = 0
|
||||
for classroom in @classrooms.models
|
||||
@classroomNotEnrolledMap[classroom.id] = 0
|
||||
for memberID in classroom.get('members')
|
||||
@classroomNotEnrolledMap[classroom.id]++ unless @memberEnrolledMap[memberID]
|
||||
@totalNotEnrolled += @classroomNotEnrolledMap[classroom.id]
|
||||
@numberOfStudents = @totalNotEnrolled
|
||||
@render?()
|
||||
|
||||
onInputStudentsInput: ->
|
||||
@numberOfStudents = Math.max(parseInt(@$('#students-input').val()) or 0, 0)
|
||||
@updatePrice()
|
||||
|
||||
updatePrice: ->
|
||||
@renderSelectors '#price-form-group'
|
||||
|
||||
numberOfStudentsIsValid: -> @numberOfStudents > 0 and @numberOfStudents < 100000
|
||||
|
||||
onClickPurchaseButton: ->
|
||||
return application.router.navigate('/teachers/signup', {trigger: true}) if me.isAnonymous()
|
||||
unless @numberOfStudentsIsValid()
|
||||
alert("Please enter the maximum number of students needed for your class.")
|
||||
return
|
||||
|
||||
@state = undefined
|
||||
@stateMessage = undefined
|
||||
@render()
|
||||
|
||||
# Show Stripe handler
|
||||
application.tracker?.trackEvent 'Started course prepaid purchase', {
|
||||
price: @pricePerStudent, students: @numberOfStudents}
|
||||
stripeHandler.open
|
||||
amount: @numberOfStudents * @pricePerStudent
|
||||
description: "Full course access for #{@numberOfStudents} students"
|
||||
bitcoin: true
|
||||
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
||||
|
||||
onStripeReceivedToken: (e) ->
|
||||
@state = 'purchasing'
|
||||
@render?()
|
||||
|
||||
data =
|
||||
maxRedeemers: @numberOfStudents
|
||||
type: 'course'
|
||||
stripe:
|
||||
token: e.token.id
|
||||
timestamp: new Date().getTime()
|
||||
|
||||
$.ajax({
|
||||
url: '/db/prepaid/-/purchase',
|
||||
data: data,
|
||||
method: 'POST',
|
||||
context: @
|
||||
success: ->
|
||||
application.tracker?.trackEvent 'Finished course prepaid purchase', {price: @pricePerStudent, seats: @numberOfStudents}
|
||||
@state = 'purchased'
|
||||
@render?()
|
||||
|
||||
error: (jqxhr, textStatus, errorThrown) ->
|
||||
application.tracker?.trackEvent 'Failed course prepaid purchase', status: textStatus
|
||||
if jqxhr.status is 402
|
||||
@state = 'error'
|
||||
@stateMessage = arguments[2]
|
||||
else
|
||||
@state = 'error'
|
||||
@stateMessage = "#{jqxhr.status}: #{jqxhr.responseText}"
|
||||
@render?()
|
||||
})
|
|
@ -24,40 +24,30 @@ module.exports = class TeacherClassView extends RootView
|
|||
template: template
|
||||
|
||||
events:
|
||||
'click .students-tab-btn': (e) ->
|
||||
e.preventDefault()
|
||||
@trigger 'open-students-tab'
|
||||
'click .course-progress-tab-btn': (e) ->
|
||||
e.preventDefault()
|
||||
@trigger 'open-course-progress-tab'
|
||||
'click .nav-tabs a': 'onClickNavTabLink'
|
||||
'click .unarchive-btn': 'onClickUnarchive'
|
||||
'click .edit-classroom': 'onClickEditClassroom'
|
||||
'click .add-students-btn': 'onClickAddStudents'
|
||||
'click .sort-by-name': 'sortByName'
|
||||
'click .sort-by-progress': 'sortByProgress'
|
||||
'click #copy-url-btn': 'copyURL'
|
||||
'click #copy-code-btn': 'copyCode'
|
||||
'click .edit-student-link': 'onClickEditStudentLink'
|
||||
'click .sort-button': 'onClickSortButton'
|
||||
'click #copy-url-btn': 'onClickCopyURLButton'
|
||||
'click #copy-code-btn': 'onClickCopyCodeButton'
|
||||
'click .remove-student-link': 'onClickRemoveStudentLink'
|
||||
'click .assign-student-button': 'onClickAssign'
|
||||
'click .enroll-student-button': 'onClickEnroll'
|
||||
'click .assign-student-button': 'onClickAssignStudentButton'
|
||||
'click .enroll-student-button': 'onClickEnrollStudentButton'
|
||||
'click .revoke-student-button': 'onClickRevokeStudentButton'
|
||||
'click .assign-to-selected-students': 'onClickBulkAssign'
|
||||
'click .enroll-selected-students': 'onClickBulkEnroll'
|
||||
'click .export-student-progress-btn': 'onClickExportStudentProgress'
|
||||
'click .select-all': 'onClickSelectAll'
|
||||
'click .student-checkbox': 'onClickStudentCheckbox'
|
||||
'change .course-select, .bulk-course-select': (e) ->
|
||||
@trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) }
|
||||
'keyup #student-search': 'onKeyPressStudentSearch'
|
||||
|
||||
getInitialState: ->
|
||||
if Backbone.history.getHash() in ['students-tab', 'course-progress-tab']
|
||||
activeTab = '#' + Backbone.history.getHash()
|
||||
else
|
||||
activeTab = '#students-tab'
|
||||
{
|
||||
sortAttribute: 'name'
|
||||
sortDirection: 1
|
||||
activeTab
|
||||
activeTab: '#' + (Backbone.history.getHash() or 'students-tab')
|
||||
students: new Users()
|
||||
classCode: ""
|
||||
joinURL: ""
|
||||
|
@ -80,38 +70,59 @@ module.exports = class TeacherClassView extends RootView
|
|||
@allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level'
|
||||
|
||||
@state = new State(@getInitialState())
|
||||
window.location.hash = @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab)
|
||||
@updateHash @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab)
|
||||
|
||||
@classroom = new Classroom({ _id: classroomID })
|
||||
@classroom.fetch()
|
||||
@supermodel.trackModel(@classroom)
|
||||
@supermodel.trackRequest @classroom.fetch()
|
||||
@onKeyPressStudentSearch = _.debounce(@onKeyPressStudentSearch, 200)
|
||||
|
||||
@students = new Users()
|
||||
@listenTo @classroom, 'sync', ->
|
||||
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
|
||||
if jqxhrs.length > 0
|
||||
@supermodel.trackCollection(@students)
|
||||
@supermodel.trackRequests jqxhrs
|
||||
|
||||
@classroom.sessions = new LevelSessions()
|
||||
requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom)
|
||||
@supermodel.trackRequests(requests)
|
||||
|
||||
@courses = new Courses()
|
||||
@courses.fetch()
|
||||
@supermodel.trackCollection(@courses)
|
||||
@students.comparator = (student1, student2) =>
|
||||
dir = @state.get('sortDirection')
|
||||
value = @state.get('sortValue')
|
||||
if value is 'name'
|
||||
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
|
||||
|
||||
if value is 'progress'
|
||||
# TODO: I would like for this to be in the Level model,
|
||||
# but it doesn't know about its own courseNumber.
|
||||
level1 = student1.latestCompleteLevel
|
||||
level2 = student2.latestCompleteLevel
|
||||
return -dir if not level1
|
||||
return dir if not level2
|
||||
return dir * (level1.courseNumber - level2.courseNumber or level1.levelNumber - level2.levelNumber)
|
||||
|
||||
if value is 'status'
|
||||
statusMap = { expired: 0, 'not-enrolled': 1, enrolled: 2 }
|
||||
diff = statusMap[student1.prepaidStatus()] - statusMap[student2.prepaidStatus()]
|
||||
return dir * diff if diff
|
||||
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
|
||||
|
||||
@courses = new Courses()
|
||||
@supermodel.trackRequest @courses.fetch()
|
||||
|
||||
@courseInstances = new CourseInstances()
|
||||
@courseInstances.fetchForClassroom(classroomID)
|
||||
@supermodel.trackCollection(@courseInstances)
|
||||
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
|
||||
|
||||
@levels = new Levels()
|
||||
@levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}})
|
||||
@supermodel.trackCollection(@levels)
|
||||
@supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}})
|
||||
|
||||
@attachMediatorEvents()
|
||||
|
||||
attachMediatorEvents: () ->
|
||||
@listenTo @state, 'sync change', @render
|
||||
@listenTo @state, 'sync change', ->
|
||||
if _.isEmpty(_.omit(@state.changed, 'searchTerm'))
|
||||
@renderSelectors('#enrollment-status-table')
|
||||
else
|
||||
@render()
|
||||
# Model/Collection events
|
||||
@listenTo @classroom, 'sync change update', ->
|
||||
@removeDeletedStudents()
|
||||
|
@ -128,8 +139,6 @@ module.exports = class TeacherClassView extends RootView
|
|||
@render() # TODO: use state
|
||||
@listenTo @courseInstances, 'add-members', ->
|
||||
noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000
|
||||
@listenToOnce @students, 'sync', # TODO: This seems like it's in the wrong place?
|
||||
@sortByName
|
||||
@listenTo @students, 'sync change update add remove reset', ->
|
||||
# Set state/props of things that depend on students?
|
||||
# Set specific parts of state based on the models, rather than just dumping the collection there?
|
||||
|
@ -141,18 +150,6 @@ module.exports = class TeacherClassView extends RootView
|
|||
@listenTo @students, 'sort', ->
|
||||
@state.set students: @students
|
||||
@render()
|
||||
|
||||
# DOM events
|
||||
@listenTo @, 'open-students-tab', ->
|
||||
if window.location.hash isnt '#students-tab'
|
||||
window.location.hash = '#students-tab'
|
||||
@state.set activeTab: '#students-tab'
|
||||
@listenTo @, 'open-course-progress-tab', ->
|
||||
if window.location.hash isnt '#course-progress-tab'
|
||||
window.location.hash = '#course-progress-tab'
|
||||
@state.set activeTab: '#course-progress-tab'
|
||||
@listenTo @, 'course-select:change', ({ selectedCourse }) ->
|
||||
@state.set selectedCourse: selectedCourse
|
||||
|
||||
setCourseMembers: =>
|
||||
for course in @courses.models
|
||||
|
@ -196,12 +193,22 @@ module.exports = class TeacherClassView extends RootView
|
|||
progressData
|
||||
classStats: @calculateClassStats()
|
||||
}
|
||||
|
||||
copyCode: ->
|
||||
|
||||
onClickNavTabLink: (e) ->
|
||||
e.preventDefault()
|
||||
hash = $(e.target).closest('a').attr('href')
|
||||
@updateHash(hash)
|
||||
@state.set activeTab: hash
|
||||
|
||||
updateHash: (hash) ->
|
||||
return if application.testing
|
||||
window.location.hash = hash
|
||||
|
||||
onClickCopyCodeButton: ->
|
||||
@$('#join-code-input').val(@state.get('classCode')).select()
|
||||
@tryCopy()
|
||||
|
||||
copyURL: ->
|
||||
onClickCopyURLButton: ->
|
||||
@$('#join-url-input').val(@state.get('joinURL')).select()
|
||||
@tryCopy()
|
||||
|
||||
|
@ -253,35 +260,20 @@ module.exports = class TeacherClassView extends RootView
|
|||
)
|
||||
true
|
||||
|
||||
sortByName: (e) ->
|
||||
if @state.get('sortValue') is 'name'
|
||||
onClickSortButton: (e) ->
|
||||
value = $(e.target).val()
|
||||
if value is @state.get('sortValue')
|
||||
@state.set('sortDirection', -@state.get('sortDirection'))
|
||||
else
|
||||
@state.set('sortValue', 'name')
|
||||
@state.set('sortDirection', 1)
|
||||
|
||||
dir = @state.get('sortDirection')
|
||||
@students.comparator = (student1, student2) ->
|
||||
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
|
||||
@state.set({
|
||||
sortValue: value
|
||||
sortDirection: 1
|
||||
})
|
||||
@students.sort()
|
||||
|
||||
sortByProgress: (e) ->
|
||||
if @state.get('sortValue') is 'progress'
|
||||
@state.set('sortDirection', -@state.get('sortDirection'))
|
||||
else
|
||||
@state.set('sortValue', 'progress')
|
||||
@state.set('sortDirection', 1)
|
||||
|
||||
dir = @state.get('sortDirection')
|
||||
|
||||
@students.comparator = (student) ->
|
||||
#TODO: I would like for this to be in the Level model,
|
||||
# but it doesn't know about its own courseNumber
|
||||
level = student.latestCompleteLevel
|
||||
if not level
|
||||
return -dir
|
||||
return dir * ((1000 * level.courseNumber) + level.levelNumber)
|
||||
@students.sort()
|
||||
onKeyPressStudentSearch: (e) ->
|
||||
console.log 'emit event'
|
||||
@state.set('searchTerm', $(e.target).val())
|
||||
|
||||
getSelectedStudentIDs: ->
|
||||
@$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
|
||||
|
@ -289,7 +281,7 @@ module.exports = class TeacherClassView extends RootView
|
|||
|
||||
ensureInstance: (courseID) ->
|
||||
|
||||
onClickEnroll: (e) ->
|
||||
onClickEnrollStudentButton: (e) ->
|
||||
userID = $(e.currentTarget).data('user-id')
|
||||
user = @students.get(userID)
|
||||
selectedUsers = new Users([user])
|
||||
|
@ -337,7 +329,7 @@ module.exports = class TeacherClassView extends RootView
|
|||
window.open(encodedUri)
|
||||
|
||||
|
||||
onClickAssign: (e) ->
|
||||
onClickAssignStudentButton: (e) ->
|
||||
userID = $(e.currentTarget).data('user-id')
|
||||
user = @students.get(userID)
|
||||
members = [userID]
|
||||
|
@ -380,6 +372,23 @@ module.exports = class TeacherClassView extends RootView
|
|||
courseInstance.addMembers members
|
||||
}
|
||||
null
|
||||
|
||||
onClickRevokeStudentButton: (e) ->
|
||||
button = $(e.currentTarget)
|
||||
userID = button.data('user-id')
|
||||
user = @students.get(userID)
|
||||
s = $.i18n.t('teacher.revoke_confirm').replace('{{student_name}}', user.broadName())
|
||||
return unless confirm(s)
|
||||
prepaid = user.makeCoursePrepaid()
|
||||
button.text($.i18n.t('teacher.revoking'))
|
||||
prepaid.revoke(user, {
|
||||
success: =>
|
||||
user.unset('coursePrepaid')
|
||||
error: (prepaid, jqxhr) =>
|
||||
msg = jqxhr.responseJSON.message
|
||||
noty text: msg, layout: 'center', type: 'error', killer: true, timeout: 3000
|
||||
complete: => @render()
|
||||
})
|
||||
|
||||
onClickSelectAll: (e) ->
|
||||
e.preventDefault()
|
||||
|
@ -419,7 +428,16 @@ module.exports = class TeacherClassView extends RootView
|
|||
stats.averageLevelsComplete = if @students.size() then (_.size(completeSessions) / @students.size()).toFixed(1) else 'N/A' # '
|
||||
stats.totalLevelsComplete = _.size(completeSessions)
|
||||
|
||||
enrolledUsers = @students.filter (user) -> user.get('coursePrepaidID')
|
||||
enrolledUsers = @students.filter (user) -> user.isEnrolled()
|
||||
stats.enrolledUsers = _.size(enrolledUsers)
|
||||
|
||||
return stats
|
||||
|
||||
studentStatusString: (student) ->
|
||||
status = student.prepaidStatus()
|
||||
expires = student.get('coursePrepaid')?.endDate
|
||||
string = switch status
|
||||
when 'not-enrolled' then $.i18n.t('teacher.status_not_enrolled')
|
||||
when 'enrolled' then (if expires then $.i18n.t('teacher.status_enrolled') else '-')
|
||||
when 'expired' then $.i18n.t('teacher.status_expired')
|
||||
return string.replace('{{date}}', moment(expires).utc().format('l'))
|
||||
|
|
|
@ -12,7 +12,6 @@ CourseInstance = require 'models/CourseInstance'
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/courses/teacher-courses-view'
|
||||
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
|
||||
module.exports = class TeacherCoursesView extends RootView
|
||||
id: 'teacher-courses-view'
|
||||
|
@ -52,17 +51,11 @@ module.exports = class TeacherCoursesView extends RootView
|
|||
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
|
||||
@supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}})
|
||||
@campaigns = new Campaigns()
|
||||
@campaigns.fetch()
|
||||
@supermodel.trackCollection(@campaigns)
|
||||
@supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels,levelsUpdated' } })
|
||||
@courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance })
|
||||
@courseInstances.comparator = 'courseID'
|
||||
@courseInstances.sliceWithMembers = -> return @filter (courseInstance) -> _.size(courseInstance.get('members')) and courseInstance.get('classroomID')
|
||||
@supermodel.loadCollection(@courseInstances, 'course_instances', {data: {ownerID: me.id}})
|
||||
@prepaids = new Prepaids()
|
||||
@prepaids.comparator = '_id'
|
||||
if not me.isAnonymous()
|
||||
@prepaids.fetchByCreator(me.id)
|
||||
@supermodel.loadCollection(@prepaids, 'prepaids') # just registers
|
||||
@members = new CocoCollection([], { model: User })
|
||||
@listenTo @members, 'sync', @render
|
||||
@
|
||||
|
|
6
app/views/teachers/HowToEnrollModal.coffee
Normal file
6
app/views/teachers/HowToEnrollModal.coffee
Normal file
|
@ -0,0 +1,6 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
|
||||
module.exports = class HowToEnrollModal extends ModalView
|
||||
id: 'how-to-enroll-modal'
|
||||
template: require 'templates/teachers/how-to-enroll-modal'
|
||||
|
70
app/views/teachers/TeachersContactModal.coffee
Normal file
70
app/views/teachers/TeachersContactModal.coffee
Normal file
|
@ -0,0 +1,70 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
State = require 'models/State'
|
||||
TrialRequests = require 'collections/TrialRequests'
|
||||
forms = require 'core/forms'
|
||||
contact = require 'core/contact'
|
||||
|
||||
module.exports = class TeachersContactModal extends ModalView
|
||||
id: 'teachers-contact-modal'
|
||||
template: require 'templates/teachers/teachers-contact-modal'
|
||||
|
||||
events:
|
||||
'submit form': 'onSubmitForm'
|
||||
'change form': 'onChangeForm'
|
||||
|
||||
initialize: (options={}) ->
|
||||
@state = new State({
|
||||
formValues: {
|
||||
email: ''
|
||||
message: ''
|
||||
}
|
||||
formErrors: {}
|
||||
sendingState: 'standby' # 'sending', 'sent', 'error'
|
||||
})
|
||||
@enrollmentsNeeded = options.enrollmentsNeeded or '-'
|
||||
@trialRequests = new TrialRequests()
|
||||
@supermodel.trackRequest @trialRequests.fetchOwn()
|
||||
@state.on 'change', @render, @
|
||||
|
||||
onLoaded: ->
|
||||
trialRequest = @trialRequests.first()
|
||||
props = trialRequest?.get('properties') or {}
|
||||
message = """
|
||||
Name of School/District: #{props.organization or ''}
|
||||
Your Name: #{props.name || ''}
|
||||
Enrollments Needed: #{@enrollmentsNeeded}
|
||||
|
||||
Message: Hi CodeCombat! I want to learn more about the Classroom experience and get enrollments so that my students can access Computer Science 2 and on.
|
||||
"""
|
||||
email = props.email or me.get('email') or ''
|
||||
@state.set('formValues', { email, message })
|
||||
super()
|
||||
|
||||
onChangeForm: ->
|
||||
# Want to re-render without losing form focus. TODO: figure out how in state system.
|
||||
@$('#submit-btn').attr('disabled', false)
|
||||
|
||||
onSubmitForm: (e) ->
|
||||
e.preventDefault()
|
||||
return if @state.get('sendingState') is 'sending'
|
||||
|
||||
formValues = forms.formToObject @$el
|
||||
@state.set('formValues', formValues)
|
||||
|
||||
formErrors = {}
|
||||
if not forms.validateEmail(formValues.email)
|
||||
formErrors.email = 'Invalid email.'
|
||||
if not formValues.message
|
||||
formErrors.message = 'Message required.'
|
||||
@state.set({ formErrors, formValues, sendingState: 'standby' })
|
||||
return unless _.isEmpty(formErrors)
|
||||
|
||||
@state.set('sendingState', 'sending')
|
||||
data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com' }, formValues)
|
||||
contact.send({
|
||||
data
|
||||
context: @
|
||||
success: -> @state.set({ sendingState: 'sent' })
|
||||
error: -> @state.set({ sendingState: 'error' })
|
||||
})
|
||||
|
|
@ -113,7 +113,7 @@
|
|||
"karma-coverage": "~0.5.1",
|
||||
"karma-firefox-launcher": "~0.1.3",
|
||||
"karma-html2js-preprocessor": "~0.1.0",
|
||||
"karma-jasmine": "~0.2.0",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-phantomjs-launcher": "~0.1.1",
|
||||
"karma-requirejs": "~0.2.1",
|
||||
"karma-script-launcher": "~0.1.0",
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// Migrate users from coursePrepaidID to coursePrepaid
|
||||
|
||||
startDate = new Date(Date.UTC(2016,4,22)).toISOString(); // NOTE: Month is 0 indexed...
|
||||
endDate = new Date(Date.UTC(2017,4,22)).toISOString();
|
||||
print('Setting start/end', startDate, endDate);
|
||||
|
||||
db.prepaids.find({type: 'course'}).limit(10).forEach(function (prepaid) {
|
||||
var properties = prepaid.properties || {};
|
||||
if (!(prepaid.endDate && prepaid.startDate)) {
|
||||
if (!prepaid.endDate) {
|
||||
if(properties.endDate) {
|
||||
print('Updating from existing end date', properties.endDate);
|
||||
prepaid.endDate = properties.endDate.toISOString();
|
||||
}
|
||||
else {
|
||||
prepaid.endDate = endDate;
|
||||
}
|
||||
}
|
||||
if (!prepaid.startDate) {
|
||||
prepaid.startDate = startDate;
|
||||
}
|
||||
print('updating prepaid', JSON.stringify(prepaid, null, '\t'));
|
||||
//print(db.prepaids.save(prepaid));
|
||||
}
|
||||
|
||||
var redeemers = prepaid.redeemers || [];
|
||||
for (var index in redeemers) {
|
||||
var redeemer = redeemers[index];
|
||||
var user = db.users.findOne({ _id: redeemer.userID }, { coursePrepaid: 1, coursePrepaidID: 1 });
|
||||
if (user.coursePrepaidID && !user.coursePrepaid) {
|
||||
var update = {
|
||||
$set: { coursePrepaid: { _id: user.coursePrepaidID, startDate: prepaid.startDate, endDate: prepaid.endDate } },
|
||||
$unset: { coursePrepaidID: '' }
|
||||
}
|
||||
print('updating user', JSON.stringify(user, null, ' '), JSON.stringify(update, null, ' '));
|
||||
//print(db.users.update({_id: user._id}, update));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -38,7 +38,6 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
|||
return @removeMember(req, res, args[0]) if req.method is 'DELETE' and 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 @getRecentAPI(req, res) if relationship is 'recent'
|
||||
return @redeemPrepaidCodeAPI(req, res) if args[1] is 'redeem_prepaid'
|
||||
return @getMyCourseLevelSessionsAPI(req, res, args[0]) if args[1] is 'my-course-level-sessions'
|
||||
return @findByLevel(req, res, args[2]) if args[1] is 'find_by_level'
|
||||
|
@ -169,33 +168,6 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
|||
cleandocs = (UserHandler.formatEntity(req, doc) for doc in users)
|
||||
@sendSuccess(res, cleandocs)
|
||||
|
||||
getRecentAPI: (req, res) ->
|
||||
return @sendUnauthorizedError(res) unless req.user?.isAdmin()
|
||||
query = {$and: [{name: {$ne: 'Single Player'}}, {hourOfCode: {$ne: true}}]}
|
||||
query["$and"].push(_id: {$gte: objectIdFromTimestamp(req.body.startDay + "T00:00:00.000Z")}) if req.body.startDay?
|
||||
query["$and"].push(_id: {$lt: objectIdFromTimestamp(req.body.endDay + "T00:00:00.000Z")}) if req.body.endDay?
|
||||
CourseInstance.find query, {courseID: 1, members: 1, ownerID: 1}, (err, courseInstances) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
userIDs = []
|
||||
for courseInstance in courseInstances
|
||||
if members = courseInstance.get('members')
|
||||
userIDs.push(userID) for userID in members
|
||||
|
||||
User.find {_id: {$in: userIDs}}, {coursePrepaidID: 1}, (err, users) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
prepaidIDs = []
|
||||
for user in users
|
||||
if prepaidID = user.get('coursePrepaidID')
|
||||
prepaidIDs.push(prepaidID)
|
||||
|
||||
Prepaid.find {_id: {$in: prepaidIDs}}, {properties: 1}, (err, prepaids) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
data =
|
||||
courseInstances: (@formatEntity(req, courseInstance) for courseInstance in courseInstances)
|
||||
students: (@formatEntity(req, user) for user in users)
|
||||
prepaids: (@formatEntity(req, prepaid) for prepaid in prepaids)
|
||||
@sendSuccess(res, data)
|
||||
|
||||
inviteStudents: (req, res, courseInstanceID) ->
|
||||
return @sendUnauthorizedError(res) if not req.user?
|
||||
if not req.body.emails
|
||||
|
|
|
@ -30,7 +30,6 @@ PrepaidHandler = class PrepaidHandler extends Handler
|
|||
return @getPrepaidAPI(req, res, args[2]) if relationship is 'code'
|
||||
return @createPrepaidAPI(req, res) if relationship is 'create'
|
||||
return @purchasePrepaidAPI(req, res) if relationship is 'purchase'
|
||||
return @postRedeemerAPI(req, res, args[0]) if relationship is 'redeemers'
|
||||
super arguments...
|
||||
|
||||
getCoursePrepaidsAPI: (req, res, code) ->
|
||||
|
@ -78,45 +77,6 @@ PrepaidHandler = class PrepaidHandler extends Handler
|
|||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, prepaid.toObject())
|
||||
|
||||
postRedeemerAPI: (req, res, prepaidID) ->
|
||||
return @sendForbiddenError(res) if prepaidID.toString() < cutoffID.toString()
|
||||
return @sendMethodNotAllowed(res, 'You may only POST redeemers.') if req.method isnt 'POST'
|
||||
return @sendBadInputError(res, 'Need an object with a userID') unless req.body?.userID
|
||||
Prepaid.findById(prepaidID).exec (err, prepaid) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res) if not prepaid
|
||||
return @sendForbiddenError(res) if prepaid.get('creator').toString() isnt req.user.id
|
||||
return @sendForbiddenError(res) if prepaid.get('redeemers')? and _.size(prepaid.get('redeemers')) >= prepaid.get('maxRedeemers')
|
||||
return @sendForbiddenError(res) unless prepaid.get('type') is 'course'
|
||||
return @sendForbiddenError(res) if prepaid.get('properties')?.endDate < new Date()
|
||||
User.findById(req.body.userID).exec (err, user) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res, 'User for given ID not found') if not user
|
||||
return @sendSuccess(res, @formatEntity(req, prepaid)) if user.get('coursePrepaidID')
|
||||
return @sendForbiddenError(res, 'Teachers may not be enrolled') if user.isTeacher()
|
||||
userID = user.get('_id')
|
||||
|
||||
query =
|
||||
_id: prepaid.get('_id')
|
||||
'redeemers.userID': { $ne: user.get('_id') }
|
||||
$where: "this.maxRedeemers > 0 && (!this.redeemers || this.redeemers.length < #{prepaid.get('maxRedeemers')})"
|
||||
update = { $push: { redeemers : { date: new Date(), userID: userID } }}
|
||||
Prepaid.update query, update, (err, result) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
if result.nModified is 0
|
||||
@logError(req.user, "POST prepaid redeemer lost race on maxRedeemers")
|
||||
return @sendForbiddenError(res)
|
||||
|
||||
user.set('coursePrepaidID', prepaid.get('_id'))
|
||||
user.set('role', 'student') if not user.get('role')
|
||||
user.save (err, user) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
# return prepaid with new redeemer added locally
|
||||
redeemers = _.clone(prepaid.get('redeemers') or [])
|
||||
redeemers.push({ date: new Date(), userID: userID })
|
||||
prepaid.set('redeemers', redeemers)
|
||||
@sendSuccess(res, @formatEntity(req, prepaid))
|
||||
|
||||
createPrepaid: (user, type, maxRedeemers, properties, done) ->
|
||||
Prepaid.generateNewCode (code) =>
|
||||
return done('Database error.') unless code
|
||||
|
@ -254,23 +214,6 @@ PrepaidHandler = class PrepaidHandler extends Handler
|
|||
slack.sendSlackMessage msg, ['tower']
|
||||
done(null, prepaid)
|
||||
|
||||
get: (req, res) ->
|
||||
if creator = req.query.creator
|
||||
return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or creator is req.user.id)
|
||||
return @sendBadInputError(res, 'Bad creator') unless utils.isID creator
|
||||
q = {
|
||||
_id: {$gt: cutoffID}
|
||||
creator: mongoose.Types.ObjectId(creator)
|
||||
type: 'course'
|
||||
}
|
||||
Prepaid.find q, (err, prepaids) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
documents = []
|
||||
for prepaid in prepaids
|
||||
documents.push(@formatEntity(req, prepaid)) unless prepaid.get('properties')?.endDate < new Date()
|
||||
return @sendSuccess(res, documents)
|
||||
else
|
||||
super(arguments...)
|
||||
|
||||
makeNewInstance: (req) ->
|
||||
prepaid = super(req)
|
||||
|
|
|
@ -98,12 +98,7 @@ module.exports =
|
|||
|
||||
user = req.body.user
|
||||
throw new errors.UnprocessableEntity('Specify an id, username or email to espionage.') unless user
|
||||
if utils.isID(user)
|
||||
query = {_id: mongoose.Types.ObjectId(user)}
|
||||
else
|
||||
user = user.toLowerCase()
|
||||
query = $or: [{nameLower: user}, {emailLower: user}]
|
||||
user = yield User.findOne(query)
|
||||
user = yield User.search(user)
|
||||
amActually = req.user
|
||||
throw new errors.NotFound() unless user
|
||||
req.loginAsync = Promise.promisify(req.login)
|
||||
|
@ -210,4 +205,4 @@ module.exports =
|
|||
if originalName is name
|
||||
res.send 200, response
|
||||
else
|
||||
throw new errors.Conflict('Name is taken', response)
|
||||
throw new errors.Conflict('Name is taken', response)
|
||||
|
|
|
@ -10,6 +10,8 @@ Course = require '../models/Course'
|
|||
User = require '../models/User'
|
||||
Level = require '../models/Level'
|
||||
parse = require '../commons/parse'
|
||||
{objectIdFromTimestamp} = require '../lib/utils'
|
||||
Prepaid = require '../models/Prepaid'
|
||||
|
||||
module.exports =
|
||||
addMembers: wrap (req, res) ->
|
||||
|
@ -42,13 +44,13 @@ module.exports =
|
|||
throw new errors.Forbidden('You must own the classroom to add members')
|
||||
|
||||
# Only the enrolled users
|
||||
users = yield User.find({ _id: { $in: userIDs }}).select('coursePrepaidID')
|
||||
usersArePrepaid = _.all((user.get('coursePrepaidID') for user in users))
|
||||
users = yield User.find({ _id: { $in: userIDs }}).select('coursePrepaid coursePrepaidID') # TODO: remove coursePrepaidID once migrated
|
||||
usersAreEnrolled = _.all((user.isEnrolled() for user in users))
|
||||
|
||||
course = yield Course.findById courseInstance.get('courseID')
|
||||
throw new errors.NotFound('Course referenced by course instance not found') unless course
|
||||
|
||||
if not (course.get('free') or usersArePrepaid)
|
||||
if not (course.get('free') or usersAreEnrolled)
|
||||
throw new errors.PaymentRequired('Cannot add users to a course instance until they are added to a prepaid')
|
||||
|
||||
userObjectIDs = (mongoose.Types.ObjectId(userID) for userID in userIDs)
|
||||
|
@ -123,3 +125,28 @@ module.exports =
|
|||
classroom = classroom.toObject({req: req})
|
||||
|
||||
res.status(200).send(classroom)
|
||||
|
||||
|
||||
fetchRecent: wrap (req, res) ->
|
||||
query = {$and: [{name: {$ne: 'Single Player'}}, {hourOfCode: {$ne: true}}]}
|
||||
query["$and"].push(_id: {$gte: objectIdFromTimestamp(req.body.startDay + "T00:00:00.000Z")}) if req.body.startDay?
|
||||
query["$and"].push(_id: {$lt: objectIdFromTimestamp(req.body.endDay + "T00:00:00.000Z")}) if req.body.endDay?
|
||||
courseInstances = yield CourseInstance.find(query, {courseID: 1, members: 1, ownerID: 1})
|
||||
|
||||
userIDs = []
|
||||
for courseInstance in courseInstances
|
||||
if members = courseInstance.get('members')
|
||||
userIDs.push(userID) for userID in members
|
||||
users = yield User.find({_id: {$in: userIDs}}, {coursePrepaid: 1})
|
||||
|
||||
prepaidIDs = []
|
||||
for user in users
|
||||
if prepaidID = user.get('coursePrepaid')
|
||||
prepaidIDs.push(prepaidID._id)
|
||||
prepaids = yield Prepaid.find({_id: {$in: prepaidIDs}}, {properties: 1})
|
||||
|
||||
res.send({
|
||||
courseInstances: (courseInstance.toObject({req: req}) for courseInstance in courseInstances)
|
||||
students: (user.toObject({req: req}) for user in users)
|
||||
prepaids: (prepaid.toObject({req: req}) for prepaid in prepaids)
|
||||
})
|
||||
|
|
|
@ -8,8 +8,10 @@ module.exports =
|
|||
courses: require './courses'
|
||||
files: require './files'
|
||||
healthcheck: require './healthcheck'
|
||||
levels: require './levels'
|
||||
named: require './named'
|
||||
patchable: require './patchable'
|
||||
prepaids: require './prepaids'
|
||||
rest: require './rest'
|
||||
trialRequests: require './trial-requests'
|
||||
users: require './users'
|
||||
|
|
81
server/middleware/levels.coffee
Normal file
81
server/middleware/levels.coffee
Normal file
|
@ -0,0 +1,81 @@
|
|||
wrap = require 'co-express'
|
||||
errors = require '../commons/errors'
|
||||
Level = require '../models/Level'
|
||||
LevelSession = require '../models/LevelSession'
|
||||
CourseInstance = require '../models/CourseInstance'
|
||||
Classroom = require '../models/Classroom'
|
||||
Course = require '../models/Course'
|
||||
database = require '../commons/database'
|
||||
|
||||
module.exports =
|
||||
upsertSession: wrap (req, res) ->
|
||||
level = yield database.getDocFromHandle(req, Level)
|
||||
if not level
|
||||
throw new errors.NotFound('Level not found.')
|
||||
|
||||
sessionQuery =
|
||||
level:
|
||||
original: level.get('original').toString()
|
||||
majorVersion: level.get('version').major
|
||||
creator: req.user.id
|
||||
|
||||
if req.query.team?
|
||||
sessionQuery.team = req.query.team
|
||||
|
||||
session = yield LevelSession.findOne(sessionQuery)
|
||||
if session
|
||||
return res.send(session.toObject({req: req}))
|
||||
|
||||
attrs = sessionQuery
|
||||
_.extend(attrs, {
|
||||
state:
|
||||
complete: false
|
||||
scripts:
|
||||
currentScript: null # will not save empty objects
|
||||
permissions: [
|
||||
{target: req.user.id, access: 'owner'}
|
||||
{target: 'public', access: 'write'}
|
||||
]
|
||||
codeLanguage: req.user.get('aceConfig')?.language ? 'python'
|
||||
})
|
||||
|
||||
if level.get('type') in ['course', 'course-ladder'] or req.query.course?
|
||||
|
||||
# Find the course and classroom that has assigned this level, verify access
|
||||
courseInstances = yield CourseInstance.find({members: req.user._id})
|
||||
classroomIDs = (courseInstance.get('classroomID') for courseInstance in courseInstances)
|
||||
classroomIDs = _.filter _.uniq classroomIDs, false, (objectID='') -> objectID.toString()
|
||||
classrooms = yield Classroom.find({ _id: { $in: classroomIDs }})
|
||||
classroomWithLevel = null
|
||||
courseID = null
|
||||
classroomMap = {}
|
||||
classroomMap[classroom.id] = classroom for classroom in classrooms
|
||||
for courseInstance in courseInstances
|
||||
classroom = classroomMap[courseInstance.get('classroomID').toString()]
|
||||
courseID = courseInstance.get('courseID')
|
||||
classroomCourse = _.find(classroom.get('courses'), (c) -> c._id.equals(courseID))
|
||||
for courseLevel in classroomCourse.levels
|
||||
if courseLevel.original.equals(level._id)
|
||||
classroomWithLevel = classroom
|
||||
break
|
||||
break if classroomWithLevel
|
||||
|
||||
unless classroomWithLevel
|
||||
throw new errors.PaymentRequired('You must be in a course which includes this level to play it')
|
||||
|
||||
course = yield Course.findById(courseID).select('free')
|
||||
unless course.get('free') or req.user.isEnrolled()
|
||||
throw new errors.PaymentRequired('You must be enrolled to access this content')
|
||||
|
||||
lang = classroomWithLevel.get('aceConfig').language
|
||||
attrs.codeLanguage = lang if lang
|
||||
|
||||
else
|
||||
requiresSubscription = level.get('requiresSubscription') or (req.user.isOnPremiumServer() and level.get('campaign') and not (level.slug in ['dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'forgetful-gemsmith', 'signs-and-portents', 'true-names']))
|
||||
canPlayAnyway = req.user.isPremium() or level.get 'adventurer'
|
||||
if requiresSubscription and not canPlayAnyway
|
||||
throw new errors.PaymentRequired('This level requires a subscription to play')
|
||||
|
||||
session = new LevelSession(attrs)
|
||||
yield session.save()
|
||||
res.send(session.toObject({req: req}))
|
152
server/middleware/prepaids.coffee
Normal file
152
server/middleware/prepaids.coffee
Normal file
|
@ -0,0 +1,152 @@
|
|||
wrap = require 'co-express'
|
||||
errors = require '../commons/errors'
|
||||
database = require '../commons/database'
|
||||
Prepaid = require '../models/Prepaid'
|
||||
User = require '../models/User'
|
||||
mongoose = require 'mongoose'
|
||||
|
||||
cutoffDate = new Date(2015,11,11)
|
||||
cutoffID = mongoose.Types.ObjectId(Math.floor(cutoffDate/1000).toString(16)+'0000000000000000')
|
||||
|
||||
module.exports =
|
||||
logError: (user, msg) ->
|
||||
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
|
||||
|
||||
|
||||
post: wrap (req, res) ->
|
||||
validTypes = ['course']
|
||||
unless req.body.type in validTypes
|
||||
throw new errors.UnprocessableEntity("type must be on of: #{validTypes}.")
|
||||
# TODO: deprecate or refactor other prepaid types
|
||||
|
||||
if req.body.creator
|
||||
user = yield User.search(req.body.creator)
|
||||
if not user
|
||||
throw new errors.NotFound('User not found')
|
||||
req.body.creator = user.id
|
||||
|
||||
prepaid = database.initDoc(req, Prepaid)
|
||||
database.assignBody(req, prepaid)
|
||||
prepaid.set('code', yield Prepaid.generateNewCodeAsync())
|
||||
prepaid.set('redeemers', [])
|
||||
database.validateDoc(prepaid)
|
||||
yield prepaid.save()
|
||||
res.status(201).send(prepaid.toObject())
|
||||
|
||||
|
||||
redeem: wrap (req, res) ->
|
||||
if not req.user?.isTeacher()
|
||||
throw new errors.Forbidden('Must be a teacher to use enrollments')
|
||||
|
||||
prepaid = yield database.getDocFromHandle(req, Prepaid)
|
||||
if not prepaid
|
||||
throw new errors.NotFound('Prepaid not found.')
|
||||
|
||||
if prepaid._id.getTimestamp().getTime() < cutoffDate.getTime()
|
||||
throw new errors.Forbidden('Cannot redeem from prepaids older than November 11, 2015')
|
||||
unless prepaid.get('creator').equals(req.user._id)
|
||||
throw new errors.Forbidden('You may not redeem enrollments from this prepaid')
|
||||
if prepaid.get('redeemers')? and _.size(prepaid.get('redeemers')) >= prepaid.get('maxRedeemers')
|
||||
throw new errors.Forbidden('This prepaid is exhausted')
|
||||
unless prepaid.get('type') is 'course'
|
||||
throw new errors.Forbidden('This prepaid is not of type "course"')
|
||||
if prepaid.get('endDate') and new Date(prepaid.get('endDate')) < new Date()
|
||||
throw new errors.Forbidden('This prepaid is expired')
|
||||
|
||||
user = yield User.findById(req.body?.userID)
|
||||
if not user
|
||||
throw new errors.NotFound('User not found.')
|
||||
|
||||
if user.isEnrolled()
|
||||
return res.status(200).send(prepaid.toObject({req: req}))
|
||||
if user.isTeacher()
|
||||
throw new errors.Forbidden('Teachers may not be enrolled')
|
||||
|
||||
query =
|
||||
_id: prepaid._id
|
||||
'redeemers.userID': { $ne: user._id }
|
||||
$where: "this.maxRedeemers > 0 && (!this.redeemers || this.redeemers.length < #{prepaid.get('maxRedeemers')})"
|
||||
update = { $push: { redeemers : { date: new Date(), userID: user._id } }}
|
||||
result = yield Prepaid.update(query, update)
|
||||
if result.nModified is 0
|
||||
@logError(req.user, "POST prepaid redeemer lost race on maxRedeemers")
|
||||
throw new errors.Forbidden('This prepaid is exhausted')
|
||||
|
||||
update = {
|
||||
$set: {
|
||||
coursePrepaid: {
|
||||
_id: prepaid._id
|
||||
startDate: prepaid.get('startDate')
|
||||
endDate: prepaid.get('endDate')
|
||||
}
|
||||
}
|
||||
}
|
||||
if not user.get('role')
|
||||
update.$set.role = 'student'
|
||||
yield user.update(update)
|
||||
|
||||
# return prepaid with new redeemer added locally
|
||||
redeemers = _.clone(prepaid.get('redeemers') or [])
|
||||
redeemers.push({ date: new Date(), userID: user._id })
|
||||
prepaid.set('redeemers', redeemers)
|
||||
res.status(201).send(prepaid.toObject({req: req}))
|
||||
|
||||
|
||||
revoke: wrap (req, res) ->
|
||||
if not req.user?.isTeacher()
|
||||
throw new errors.Forbidden('Must be a teacher to use enrollments')
|
||||
|
||||
prepaid = yield database.getDocFromHandle(req, Prepaid)
|
||||
if not prepaid
|
||||
throw new errors.NotFound('Prepaid not found.')
|
||||
|
||||
unless prepaid.get('creator').equals(req.user._id)
|
||||
throw new errors.Forbidden('You may not revoke enrollments you do not own.')
|
||||
unless prepaid.get('type') is 'course'
|
||||
throw new errors.Forbidden('This prepaid is not of type "course".')
|
||||
if prepaid.get('endDate') and new Date(prepaid.get('endDate')) < new Date()
|
||||
throw new errors.Forbidden('This prepaid is expired.')
|
||||
|
||||
user = yield User.findById(req.body?.userID)
|
||||
if not user
|
||||
throw new errors.NotFound('User not found.')
|
||||
|
||||
if not user.isEnrolled()
|
||||
throw new errors.UnprocessableEntity('User to revoke must be enrolled first.')
|
||||
if not _.any(prepaid.get('redeemers'), (obj) -> obj.userID.equals(user._id))
|
||||
throw new errors.UnprocessableEntity('User was not enrolled with this set of enrollments')
|
||||
|
||||
query =
|
||||
_id: prepaid._id
|
||||
'redeemers.userID': { $eq: user._id }
|
||||
update = { $pull: { redeemers : { userID: user._id } }}
|
||||
result = yield Prepaid.update(query, update)
|
||||
if result.nModified is 0
|
||||
@logError(req.user, "POST prepaid redeemer lost race on maxRedeemers")
|
||||
throw new errors.UnprocessableEntity('User was not enrolled with this set of enrollments (race)')
|
||||
|
||||
user.set('coursePrepaid', undefined)
|
||||
yield user.save()
|
||||
|
||||
# return prepaid with new redeemer added locally
|
||||
prepaid.set('redeemers', _.filter(prepaid.get('redeemers') or [], (obj) -> not obj.userID.equals(user._id)))
|
||||
res.status(200).send(prepaid.toObject({req: req}))
|
||||
|
||||
|
||||
fetchByCreator: wrap (req, res, next) ->
|
||||
creator = req.query.creator
|
||||
return next() if not creator
|
||||
|
||||
unless req.user.isAdmin() or creator is req.user.id
|
||||
throw new errors.Forbidden('Must be logged in as given creator')
|
||||
unless database.isID(creator)
|
||||
throw new errors.UnprocessableEntity('Invalid creator')
|
||||
|
||||
q = {
|
||||
_id: { $gt: cutoffID }
|
||||
creator: mongoose.Types.ObjectId(creator)
|
||||
type: 'course'
|
||||
}
|
||||
|
||||
prepaids = yield Prepaid.find(q)
|
||||
res.send((prepaid.toObject({req: req}) for prepaid in prepaids))
|
|
@ -90,4 +90,20 @@ LevelSessionSchema.statics.editableProperties = ['multiplayer', 'players', 'code
|
|||
'browser']
|
||||
LevelSessionSchema.statics.jsonSchema = jsonschema
|
||||
|
||||
LevelSessionSchema.set('toObject', {
|
||||
transform: (doc, ret, options) ->
|
||||
req = options.req
|
||||
return ret unless req # TODO: Make deleting properties the default, but the consequences are far reaching
|
||||
|
||||
submittedCode = doc.get('submittedCode')
|
||||
unless req.user?.isAdmin() or req.user?.id is doc.get('creator') or ('employer' in (req.user?.get('permissions') ? [])) or not doc.get('submittedCode') # TODO: only allow leaderboard access to non-top-5 solutions
|
||||
ret = _.omit ret, LevelSession.privateProperties
|
||||
if req.query.interpret
|
||||
plan = submittedCode[if doc.get('team') is 'humans' then 'hero-placeholder' else 'hero-placeholder-1']?.plan ? ''
|
||||
plan = LZString.compressToUTF16 plan
|
||||
ret.interpret = plan
|
||||
ret.code = submittedCode
|
||||
return ret
|
||||
})
|
||||
|
||||
module.exports = LevelSession = mongoose.model('level.session', LevelSessionSchema, 'level.sessions')
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
Promise = require 'bluebird'
|
||||
mongoose = require 'mongoose'
|
||||
config = require '../../server_config'
|
||||
PrepaidSchema = new mongoose.Schema {
|
||||
creator: mongoose.Schema.Types.ObjectId
|
||||
}, {strict: false, minimize: false,read:config.mongo.readpref}
|
||||
co = require 'co'
|
||||
jsonSchema = require '../../app/schemas/models/prepaid.schema'
|
||||
|
||||
PrepaidSchema.index({code: 1}, { unique: true })
|
||||
PrepaidSchema.index({'redeemers.userID': 1})
|
||||
PrepaidSchema.index({owner: 1, endDate: 1}, { sparse: true })
|
||||
|
||||
PrepaidSchema.statics.DEFAULT_START_DATE = new Date(2016,4,15).toISOString()
|
||||
PrepaidSchema.statics.DEFAULT_END_DATE = new Date(2017,5,1).toISOString()
|
||||
|
||||
PrepaidSchema.statics.generateNewCode = (done) ->
|
||||
# Deprecated for not following Node callback convention. TODO: Remove
|
||||
tryCode = ->
|
||||
code = _.sample("abcdefghijklmnopqrstuvwxyz0123456789", 8).join('')
|
||||
Prepaid.findOne code: code, (err, prepaid) ->
|
||||
|
@ -15,6 +23,14 @@ PrepaidSchema.statics.generateNewCode = (done) ->
|
|||
return done(code) unless prepaid
|
||||
tryCode()
|
||||
tryCode()
|
||||
|
||||
PrepaidSchema.statics.generateNewCodeAsync = co.wrap (done) ->
|
||||
code = null
|
||||
while true
|
||||
code = _.sample("abcdefghijklmnopqrstuvwxyz0123456789", 8).join('')
|
||||
prepaid = yield Prepaid.findOne({code: code})
|
||||
break if not prepaid
|
||||
return code
|
||||
|
||||
PrepaidSchema.pre('save', (next) ->
|
||||
@set('exhausted', @get('maxRedeemers') <= _.size(@get('redeemers')))
|
||||
|
@ -27,10 +43,17 @@ PrepaidSchema.pre('save', (next) ->
|
|||
)
|
||||
|
||||
PrepaidSchema.post 'init', (doc) ->
|
||||
doc.set('maxRedeemers', parseInt(doc.get('maxRedeemers')))
|
||||
doc.set('maxRedeemers', parseInt(doc.get('maxRedeemers') ? 0))
|
||||
if @get('type') is 'course'
|
||||
if not @get('startDate')
|
||||
@set('startDate', Prepaid.DEFAULT_START_DATE)
|
||||
if not @get('endDate')
|
||||
@set('endDate', Prepaid.DEFAULT_END_DATE)
|
||||
|
||||
PrepaidSchema.statics.postEditableProperties = [
|
||||
'creator', 'maxRedeemers', 'properties', 'type'
|
||||
'creator', 'maxRedeemers', 'properties', 'type', 'startDate', 'endDate'
|
||||
]
|
||||
PrepaidSchema.statics.editableProperties = []
|
||||
PrepaidSchema.statics.jsonSchema = jsonSchema
|
||||
|
||||
module.exports = Prepaid = mongoose.model('prepaid', PrepaidSchema)
|
||||
|
|
|
@ -35,6 +35,7 @@ UserSchema.index({'siteref': 1}, {name: 'siteref index', sparse: true})
|
|||
UserSchema.index({'schoolName': 1}, {name: 'schoolName index', sparse: true})
|
||||
UserSchema.index({'country': 1}, {name: 'country index', sparse: true})
|
||||
UserSchema.index({'role': 1}, {name: 'role index', sparse: true})
|
||||
UserSchema.index({'coursePrepaid._id': 1}, {name: 'course prepaid id index', sparse: true})
|
||||
|
||||
UserSchema.post('init', ->
|
||||
@set('anonymous', false) if @get('email')
|
||||
|
@ -106,6 +107,15 @@ UserSchema.methods.trackActivity = (activityName, increment) ->
|
|||
activity[activityName].last = now
|
||||
@set 'activity', activity
|
||||
activity
|
||||
|
||||
UserSchema.statics.search = (term, done) ->
|
||||
utils = require '../lib/utils'
|
||||
if utils.isID(term)
|
||||
query = {_id: mongoose.Types.ObjectId(term)}
|
||||
else
|
||||
term = term.toLowerCase()
|
||||
query = $or: [{nameLower: term}, {emailLower: term}]
|
||||
return User.findOne(query).exec(done)
|
||||
|
||||
emailNameMap =
|
||||
generalNews: 'announcement'
|
||||
|
@ -291,6 +301,12 @@ UserSchema.methods.level = ->
|
|||
a = 5
|
||||
b = c = 100
|
||||
if xp > 0 then Math.floor(a * Math.log((1 / b) * (xp + c))) + 1 else 1
|
||||
|
||||
UserSchema.methods.isEnrolled = ->
|
||||
coursePrepaid = @get('coursePrepaid')
|
||||
return false unless coursePrepaid
|
||||
return true unless coursePrepaid.endDate
|
||||
return coursePrepaid.endDate > new Date().toISOString()
|
||||
|
||||
UserSchema.statics.saveActiveUser = (id, event, done=null) ->
|
||||
# TODO: Disabling this until we know why our app servers CPU grows out of control.
|
||||
|
@ -350,9 +366,18 @@ UserSchema.post 'save', (doc) ->
|
|||
doc.newsSubsChanged = not _.isEqual(_.pick(doc.get('emails'), mail.NEWS_GROUPS), _.pick(doc.startingEmails, mail.NEWS_GROUPS))
|
||||
UserSchema.statics.updateServiceSettings(doc)
|
||||
|
||||
|
||||
UserSchema.post 'init', (doc) ->
|
||||
doc.wasTeacher = doc.isTeacher()
|
||||
doc.startingEmails = _.cloneDeep(doc.get('emails'))
|
||||
if @get('coursePrepaidID') and not @get('coursePrepaid')
|
||||
Prepaid = require './Prepaid'
|
||||
@set('coursePrepaid', {
|
||||
_id: @get('coursePrepaidID')
|
||||
startDate: Prepaid.DEFAULT_START_DATE
|
||||
endDate: Prepaid.DEFAULT_END_DATE
|
||||
})
|
||||
@set('coursePrepaidID', undefined)
|
||||
|
||||
UserSchema.statics.hashPassword = (password) ->
|
||||
password = password.toLowerCase()
|
||||
|
@ -370,7 +395,7 @@ UserSchema.statics.privateProperties = [
|
|||
'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID',
|
||||
'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement',
|
||||
'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID', 'chinaVersion', 'country',
|
||||
'schoolName', 'ageRange', 'role'
|
||||
'schoolName', 'ageRange', 'role', 'enrollmentRequestSent'
|
||||
]
|
||||
UserSchema.statics.jsonSchema = jsonschema
|
||||
UserSchema.statics.editableProperties = [
|
||||
|
@ -378,7 +403,8 @@ UserSchema.statics.editableProperties = [
|
|||
'firstName', 'lastName', 'gender', 'ageRange', 'facebookID', 'gplusID', 'emails',
|
||||
'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage',
|
||||
'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile', 'savedEmployerFilterAlerts',
|
||||
'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName', 'role', 'birthday'
|
||||
'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName', 'role', 'birthday',
|
||||
'enrollmentRequestSent'
|
||||
]
|
||||
|
||||
UserSchema.statics.serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset', 'lastIP']
|
||||
|
|
|
@ -52,7 +52,11 @@ createMailContext = (req, done) ->
|
|||
email_data:
|
||||
subject: "[CodeCombat] #{subject ? ('Feedback - ' + (sender or user.get('email')))}"
|
||||
content: content
|
||||
if recipientID and (user.isAdmin() or ('employer' in (user.get('permissions') ? [])))
|
||||
if recipientID is 'schools@codecombat.com'
|
||||
context.recipient.address = 'schools@codecombat.com'
|
||||
req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop)
|
||||
done context
|
||||
else if recipientID and (user.isAdmin() or ('employer' in (user.get('permissions') ? [])))
|
||||
User.findById(recipientID, 'email').exec (err, document) ->
|
||||
if err
|
||||
log.error "Error looking up recipient to email from #{recipientID}: #{err}" if err
|
||||
|
|
|
@ -72,6 +72,7 @@ module.exports.setup = (app) ->
|
|||
app.get('/db/course/:handle', mw.rest.getByHandle(Course))
|
||||
app.get('/db/course/:handle/levels/:levelOriginal/next', mw.courses.fetchNextLevel)
|
||||
|
||||
app.get('/db/course_instance/-/recent', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchRecent)
|
||||
app.get('/db/course_instance/:handle/levels/:levelOriginal/next', mw.courseInstances.fetchNextLevel)
|
||||
app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers)
|
||||
app.get('/db/course_instance/:handle/classroom', mw.auth.checkLoggedIn(), mw.courseInstances.fetchClassroom)
|
||||
|
@ -84,6 +85,13 @@ module.exports.setup = (app) ->
|
|||
app.put('/db/user/-/remain-teacher', mw.users.remainTeacher)
|
||||
app.post('/db/user/:userID/request-verify-email', mw.users.sendVerificationEmail)
|
||||
app.post('/db/user/:userID/verify/:verificationCode', mw.users.verifyEmailAddress) # TODO: Finalize URL scheme
|
||||
|
||||
app.get('/db/level/:handle/session', mw.auth.checkHasUser(), mw.levels.upsertSession)
|
||||
|
||||
app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator)
|
||||
app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post)
|
||||
app.post('/db/prepaid/:handle/redeemers', mw.prepaids.redeem)
|
||||
app.delete('/db/prepaid/:handle/redeemers', mw.prepaids.revoke)
|
||||
|
||||
app.get '/db/products', require('./db/product').get
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ if (database.generateMongoConnectionString() !== dbString) {
|
|||
throw Error('Stopping server tests because db connection string was not as expected.');
|
||||
}
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 120; // for long Stripe tests
|
||||
//jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 120; // for long Stripe tests
|
||||
require('../server/common'); // Make sure global testing functions are set up
|
||||
|
||||
var initialized = false;
|
||||
|
@ -93,4 +93,4 @@ beforeEach(function(done) {
|
|||
initialized = true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
25
spec/server/functional/contact.spec.coffee
Normal file
25
spec/server/functional/contact.spec.coffee
Normal file
|
@ -0,0 +1,25 @@
|
|||
utils = require '../utils'
|
||||
sendwithus = require '../../../server/sendwithus'
|
||||
request = require '../request'
|
||||
User = require '../../../server/models/User'
|
||||
|
||||
describe 'POST /contact', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
spyOn(sendwithus.api, 'send')
|
||||
@teacher = yield utils.initUser({role: 'teacher'})
|
||||
yield utils.loginUser(@teacher)
|
||||
done()
|
||||
|
||||
describe 'when recipientID is "schools@codecombat.com"', ->
|
||||
it 'sends to that email', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync({url: getURL('/contact'), json: {
|
||||
sender: 'some@email.com'
|
||||
message: 'A message'
|
||||
recipientID: 'schools@codecombat.com'
|
||||
}})
|
||||
expect(sendwithus.api.send).toHaveBeenCalled()
|
||||
user = yield User.findById(@teacher.id)
|
||||
yield new Promise((resolve) -> setTimeout(resolve, 10))
|
||||
expect(user.get('enrollmentRequestSent')).toBe(true)
|
||||
done()
|
|
@ -11,6 +11,7 @@ Campaign = require '../../../server/models/Campaign'
|
|||
Level = require '../../../server/models/Level'
|
||||
Prepaid = require '../../../server/models/Prepaid'
|
||||
request = require '../request'
|
||||
moment = require 'moment'
|
||||
|
||||
courseFixture = {
|
||||
name: 'Unnamed course'
|
||||
|
@ -101,27 +102,19 @@ describe 'POST /db/course_instance', ->
|
|||
describe 'POST /db/course_instance/:id/members', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
utils.clearModels([CourseInstance, Course, User, Classroom, Prepaid])
|
||||
yield utils.clearModels([CourseInstance, Course, User, Classroom, Prepaid, Campaign, Level])
|
||||
@teacher = yield utils.initUser({role: 'teacher'})
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
@level = yield utils.makeLevel({type: 'course'})
|
||||
@campaign = yield utils.makeCampaign({}, {levels: [@level]})
|
||||
@course = yield utils.makeCourse({free: true}, {campaign: @campaign})
|
||||
@student = yield utils.initUser({role: 'student'})
|
||||
@prepaid = yield utils.makePrepaid({creator: @teacher.id})
|
||||
members = [@student]
|
||||
yield utils.loginUser(@teacher)
|
||||
courseData = _.extend({free: true}, courseFixture)
|
||||
@course = yield new Course(courseData).save()
|
||||
classroomData = _.extend({ownerID: @teacher._id}, classroomFixture)
|
||||
@classroom = yield new Classroom(classroomData).save()
|
||||
url = getURL('/db/course_instance')
|
||||
data = {
|
||||
name: 'Some Name'
|
||||
courseID: @course.id
|
||||
classroomID: @classroom.id
|
||||
}
|
||||
[res, body] = yield request.postAsync {uri: url, json: data}
|
||||
@courseInstance = yield CourseInstance.findById res.body._id
|
||||
@student = yield utils.initUser()
|
||||
@prepaid = yield new Prepaid({
|
||||
type: 'course'
|
||||
maxRedeemers: 10
|
||||
redeemers: []
|
||||
}).save()
|
||||
@classroom = yield utils.makeClassroom({aceConfig: { language: 'javascript' }}, { members })
|
||||
@courseInstance = yield utils.makeCourseInstance({}, { @course, @classroom })
|
||||
done()
|
||||
|
||||
it 'adds an array of members to the given CourseInstance', utils.wrap (done) ->
|
||||
|
@ -135,8 +128,6 @@ describe 'POST /db/course_instance/:id/members', ->
|
|||
done()
|
||||
|
||||
it 'adds a member to the given CourseInstance', utils.wrap (done) ->
|
||||
@classroom.set('members', [@student._id])
|
||||
yield @classroom.save()
|
||||
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
||||
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
@ -145,8 +136,6 @@ describe 'POST /db/course_instance/:id/members', ->
|
|||
done()
|
||||
|
||||
it 'adds the CourseInstance id to the user', utils.wrap (done) ->
|
||||
@classroom.set('members', [@student._id])
|
||||
yield @classroom.save()
|
||||
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
||||
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
||||
user = yield User.findById(@student.id)
|
||||
|
@ -154,14 +143,14 @@ describe 'POST /db/course_instance/:id/members', ->
|
|||
done()
|
||||
|
||||
it 'return 403 if the member is not in the classroom', utils.wrap (done) ->
|
||||
@classroom.set('members', [])
|
||||
yield @classroom.save()
|
||||
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
||||
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'returns 403 if the user does not own the course instance and is not adding self', utils.wrap (done) ->
|
||||
@classroom.set('members', [@student._id])
|
||||
yield @classroom.save()
|
||||
otherUser = yield utils.initUser()
|
||||
yield utils.loginUser(otherUser)
|
||||
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
||||
|
@ -171,9 +160,7 @@ describe 'POST /db/course_instance/:id/members', ->
|
|||
|
||||
it 'returns 200 if the user is a member of the classroom and is adding self', ->
|
||||
|
||||
it 'return 402 if the course is not free and the user is not in a prepaid', utils.wrap (done) ->
|
||||
@classroom.set('members', [@student._id])
|
||||
yield @classroom.save()
|
||||
it 'return 402 if the course is not free and the user is not enrolled', utils.wrap (done) ->
|
||||
@course.set('free', false)
|
||||
yield @course.save()
|
||||
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
||||
|
@ -181,9 +168,17 @@ describe 'POST /db/course_instance/:id/members', ->
|
|||
expect(res.statusCode).toBe(402)
|
||||
done()
|
||||
|
||||
it 'works if the course is not free and the user is in a prepaid', utils.wrap (done) ->
|
||||
@classroom.set('members', [@student._id])
|
||||
yield @classroom.save()
|
||||
it 'works if the course is not free and the user is enrolled', utils.wrap (done) ->
|
||||
@course.set('free', false)
|
||||
yield @course.save()
|
||||
@student.set('coursePrepaid', _.pick(@prepaid.toObject(), '_id', 'startDate', 'endDate'))
|
||||
yield @student.save()
|
||||
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
||||
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
done()
|
||||
|
||||
it 'works if the course is not free and the user is enrolled but is not migrated', utils.wrap (done) ->
|
||||
@course.set('free', false)
|
||||
yield @course.save()
|
||||
@student.set('coursePrepaidID', @prepaid._id)
|
||||
|
@ -365,3 +360,56 @@ describe 'GET /db/course_instance/:handle/classroom', ->
|
|||
[res, body] = yield request.getAsync(@url, {json: true})
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
describe 'GET /db/course_instance/-/recent', ->
|
||||
|
||||
url = getURL('/db/course_instance/-/recent')
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([CourseInstance, Course, User, Classroom, Prepaid, Campaign, Level])
|
||||
@teacher = yield utils.initUser({role: 'teacher'})
|
||||
@admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(@admin)
|
||||
@campaign = yield utils.makeCampaign()
|
||||
@course = yield utils.makeCourse({free: true}, {campaign: @campaign})
|
||||
@student = yield utils.initUser({role: 'student'})
|
||||
@prepaid = yield utils.makePrepaid({creator: @teacher.id})
|
||||
members = [@student]
|
||||
yield utils.loginUser(@teacher)
|
||||
@classroom = yield utils.makeClassroom({aceConfig: { language: 'javascript' }}, { members })
|
||||
@courseInstance = yield utils.makeCourseInstance({}, { @course, @classroom, members })
|
||||
[res, body] = yield request.postAsync({url: getURL("/db/prepaid/#{@prepaid.id}/redeemers"), json: { userID: @student.id} })
|
||||
yield utils.loginUser(@admin)
|
||||
done()
|
||||
|
||||
it 'returns all non-HoC course instances and their related users and prepaids', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync(url, { json: true })
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.body.courseInstances[0]._id).toBe(@courseInstance.id)
|
||||
expect(res.body.students[0]._id).toBe(@student.id)
|
||||
expect(res.body.prepaids[0]._id).toBe(@prepaid.id)
|
||||
done()
|
||||
|
||||
it 'returns course instances within a specified range', utils.wrap (done) ->
|
||||
startDay = moment().subtract(1, 'day').format('YYYY-MM-DD')
|
||||
endDay = moment().add(1, 'day').format('YYYY-MM-DD')
|
||||
[res, body] = yield request.getAsync(url, { json: { startDay, endDay } })
|
||||
expect(res.body.courseInstances.length).toBe(1)
|
||||
|
||||
startDay = moment().add(1, 'day').format('YYYY-MM-DD')
|
||||
endDay = moment().add(2, 'day').format('YYYY-MM-DD')
|
||||
[res, body] = yield request.getAsync(url, { json: { startDay, endDay } })
|
||||
expect(res.body.courseInstances.length).toBe(0)
|
||||
|
||||
startDay = moment().subtract(2, 'day').format('YYYY-MM-DD')
|
||||
endDay = moment().subtract(1, 'day').format('YYYY-MM-DD')
|
||||
[res, body] = yield request.getAsync(url, { json: { startDay, endDay } })
|
||||
expect(res.body.courseInstances.length).toBe(0)
|
||||
|
||||
done()
|
||||
|
||||
it 'returns 403 if not an admin', utils.wrap (done) ->
|
||||
yield utils.loginUser(@teacher)
|
||||
[res, body] = yield request.getAsync(url, { json: true })
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
|
|
@ -6,6 +6,8 @@ CourseInstance = require '../../../server/models/CourseInstance'
|
|||
Level = require '../../../server/models/Level'
|
||||
User = require '../../../server/models/User'
|
||||
request = require '../request'
|
||||
utils = require '../utils'
|
||||
moment = require 'moment'
|
||||
|
||||
describe 'Level', ->
|
||||
|
||||
|
@ -38,85 +40,99 @@ describe 'Level', ->
|
|||
done()
|
||||
|
||||
|
||||
describe 'GET /db/level/<id>/session', ->
|
||||
describe 'GET /db/level/:handle/session', ->
|
||||
|
||||
describe 'when level is a course level', ->
|
||||
|
||||
levelID = null
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Campaign, Course, CourseInstance, Level, User])
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
@level = yield utils.makeLevel({type: 'course'})
|
||||
@campaign = yield utils.makeCampaign({}, {levels: [@level]})
|
||||
@course = yield utils.makeCourse({free: true}, {campaign: @campaign})
|
||||
@student = yield utils.initUser({role: 'student'})
|
||||
members = [@student]
|
||||
teacher = yield utils.initUser({role: 'teacher'})
|
||||
yield utils.loginUser(teacher)
|
||||
@classroom = yield utils.makeClassroom({aceConfig: { language: 'javascript' }}, { members })
|
||||
@courseInstance = yield utils.makeCourseInstance({}, { @course, @classroom, members })
|
||||
@url = getURL("/db/level/#{@level.id}/session")
|
||||
yield utils.loginUser(@student)
|
||||
done()
|
||||
|
||||
it 'creates a new session if the user is in a course with that level', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: @url, json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.codeLanguage).toBe('javascript')
|
||||
done()
|
||||
|
||||
it 'sets up a course instance', (done) ->
|
||||
|
||||
clearModels [Campaign, Course, CourseInstance, Level, User], (err) ->
|
||||
|
||||
loginAdmin (admin) ->
|
||||
|
||||
url = getURL('/db/level')
|
||||
body =
|
||||
name: 'Course Level'
|
||||
type: 'course'
|
||||
permissions: simplePermissions
|
||||
|
||||
request.post {uri: url, json: body }, (err, res, level) ->
|
||||
levelID = level._id
|
||||
|
||||
url = getURL('/db/campaign')
|
||||
body =
|
||||
name: 'Course Campaign'
|
||||
levels: {}
|
||||
body.levels[level.original] = { 'original': level.original }
|
||||
|
||||
request.post { uri: url, json: body }, (err, res, campaign) ->
|
||||
|
||||
course = new Course({
|
||||
name: 'Test Course'
|
||||
campaignID: ObjectId(campaign._id)
|
||||
})
|
||||
|
||||
course.save (err) ->
|
||||
|
||||
expect(err).toBeNull()
|
||||
|
||||
loginJoe (joe) ->
|
||||
|
||||
classroom = new Classroom({
|
||||
name: 'Test Classroom'
|
||||
members: [ joe.get('_id') ]
|
||||
aceConfig: { language: 'javascript' }
|
||||
})
|
||||
|
||||
classroom.save (err, classroom) ->
|
||||
|
||||
expect(err).toBeNull()
|
||||
|
||||
courseInstance = new CourseInstance({
|
||||
name: 'Course Instance'
|
||||
members: [
|
||||
joe.get('_id')
|
||||
]
|
||||
courseID: ObjectId(course.id)
|
||||
classroomID: ObjectId(classroom.id)
|
||||
})
|
||||
it 'returns 402 if the user is not in a course with that level', utils.wrap (done) ->
|
||||
otherStudent = yield utils.initUser({role: 'student'})
|
||||
yield utils.loginUser(otherStudent)
|
||||
[res, body] = yield request.getAsync({ uri: @url, json: true })
|
||||
expect(res.statusCode).toBe(402)
|
||||
expect(res.body.message).toBe('You must be in a course which includes this level to play it')
|
||||
done()
|
||||
|
||||
describe 'when the course is not free', ->
|
||||
|
||||
courseInstance.save (err) ->
|
||||
|
||||
expect(err).toBeNull()
|
||||
done()
|
||||
beforeEach utils.wrap (done) ->
|
||||
@course.set({free: false})
|
||||
yield @course.save()
|
||||
done()
|
||||
|
||||
it 'returns 402 if the user is not enrolled', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync({ uri: @url, json: true })
|
||||
expect(res.statusCode).toBe(402)
|
||||
expect(res.body.message).toBe('You must be enrolled to access this content')
|
||||
done()
|
||||
|
||||
it 'creates the session if the user is enrolled', utils.wrap (done) ->
|
||||
@student.set({
|
||||
coursePrepaid: {
|
||||
_id: {}
|
||||
startDate: moment().subtract(1, 'month').toISOString()
|
||||
endDate: moment().add(1, 'month').toISOString()
|
||||
}
|
||||
})
|
||||
@student.save()
|
||||
[res, body] = yield request.getAsync({ uri: @url, json: true })
|
||||
expect(res.statusCode).toBe(200)
|
||||
done()
|
||||
|
||||
it 'creates a new session if the user is in a course with that level', (done) ->
|
||||
loginJoe (joe) ->
|
||||
|
||||
url = getURL("/db/level/#{levelID}/session")
|
||||
|
||||
request.get { uri: url, json: true }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.codeLanguage).toBe('javascript')
|
||||
done()
|
||||
|
||||
it 'does not create a new session if the user is not in a course with that level', (done) ->
|
||||
loginSam (sam) ->
|
||||
|
||||
url = getURL("/db/level/#{levelID}/session")
|
||||
|
||||
request.get { uri: url }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(402)
|
||||
done()
|
||||
it 'returns 402 if the user\'s enrollment is expired', utils.wrap (done) ->
|
||||
@student.set({
|
||||
coursePrepaid: {
|
||||
_id: {}
|
||||
startDate: moment().subtract(2, 'month').toISOString()
|
||||
endDate: moment().subtract(1, 'month').toISOString()
|
||||
}
|
||||
})
|
||||
@student.save()
|
||||
[res, body] = yield request.getAsync({ uri: @url, json: true })
|
||||
expect(res.statusCode).toBe(402)
|
||||
expect(res.body.message).toBe('You must be enrolled to access this content')
|
||||
done()
|
||||
|
||||
|
||||
describe 'when the level is NOT a course level', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Level, User])
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
@level = yield utils.makeLevel()
|
||||
|
||||
@player = yield utils.initUser()
|
||||
yield utils.loginUser(@player)
|
||||
@url = getURL("/db/level/#{@level.id}/session")
|
||||
done()
|
||||
|
||||
it 'idempotently creates and returns a session for that level', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: @url, json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
sessionID = body._id
|
||||
[res, body] = yield request.getAsync { uri: @url, json: true }
|
||||
expect(body._id).toBe(sessionID)
|
||||
done()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require '../common'
|
||||
Level = require '../../../server/models/Level'
|
||||
LevelComponent = require '../../../server/models/LevelComponent'
|
||||
User = require '../../../server/models/User'
|
||||
request = require '../request'
|
||||
|
||||
describe 'LevelComponent', ->
|
||||
|
@ -20,7 +21,7 @@ describe 'LevelComponent', ->
|
|||
url = getURL('/db/level.component')
|
||||
|
||||
it 'preparing test : clears things first.', (done) ->
|
||||
clearModels [Level, LevelComponent], (err) ->
|
||||
clearModels [Level, LevelComponent, User], (err) ->
|
||||
expect(err).toBeNull()
|
||||
done()
|
||||
|
||||
|
|
|
@ -13,9 +13,248 @@ Course = require '../../../server/models/Course'
|
|||
CourseInstance = require '../../../server/models/CourseInstance'
|
||||
request = require '../request'
|
||||
|
||||
describe 'POST /db/prepaid', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User, Prepaid])
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
done()
|
||||
|
||||
it 'creates a new prepaid for type "course"', utils.wrap (done) ->
|
||||
user = yield utils.initUser()
|
||||
[res, body] = yield request.postAsync({url: getURL('/db/prepaid'), json: {
|
||||
type: 'course'
|
||||
creator: user.id
|
||||
}})
|
||||
expect(res.statusCode).toBe(201)
|
||||
prepaid = yield Prepaid.findById(res.body._id)
|
||||
expect(prepaid).toBeDefined()
|
||||
expect(prepaid.get('creator').equals(user._id)).toBe(true)
|
||||
expect(prepaid.get('code')).toBeDefined()
|
||||
done()
|
||||
|
||||
it 'does not work for non-admins', utils.wrap (done) ->
|
||||
user = yield utils.initUser()
|
||||
yield utils.loginUser(user)
|
||||
[res, body] = yield request.postAsync({url: getURL('/db/prepaid'), json: {
|
||||
type: 'course'
|
||||
creator: user.id
|
||||
}})
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'accepts start and end dates', utils.wrap (done) ->
|
||||
user = yield utils.initUser()
|
||||
[res, body] = yield request.postAsync({url: getURL('/db/prepaid'), json: {
|
||||
type: 'course'
|
||||
creator: user.id
|
||||
startDate: new Date().toISOString(2001,1,1)
|
||||
endDate: new Date().toISOString(2010,1,1)
|
||||
}})
|
||||
expect(res.statusCode).toBe(201)
|
||||
prepaid = yield Prepaid.findById(res.body._id)
|
||||
expect(prepaid).toBeDefined()
|
||||
expect(prepaid.get('startDate')).toBeDefined()
|
||||
expect(prepaid.get('endDate')).toBeDefined()
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/prepaid/:handle', ->
|
||||
it 'populates startDate and endDate with default values', utils.wrap (done) ->
|
||||
prepaid = new Prepaid({type: 'course' })
|
||||
yield prepaid.save()
|
||||
[res, body] = yield request.getAsync({url: getURL("/db/prepaid/#{prepaid.id}"), json: true})
|
||||
expect(body.endDate).toBe(Prepaid.DEFAULT_END_DATE)
|
||||
expect(body.startDate).toBe(Prepaid.DEFAULT_START_DATE)
|
||||
done()
|
||||
|
||||
|
||||
describe 'POST /db/prepaid/:handle/redeemers', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Course, CourseInstance, Payment, Prepaid, User])
|
||||
@teacher = yield utils.initUser({role: 'teacher'})
|
||||
@admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(@admin)
|
||||
@prepaid = yield utils.makePrepaid({ creator: @teacher.id })
|
||||
yield utils.loginUser(@teacher)
|
||||
@student = yield utils.initUser()
|
||||
@url = getURL("/db/prepaid/#{@prepaid.id}/redeemers")
|
||||
done()
|
||||
|
||||
it 'adds a given user to the redeemers property', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(body.redeemers.length).toBe(1)
|
||||
expect(res.statusCode).toBe(201)
|
||||
prepaid = yield Prepaid.findById(body._id)
|
||||
expect(prepaid.get('redeemers').length).toBe(1)
|
||||
@student = yield User.findById(@student.id)
|
||||
expect(@student.get('coursePrepaid')._id.equals(@prepaid._id)).toBe(true)
|
||||
expect(@student.get('role')).toBe('student')
|
||||
done()
|
||||
|
||||
it 'returns 403 if maxRedeemers is reached', utils.wrap (done) ->
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
prepaid = yield utils.makePrepaid({ creator: @teacher.id, maxRedeemers: 0 })
|
||||
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
|
||||
yield utils.loginUser(@teacher)
|
||||
[res, body] = yield request.postAsync({uri: url, json: { userID: @student.id } })
|
||||
expect(res.statusCode).toBe(403)
|
||||
expect(res.body.message).toBe('This prepaid is exhausted')
|
||||
done()
|
||||
|
||||
it 'returns 403 unless the user is the "creator"', utils.wrap (done) ->
|
||||
@otherTeacher = yield utils.initUser({role: 'teacher'})
|
||||
yield utils.loginUser(@otherTeacher)
|
||||
[res, body] = yield request.postAsync({uri: @url, json: { userID: @student.id } })
|
||||
expect(res.statusCode).toBe(403)
|
||||
expect(res.body.message).toBe('You may not redeem enrollments from this prepaid')
|
||||
done()
|
||||
|
||||
it 'returns 403 if the prepaid is expired', utils.wrap (done) ->
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
prepaid = yield utils.makePrepaid({ creator: @teacher.id, endDate: moment().subtract(1, 'month').toISOString() })
|
||||
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
|
||||
yield utils.loginUser(@teacher)
|
||||
[res, body] = yield request.postAsync({uri: url, json: { userID: @student.id } })
|
||||
expect(res.statusCode).toBe(403)
|
||||
expect(res.body.message).toBe('This prepaid is expired')
|
||||
done()
|
||||
|
||||
it 'is idempotent across prepaids collection', utils.wrap (done) ->
|
||||
student = yield utils.initUser({ coursePrepaid: { _id: new Prepaid()._id } })
|
||||
[res, body] = yield request.postAsync({uri: @url, json: { userID: student.id } })
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.redeemers.length).toBe(0)
|
||||
done()
|
||||
|
||||
it 'is idempotent to itself', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync({uri: @url, json: { userID: @student.id } })
|
||||
expect(body.redeemers?.length).toBe(1)
|
||||
expect(res.statusCode).toBe(201)
|
||||
[res, body] = yield request.postAsync({uri: @url, json: { userID: @student.id } })
|
||||
expect(body.redeemers?.length).toBe(1)
|
||||
expect(res.statusCode).toBe(200)
|
||||
prepaid = yield Prepaid.findById(body._id)
|
||||
expect(prepaid.get('redeemers').length).toBe(1)
|
||||
student = yield User.findById(@student.id)
|
||||
expect(student.get('coursePrepaid')._id.equals(@prepaid._id)).toBe(true)
|
||||
done()
|
||||
|
||||
it 'updates the user if their enrollment is expired', utils.wrap (done) ->
|
||||
yield utils.loginUser(@admin)
|
||||
prepaid = yield utils.makePrepaid({
|
||||
creator: @teacher.id
|
||||
startDate: moment().subtract(2, 'month').toISOString()
|
||||
endDate: moment().subtract(1, 'month').toISOString()
|
||||
})
|
||||
@student.set('coursePrepaid', _.pick(prepaid.toObject(), '_id', 'startDate', 'endDate'))
|
||||
yield @student.save()
|
||||
yield utils.loginUser(@teacher)
|
||||
[res, body] = yield request.postAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(body.redeemers.length).toBe(1)
|
||||
expect(res.statusCode).toBe(201)
|
||||
student = yield User.findById(@student.id)
|
||||
expect(student.get('coursePrepaid')._id.equals(@prepaid._id)).toBe(true)
|
||||
done()
|
||||
|
||||
|
||||
describe 'DELETE /db/prepaid/:handle/redeemers', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Course, CourseInstance, Payment, Prepaid, User])
|
||||
@teacher = yield utils.initUser({role: 'teacher'})
|
||||
@admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(@admin)
|
||||
@prepaid = yield utils.makePrepaid({ creator: @teacher.id })
|
||||
yield utils.loginUser(@teacher)
|
||||
@student = yield utils.initUser()
|
||||
@url = getURL("/db/prepaid/#{@prepaid.id}/redeemers")
|
||||
[res, body] = yield request.postAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(res.statusCode).toBe(201)
|
||||
done()
|
||||
|
||||
it 'removes a given user to the redeemers property', utils.wrap (done) ->
|
||||
prepaid = yield Prepaid.findById(@prepaid.id)
|
||||
expect(prepaid.get('redeemers').length).toBe(1)
|
||||
[res, body] = yield request.delAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(body.redeemers.length).toBe(0)
|
||||
expect(res.statusCode).toBe(200)
|
||||
prepaid = yield Prepaid.findById(body._id)
|
||||
expect(prepaid.get('redeemers').length).toBe(0)
|
||||
student = yield User.findById(@student.id)
|
||||
expect(student.get('coursePrepaid')).toBeUndefined()
|
||||
done()
|
||||
|
||||
it 'works if the user has not migrated from coursePrepaidID to coursePrepaid', utils.wrap (done) ->
|
||||
yield @student.update({
|
||||
$set: { coursePrepaidID: @prepaid._id }
|
||||
$unset: { coursePrepaid: '' }
|
||||
})
|
||||
yield @student.save()
|
||||
[res, body] = yield request.delAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(body.redeemers.length).toBe(0)
|
||||
expect(res.statusCode).toBe(200)
|
||||
prepaid = yield Prepaid.findById(body._id)
|
||||
expect(prepaid.get('redeemers').length).toBe(0)
|
||||
student = yield User.findById(@student.id)
|
||||
expect(student.get('coursePrepaid')).toBeUndefined()
|
||||
expect(student.get('coursePrepaidID')).toBeUndefined()
|
||||
done()
|
||||
|
||||
it 'returns 403 unless the user is the "creator"', utils.wrap (done) ->
|
||||
otherTeacher = yield utils.initUser({role: 'teacher'})
|
||||
yield utils.loginUser(otherTeacher)
|
||||
[res, body] = yield request.delAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'returns 422 unless the target user is in "redeemers"', utils.wrap (done) ->
|
||||
otherStudent = yield utils.initUser({role: 'student'})
|
||||
[res, body] = yield request.delAsync {uri: @url, json: { userID: otherStudent.id } }
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/prepaid?creator=:id', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Course, CourseInstance, Payment, Prepaid, User])
|
||||
@teacher = yield utils.initUser({role: 'teacher'})
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
@prepaid = yield utils.makePrepaid({ creator: @teacher.id })
|
||||
@otherPrepaid = yield utils.makePrepaid({ creator: admin.id })
|
||||
@expiredPrepaid = yield utils.makePrepaid({ creator: @teacher.id, endDate: moment().subtract(1, 'month').toISOString() })
|
||||
@unmigratedPrepaid = yield utils.makePrepaid({ creator: @teacher.id })
|
||||
yield @unmigratedPrepaid.update({$unset: { endDate: '', startDate: '' }})
|
||||
yield utils.loginUser(@teacher)
|
||||
done()
|
||||
|
||||
it 'return all prepaids for the creator', utils.wrap (done) ->
|
||||
url = getURL("/db/prepaid?creator=#{@teacher.id}")
|
||||
[res, body] = yield request.getAsync({uri: url, json: true})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.body.length).toEqual(3)
|
||||
if _.any((prepaid._id is @otherPrepaid.id for prepaid in res.body))
|
||||
fail('Found the admin prepaid in response')
|
||||
for prepaid in res.body
|
||||
unless prepaid.startDate and prepaid.endDate
|
||||
fail('All prepaids should have start and end dates')
|
||||
expect(res.body[0]._id).toBe(@prepaid.id)
|
||||
done()
|
||||
|
||||
it 'returns 403 if the user tries to view another user\'s prepaids', utils.wrap (done) ->
|
||||
anotherUser = yield utils.initUser()
|
||||
url = getURL("/db/prepaid?creator=#{anotherUser.id}")
|
||||
[res, body] = yield request.getAsync({uri: url, json: true})
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
|
||||
describe '/db/prepaid', ->
|
||||
prepaidURL = getURL('/db/prepaid')
|
||||
prepaidCreateURL = getURL('/db/prepaid/-/create')
|
||||
|
||||
headers = {'X-Change-Plan': 'true'}
|
||||
|
||||
|
@ -53,257 +292,6 @@ describe '/db/prepaid', ->
|
|||
throw err if err
|
||||
done()
|
||||
|
||||
describe 'POST /db/prepaid/<id>/redeemers', ->
|
||||
|
||||
it 'adds a given user to the redeemers property', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: 1,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
code: 0
|
||||
type: 'course'
|
||||
})
|
||||
prepaid.save (err, prepaid) ->
|
||||
otherUser = new User()
|
||||
otherUser.save (err, otherUser) ->
|
||||
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
|
||||
redeemer = { userID: otherUser.id }
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(body.redeemers.length).toBe(1)
|
||||
expect(res.statusCode).toBe(200)
|
||||
prepaid = Prepaid.findById body._id, (err, prepaid) ->
|
||||
expect(err).toBeNull()
|
||||
expect(prepaid.get('redeemers').length).toBe(1)
|
||||
User.findById otherUser.id, (err, user) ->
|
||||
expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true)
|
||||
expect(user.get('role')).toBe('student')
|
||||
done()
|
||||
|
||||
it 'does not allow more redeemers than maxRedeemers', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: 0,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
code: 1
|
||||
type: 'course'
|
||||
})
|
||||
prepaid.save (err, prepaid) ->
|
||||
otherUser = new User()
|
||||
otherUser.save (err, otherUser) ->
|
||||
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
|
||||
redeemer = { userID: otherUser.id }
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'only allows the owner of the prepaid to add redeemers', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: 1000,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
code: 2
|
||||
type: 'course'
|
||||
})
|
||||
prepaid.save (err, prepaid) ->
|
||||
loginNewUser (user2) ->
|
||||
otherUser = new User()
|
||||
otherUser.save (err, otherUser) ->
|
||||
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
|
||||
redeemer = { userID: otherUser.id }
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'is idempotent across prepaids collection', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
otherUser = new User({
|
||||
'coursePrepaidID': new ObjectId()
|
||||
})
|
||||
otherUser.save (err, otherUser) ->
|
||||
prepaid1 = new Prepaid({
|
||||
redeemers: [{userID: otherUser.get('_id')}],
|
||||
code: 3
|
||||
type: 'course'
|
||||
})
|
||||
prepaid1.save (err, prepaid1) ->
|
||||
otherUser.set 'coursePrepaidID', prepaid1.id
|
||||
otherUser.save (err, otherUser) ->
|
||||
prepaid2 = new Prepaid({
|
||||
maxRedeemers: 10,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
code: 4
|
||||
type: 'course'
|
||||
})
|
||||
prepaid2.save (err, prepaid2) ->
|
||||
url = getURL("/db/prepaid/#{prepaid2.id}/redeemers")
|
||||
redeemer = { userID: otherUser.id }
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.redeemers.length).toBe(0)
|
||||
done()
|
||||
|
||||
it 'is idempotent to itself for a user other than the creator', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: 2,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
code: 0
|
||||
type: 'course'
|
||||
})
|
||||
prepaid.save (err, prepaid) ->
|
||||
otherUser = new User()
|
||||
otherUser.save (err, otherUser) ->
|
||||
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
|
||||
redeemer = { userID: otherUser.id }
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(body.redeemers?.length).toBe(1)
|
||||
expect(res.statusCode).toBe(200)
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(body.redeemers?.length).toBe(1)
|
||||
expect(res.statusCode).toBe(200)
|
||||
prepaid = Prepaid.findById body._id, (err, prepaid) ->
|
||||
expect(err).toBeNull()
|
||||
expect(prepaid.get('redeemers').length).toBe(1)
|
||||
User.findById otherUser.id, (err, user) ->
|
||||
expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true)
|
||||
done()
|
||||
|
||||
it 'is idempotent to itself for the creator', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: 2,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
code: 0
|
||||
type: 'course'
|
||||
})
|
||||
prepaid.save (err, prepaid) ->
|
||||
otherUser = new User()
|
||||
otherUser.save (err, otherUser) ->
|
||||
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
|
||||
redeemer = { userID: user1.id }
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(body.redeemers?.length).toBe(1)
|
||||
expect(res.statusCode).toBe(200)
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(body.redeemers?.length).toBe(1)
|
||||
expect(res.statusCode).toBe(200)
|
||||
prepaid = Prepaid.findById body._id, (err, prepaid) ->
|
||||
expect(err).toBeNull()
|
||||
expect(prepaid.get('redeemers').length).toBe(1)
|
||||
User.findById user1.id, (err, user) ->
|
||||
expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true)
|
||||
redeemer = { userID: otherUser.id }
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(body.redeemers?.length).toBe(2)
|
||||
expect(res.statusCode).toBe(200)
|
||||
done()
|
||||
|
||||
it 'return terminal prepaids', (done) ->
|
||||
endDate = new Date()
|
||||
endDate.setUTCMonth(endDate.getUTCMonth() + 2)
|
||||
loginNewUser (user1) ->
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: 500,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
type: 'course'
|
||||
properties:
|
||||
endDate: endDate
|
||||
})
|
||||
prepaid.save (err, prepaid) ->
|
||||
expect(err).toBeNull()
|
||||
url = getURL("/db/prepaid?creator=#{user1.id}")
|
||||
request.get {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
documents = JSON.parse(body)
|
||||
expect(documents.length).toEqual(1)
|
||||
return done() unless documents.length is 1
|
||||
expect(documents[0]?.properties?.endDate).toEqual(endDate.toISOString())
|
||||
done()
|
||||
|
||||
it 'do not return expired terminal prepaids', (done) ->
|
||||
endDate = new Date()
|
||||
endDate.setUTCMonth(endDate.getUTCMonth() - 1)
|
||||
loginNewUser (user1) ->
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: 500,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
type: 'course'
|
||||
properties:
|
||||
endDate: endDate
|
||||
})
|
||||
prepaid.save (err, prepaid) ->
|
||||
expect(err).toBeNull()
|
||||
url = getURL("/db/prepaid?creator=#{user1.id}")
|
||||
request.get {uri: url}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
documents = JSON.parse(body)
|
||||
expect(documents.length).toEqual(0)
|
||||
done()
|
||||
|
||||
it 'redeem terminal prepaids', (done) ->
|
||||
endDate = new Date()
|
||||
endDate.setUTCMonth(endDate.getUTCMonth() + 2)
|
||||
loginNewUser (user1) ->
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: 500,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
type: 'course'
|
||||
properties:
|
||||
endDate: endDate
|
||||
})
|
||||
prepaid.save (err, prepaid) ->
|
||||
expect(err).toBeNull()
|
||||
otherUser = new User()
|
||||
otherUser.save (err, otherUser) ->
|
||||
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
|
||||
redeemer = { userID: otherUser.id }
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(body.redeemers?.length).toBe(1)
|
||||
expect(res.statusCode).toBe(200)
|
||||
return done() unless res.statusCode is 200
|
||||
prepaid = Prepaid.findById body._id, (err, prepaid) ->
|
||||
expect(err).toBeNull()
|
||||
expect(prepaid.get('redeemers').length).toBe(1)
|
||||
User.findById otherUser.id, (err, user) ->
|
||||
expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true)
|
||||
done()
|
||||
|
||||
it 'do not redeem expired terminal prepaids', (done) ->
|
||||
endDate = new Date()
|
||||
endDate.setUTCMonth(endDate.getUTCMonth() - 1)
|
||||
loginNewUser (user1) ->
|
||||
prepaid = new Prepaid({
|
||||
maxRedeemers: 500,
|
||||
redeemers: [],
|
||||
creator: user1.get('_id')
|
||||
type: 'course'
|
||||
properties:
|
||||
endDate: endDate
|
||||
})
|
||||
prepaid.save (err, prepaid) ->
|
||||
expect(err).toBeNull()
|
||||
otherUser = new User()
|
||||
otherUser.save (err, otherUser) ->
|
||||
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
|
||||
redeemer = { userID: otherUser.id }
|
||||
request.post {uri: url, json: redeemer }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'Clear database', (done) ->
|
||||
clearModels [Course, CourseInstance, Payment, Prepaid, User], (err) ->
|
||||
throw err if err
|
||||
done()
|
||||
|
||||
it 'Anonymous creates prepaid code', (done) ->
|
||||
createPrepaid 'subscription', 1, 0, (err, res, body) ->
|
||||
expect(err).toBeNull()
|
||||
|
|
|
@ -3,6 +3,7 @@ utils = require '../utils'
|
|||
urlUser = '/db/user'
|
||||
User = require '../../../server/models/User'
|
||||
Classroom = require '../../../server/models/Classroom'
|
||||
Prepaid = require '../../../server/models/Prepaid'
|
||||
request = require '../request'
|
||||
|
||||
describe 'POST /db/user', ->
|
||||
|
@ -511,6 +512,18 @@ describe 'GET /db/user', ->
|
|||
# Add to the test case above an extra data check
|
||||
|
||||
xit 'can fetch another user with restricted fields'
|
||||
|
||||
|
||||
describe 'GET /db/user/:handle', ->
|
||||
it 'populates coursePrepaid from coursePrepaidID', utils.wrap (done) ->
|
||||
course = yield utils.makeCourse()
|
||||
user = yield utils.initUser({coursePrepaidID: course.id})
|
||||
[res, body] = yield request.getAsync({url: getURL("/db/user/#{user.id}"), json: true})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.body.coursePrepaid._id).toBe(course.id)
|
||||
expect(res.body.coursePrepaid.startDate).toBe(Prepaid.DEFAULT_START_DATE)
|
||||
done()
|
||||
|
||||
|
||||
describe 'DELETE /db/user', ->
|
||||
it 'can delete a user', utils.wrap (done) ->
|
||||
|
|
|
@ -6,6 +6,11 @@ User = require '../../server/models/User'
|
|||
Level = require '../../server/models/Level'
|
||||
Achievement = require '../../server/models/Achievement'
|
||||
Campaign = require '../../server/models/Campaign'
|
||||
Course = require '../../server/models/Course'
|
||||
Prepaid = require '../../server/models/Prepaid'
|
||||
Classroom = require '../../server/models/Classroom'
|
||||
CourseInstance = require '../../server/models/CourseInstance'
|
||||
moment = require 'moment'
|
||||
campaignSchema = require '../../app/schemas/models/campaign.schema'
|
||||
campaignLevelProperties = _.keys(campaignSchema.properties.levels.additionalProperties.properties)
|
||||
campaignAdjacentCampaignProperties = _.keys(campaignSchema.properties.adjacentCampaigns.additionalProperties.properties)
|
||||
|
@ -124,4 +129,60 @@ module.exports = mw =
|
|||
|
||||
request.post { uri: getURL('/db/campaign'), json: data }, (err, res) ->
|
||||
return done(err) if err
|
||||
Campaign.findById(res.body._id).exec done
|
||||
Campaign.findById(res.body._id).exec done
|
||||
|
||||
makeCourse: (data={}, sources={}) ->
|
||||
|
||||
if sources.campaign and not data.campaignID
|
||||
data.campaignID = sources.campaign._id
|
||||
|
||||
course = new Course(data)
|
||||
return course.save()
|
||||
|
||||
makePrepaid: Promise.promisify (data, sources, done) ->
|
||||
args = Array.from(arguments)
|
||||
[done, [data, sources]] = [args.pop(), args]
|
||||
|
||||
data = _.extend({}, {
|
||||
type: 'course'
|
||||
maxRedeemers: 9001
|
||||
endDate: moment().add(1, 'month').toISOString()
|
||||
startDate: new Date().toISOString()
|
||||
}, data)
|
||||
|
||||
request.post { uri: getURL('/db/prepaid'), json: data }, (err, res) ->
|
||||
return done(err) if err
|
||||
expect(res.statusCode).toBe(201)
|
||||
Prepaid.findById(res.body._id).exec done
|
||||
|
||||
makeClassroom: (data={}, sources={}) -> co ->
|
||||
data = _.extend({}, {
|
||||
name: _.uniqueId('Classroom ')
|
||||
}, data)
|
||||
|
||||
[res, body] = yield request.postAsync { uri: getURL('/db/classroom'), json: data }
|
||||
expect(res.statusCode).toBe(201)
|
||||
classroom = yield Classroom.findById(res.body._id)
|
||||
if sources.members
|
||||
classroom.set('members', _.map(sources.members, '_id'))
|
||||
yield classroom.save()
|
||||
return classroom
|
||||
|
||||
makeCourseInstance: (data={}, sources={}) -> co ->
|
||||
if sources.course and not data.courseID
|
||||
data.courseID = sources.course.id
|
||||
if sources.classroom and not data.classroomID
|
||||
data.classroomID = sources.classroom.id
|
||||
|
||||
[res, body] = yield request.postAsync({ uri: getURL('/db/course_instance'), json: data })
|
||||
expect(res.statusCode).toBe(200)
|
||||
courseInstance = yield CourseInstance.findById(res.body._id)
|
||||
if sources.members
|
||||
userIDs = _.map(sources.members, 'id')
|
||||
[res, body] = yield request.postAsync({
|
||||
url: getURL("/db/course_instance/#{courseInstance.id}/members")
|
||||
json: { userIDs: userIDs }
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
courseInstance = yield CourseInstance.findById(res.body._id)
|
||||
return courseInstance
|
||||
|
|
|
@ -9,6 +9,7 @@ Achievement = require 'models/Achievement'
|
|||
EarnedAchievement = require 'models/EarnedAchievement'
|
||||
ThangType = require 'models/ThangType'
|
||||
Users = require 'collections/Users'
|
||||
Prepaid = require 'models/Prepaid'
|
||||
|
||||
module.exports = {
|
||||
|
||||
|
@ -36,7 +37,7 @@ module.exports = {
|
|||
}, attrs)
|
||||
return new Level(attrs)
|
||||
|
||||
makeUser: (attrs) ->
|
||||
makeUser: (attrs, sources={}) ->
|
||||
_id = _.uniqueId('user_')
|
||||
attrs = _.extend({
|
||||
_id: _id
|
||||
|
@ -45,6 +46,10 @@ module.exports = {
|
|||
anonymous: false
|
||||
name: _.string.humanize(_id)
|
||||
}, attrs)
|
||||
|
||||
if sources.prepaid and not attrs.coursePrepaid
|
||||
attrs.coursePrepaid = sources.prepaid.pick('_id', 'startDate', 'endDate')
|
||||
|
||||
return new User(attrs)
|
||||
|
||||
makeClassroom: (attrs, sources={}) ->
|
||||
|
@ -148,6 +153,38 @@ module.exports = {
|
|||
}, attrs)
|
||||
return new ThangType(attrs)
|
||||
|
||||
makePrepaid: (attrs, sources={}) ->
|
||||
_id = _.uniqueId('prepaid_')
|
||||
attrs = _.extend({}, {
|
||||
_id
|
||||
type: 'course'
|
||||
maxRedeemers: 10
|
||||
endDate: moment().add(1, 'month').toISOString()
|
||||
startDate: moment().subtract(1, 'month').toISOString()
|
||||
}, attrs)
|
||||
|
||||
if not attrs.redeemers
|
||||
redeemers = sources.redeemers or new Users()
|
||||
attrs.redeemers = ({
|
||||
userID: redeemer.id
|
||||
date: moment().subtract(1, 'month').toISOString()
|
||||
} for redeemer in redeemers.models)
|
||||
|
||||
return new Prepaid(attrs)
|
||||
|
||||
makeTrialRequest: (attrs, sources={}) ->
|
||||
_id = _.uniqueId('trial_request_')
|
||||
attrs = _.extend({}, {
|
||||
_id
|
||||
properties: {
|
||||
firstName: 'Mr'
|
||||
lastName: 'Professorson'
|
||||
name: 'Mr Professorson'
|
||||
email: 'an@email.com'
|
||||
phoneNumber: '555-555-5555'
|
||||
organization: 'Greendale'
|
||||
}
|
||||
}, attrs)
|
||||
}
|
||||
|
||||
|
||||
|
|
100
test/app/views/courses/EnrollmentsView.spec.coffee
Normal file
100
test/app/views/courses/EnrollmentsView.spec.coffee
Normal file
|
@ -0,0 +1,100 @@
|
|||
EnrollmentsView = require 'views/courses/EnrollmentsView'
|
||||
Courses = require 'collections/Courses'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
Users = require 'collections/Users'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
factories = require 'test/app/factories'
|
||||
TeachersContactModal = require 'views/teachers/TeachersContactModal'
|
||||
|
||||
describe 'EnrollmentsView', ->
|
||||
|
||||
beforeEach (done) ->
|
||||
me.set('anonymous', false)
|
||||
me.set('role', 'teacher')
|
||||
@view = new EnrollmentsView()
|
||||
|
||||
# Make three classrooms, sharing users from a pool of 10, 5 of which are enrolled
|
||||
prepaid = factories.makePrepaid()
|
||||
students = new Users(_.times(10, (i) ->
|
||||
factories.makeUser({}, { prepaid: if i%2 then prepaid else null }))
|
||||
)
|
||||
|
||||
userSlices = [
|
||||
new Users(students.slice(0, 5))
|
||||
new Users(students.slice(3, 8))
|
||||
new Users(students.slice(7, 10))
|
||||
]
|
||||
|
||||
classrooms = new Classrooms(factories.makeClassroom({}, {members: userSlice}) for userSlice in userSlices)
|
||||
@view.classrooms.fakeRequests[0].respondWith({ status: 200, responseText: classrooms.stringify() })
|
||||
for request, i in @view.members.fakeRequests
|
||||
request.respondWith({status: 200, responseText: userSlices[i].stringify()})
|
||||
|
||||
# Make prepaids of various status
|
||||
prepaids = new Prepaids([
|
||||
factories.makePrepaid({}, {redeemers: new Users(_.times(5, -> factories.makeUser()))})
|
||||
factories.makePrepaid()
|
||||
factories.makePrepaid({ # pending
|
||||
startDate: moment().add(2, 'months').toISOString()
|
||||
endDate: moment().add(14, 'months').toISOString()
|
||||
})
|
||||
factories.makePrepaid( # empty
|
||||
{ maxRedeemers: 2 },
|
||||
{redeemers: new Users(_.times(2, -> factories.makeUser()))}
|
||||
)
|
||||
])
|
||||
@view.prepaids.fakeRequests[0].respondWith({ status: 200, responseText: prepaids.stringify() })
|
||||
|
||||
# Make a few courses, one free
|
||||
courses = new Courses([
|
||||
factories.makeCourse({free: true})
|
||||
factories.makeCourse({free: false})
|
||||
factories.makeCourse({free: false})
|
||||
factories.makeCourse({free: false})
|
||||
])
|
||||
@view.courses.fakeRequests[0].respondWith({ status: 200, responseText: courses.stringify() })
|
||||
|
||||
jasmine.demoEl(@view.$el)
|
||||
window.view = @view
|
||||
@view.supermodel.once 'loaded-all', done
|
||||
|
||||
|
||||
it 'shows how many courses there are which enrolled students will have access to', ->
|
||||
expect(_.contains(@view.$('#enrollments-blurb').text(), '2–4')).toBe(true)
|
||||
if @view.$('#actions-col').length isnt 1
|
||||
fail('There should be an #action-col, other tests depend on it.')
|
||||
|
||||
describe '"Get Enrollments" area', ->
|
||||
|
||||
describe '"Contact Us" button', ->
|
||||
it 'opens a TeachersContactModal, passing in the number of enrollments', ->
|
||||
spyOn(@view, 'openModalView')
|
||||
@view.state.set('numberOfStudents', 20)
|
||||
@view.$('#contact-us-btn').click()
|
||||
expect(view.openModalView).toHaveBeenCalled()
|
||||
args = view.openModalView.calls.argsFor(0)
|
||||
expect(args[0] instanceof TeachersContactModal).toBe(true)
|
||||
expect(args[0].enrollmentsNeeded).toBe(20)
|
||||
|
||||
describe 'when the teacher has made contact', ->
|
||||
beforeEach ->
|
||||
me.set('enrollmentRequestSent', true)
|
||||
@view.render()
|
||||
|
||||
it 'shows confirmation and a mailto link to schools@codecombat.com', ->
|
||||
if not @view.$('#request-sent-btn').length
|
||||
fail('Request button not found.')
|
||||
if not @view.$('#enrollment-request-sent-blurb').length
|
||||
fail('Enrollment request sent blurb not found.')
|
||||
# TODO: Figure out why this fails in Travis. Seems like it's not loading en locale
|
||||
# if not @view.$('a[href="mailto:schools@codecombat.com"]').length
|
||||
# fail('Mailto: link not found.')
|
||||
|
||||
describe 'when there are no prepaids to show', ->
|
||||
beforeEach (done) ->
|
||||
@view.prepaids.reset()
|
||||
_.defer(done)
|
||||
|
||||
it 'fills the void with the rest of the page content', ->
|
||||
expect(@view.$('#actions-col').length).toBe(0)
|
||||
|
53
test/app/views/courses/TeachersContactModal.spec.coffee
Normal file
53
test/app/views/courses/TeachersContactModal.spec.coffee
Normal file
|
@ -0,0 +1,53 @@
|
|||
TeachersContactModal = require 'views/teachers/TeachersContactModal'
|
||||
TrialRequests = require 'collections/TrialRequests'
|
||||
factories = require 'test/app/factories'
|
||||
|
||||
describe 'TeachersContactModal', ->
|
||||
beforeEach (done) ->
|
||||
@modal = new TeachersContactModal({ enrollmentsNeeded: 10 })
|
||||
@modal.render()
|
||||
trialRequests = new TrialRequests([factories.makeTrialRequest()])
|
||||
@modal.trialRequests.fakeRequests[0].respondWith({ status: 200, responseText: trialRequests.stringify() })
|
||||
@modal.supermodel.once('loaded-all', done)
|
||||
jasmine.demoModal(@modal)
|
||||
|
||||
it 'shows an error when the email is invalid and the form is submitted', ->
|
||||
@modal.$('input[name="email"]').val('not an email')
|
||||
@modal.$('form').submit()
|
||||
expect(@modal.$('input[name="email"]').closest('.form-group').hasClass('has-error')).toBe(true)
|
||||
|
||||
it 'shows an error when the message is empty and the form is submitted', ->
|
||||
@modal.$('textarea[name="message"]').val('')
|
||||
@modal.$('form').submit()
|
||||
expect(@modal.$('textarea[name="message"]').closest('.form-group').hasClass('has-error')).toBe(true)
|
||||
|
||||
describe 'submit form', ->
|
||||
beforeEach ->
|
||||
@modal.$('form').submit()
|
||||
|
||||
it 'disables inputs', ->
|
||||
for el in @modal.$('button, input, textarea')
|
||||
expect($(el).is(':disabled')).toBe(true)
|
||||
|
||||
describe 'failed contact', ->
|
||||
beforeEach ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({status: 500})
|
||||
|
||||
it 'shows an error', ->
|
||||
expect(@modal.$('.alert-danger').length).toBe(1)
|
||||
|
||||
describe 'successful contact', ->
|
||||
beforeEach ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({status: 200, responseText: '{}'})
|
||||
|
||||
it 'shows a success message', ->
|
||||
expect(@modal.$('.alert-success').length).toBe(1)
|
||||
|
||||
describe 'submit button', ->
|
||||
it 'is disabled until one of the inputs changes', ->
|
||||
expect(@modal.$('#submit-btn').is(':disabled')).toBe(true)
|
||||
@modal.$('input[name="email"]').trigger('change')
|
||||
expect(@modal.$('#submit-btn').is(':disabled')).toBe(false)
|
||||
|
|
@ -1,58 +1,68 @@
|
|||
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
Courses = require 'collections/Courses'
|
||||
Levels = require 'collections/Levels'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
Users = require 'collections/Users'
|
||||
forms = require 'core/forms'
|
||||
factories = require 'test/app/factories'
|
||||
|
||||
# Needs some fixing
|
||||
xdescribe 'ActivateLicensesModal', ->
|
||||
|
||||
@modal = null
|
||||
|
||||
me = require 'test/app/fixtures/teacher'
|
||||
prepaids = require 'test/app/fixtures/prepaids'
|
||||
classrooms = require 'test/app/fixtures/classrooms/unarchived-classrooms'
|
||||
users = require 'test/app/fixtures/students'
|
||||
responses = {
|
||||
'/db/prepaid': prepaids.toJSON()
|
||||
'/db/classroom': classrooms.toJSON()
|
||||
# '/members': users.toJSON() # TODO: Respond with different ones for different classrooms
|
||||
}
|
||||
|
||||
makeModal = (options) ->
|
||||
(done) ->
|
||||
@selectedUsers = new Users(@users.models.slice(0,(options?.numSelected or 3)))
|
||||
@modal = new ActivateLicensesModal({
|
||||
@classroom, @users, @selectedUsers
|
||||
describe 'ActivateLicensesModal', ->
|
||||
|
||||
beforeEach (done) ->
|
||||
@members = new Users(_.times(4, (i) -> factories.makeUser()))
|
||||
@classrooms = new Classrooms([
|
||||
factories.makeClassroom({}, { @members })
|
||||
factories.makeClassroom()
|
||||
])
|
||||
selectedUsers = new Users(@members.slice(0,3))
|
||||
options = _.extend({}, {
|
||||
classroom: @classrooms.first(), @classrooms, users: @members, selectedUsers
|
||||
}, options)
|
||||
@modal = new ActivateLicensesModal(options)
|
||||
@prepaidThatExpiresSooner = factories.makePrepaid({maxRedeemers: 1, endDate: moment().add(1, 'month').toISOString()})
|
||||
@prepaidThatExpiresLater = factories.makePrepaid({maxRedeemers: 1, endDate: moment().add(2, 'months').toISOString()})
|
||||
prepaids = new Prepaids([
|
||||
# empty
|
||||
factories.makePrepaid({maxRedeemers: 0, endDate: moment().add(1, 'day').toISOString()})
|
||||
|
||||
# expired
|
||||
factories.makePrepaid({maxRedeemers: 10, endDate: moment().subtract(1, 'day').toISOString()})
|
||||
|
||||
# pending
|
||||
factories.makePrepaid({
|
||||
maxRedeemers: 100
|
||||
startDate: moment().add(1, 'month').toISOString()
|
||||
endDate: moment().add(2, 'months').toISOString()
|
||||
})
|
||||
jasmine.Ajax.requests.sendResponses(responses)
|
||||
_.filter(jasmine.Ajax.requests.all().slice(), (request) ->
|
||||
/\/db\/classroom\/.*\/members/.test(request.url) and request.readyState < 4
|
||||
).forEach (request) ->
|
||||
request.respondWith(users.toJSON)
|
||||
# debugger
|
||||
|
||||
jasmine.demoModal(@modal)
|
||||
_.defer done
|
||||
|
||||
beforeEach ->
|
||||
@classroom = classrooms.get('active-classroom')
|
||||
@users = require 'test/app/fixtures/students'
|
||||
|
||||
afterEach ->
|
||||
@modal.stopListening()
|
||||
|
||||
# these should be used
|
||||
@prepaidThatExpiresSooner
|
||||
@prepaidThatExpiresLater
|
||||
])
|
||||
@modal.prepaids.fakeRequests[0].respondWith({ status: 200, responseText: prepaids.stringify() })
|
||||
@modal.classrooms.fakeRequests[0].respondWith({
|
||||
status: 200
|
||||
responseText: @classrooms.stringify()
|
||||
})
|
||||
@modal.classrooms.first().users.fakeRequests[0].respondWith({
|
||||
status: 200
|
||||
responseText: @members.stringify()
|
||||
})
|
||||
|
||||
jasmine.demoModal(@modal)
|
||||
_.defer done
|
||||
|
||||
describe 'the class dropdown', ->
|
||||
beforeEach makeModal()
|
||||
it 'contains an All Students option', ->
|
||||
expect(@modal.$('select option:last-child').data('i18n')).toBe('teacher.all_students')
|
||||
|
||||
# punted indefinitely
|
||||
xit 'should contain an All Students option', ->
|
||||
expect(@modal.$('select option:last-child').html()).toBe('All Students')
|
||||
it 'displays the current classname', ->
|
||||
expect(@modal.$('option:selected').html()).toBe(@classrooms.first().get('name'))
|
||||
|
||||
it 'should display the current classname', ->
|
||||
expect(@modal.$('option:selected').html()).toBe('Teacher Zero\'s Classroomiest Classroom')
|
||||
|
||||
it 'should contain all of the teacher\'s classes'
|
||||
|
||||
it 'shouldn\'t contain anyone else\'s classrooms'
|
||||
it 'contains all of the teacher\'s classes', ->
|
||||
expect(@modal.$('select option').length).toBe(3) # including 'All Students' options
|
||||
|
||||
describe 'the checklist of students', ->
|
||||
it 'should separate the unenrolled from the enrolled students'
|
||||
|
@ -63,21 +73,35 @@ xdescribe 'ActivateLicensesModal', ->
|
|||
|
||||
|
||||
describe 'the credits availble count', ->
|
||||
beforeEach makeModal()
|
||||
it 'should match the number of unused prepaids', ->
|
||||
expect(@modal.$('#total-available').html()).toBe('2')
|
||||
|
||||
describe 'the Enroll button', ->
|
||||
beforeEach makeModal()
|
||||
it 'should show the number of selected students', ->
|
||||
expect(@modal.$('#total-selected-span').html()).toBe('3')
|
||||
|
||||
it 'should fire off one request when clicked'
|
||||
|
||||
describe 'when the teacher has enough enrollments', ->
|
||||
beforeEach makeModal({ numSelected: 2 })
|
||||
beforeEach ->
|
||||
selected = @modal.state.get('selectedUsers')
|
||||
selected.remove(selected.first())
|
||||
|
||||
it 'should be enabled', ->
|
||||
expect(@modal.$('#activate-licenses-btn').hasClass('disabled')).toBe(false)
|
||||
|
||||
describe 'when clicked', ->
|
||||
beforeEach ->
|
||||
@modal.$('form').submit()
|
||||
|
||||
it 'enrolls the selected students with the soonest-to-expire, available prepaid', ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
if request.url.indexOf(@prepaidThatExpiresSooner.id) is -1
|
||||
fail('The first prepaid should be the prepaid that expires sooner')
|
||||
request.respondWith({ status: 200, responseText: '{ "redeemers": [{}] }' })
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
if request.url.indexOf(@prepaidThatExpiresLater.id) is -1
|
||||
fail('The second prepaid should be the prepaid that expires later')
|
||||
|
||||
describe 'when the teacher doesn\'t have enough enrollments', ->
|
||||
it 'should be disabled', ->
|
||||
|
|
|
@ -25,7 +25,16 @@ describe 'TeacherClassView', ->
|
|||
me = factories.makeUser({})
|
||||
|
||||
@courses = new Courses([factories.makeCourse({name: 'First Course'}), factories.makeCourse({name: 'Second Course'})])
|
||||
@students = new Users(_.times(2, -> factories.makeUser()))
|
||||
available = factories.makePrepaid()
|
||||
expired = factories.makePrepaid({endDate: moment().subtract(1, 'day').toISOString()})
|
||||
@students = new Users([
|
||||
factories.makeUser({name: 'Abner'})
|
||||
factories.makeUser({name: 'Abigail'})
|
||||
factories.makeUser({name: 'Abby'}, {prepaid: available})
|
||||
factories.makeUser({name: 'Ben'}, {prepaid: available})
|
||||
factories.makeUser({name: 'Ned'}, {prepaid: expired})
|
||||
factories.makeUser({name: 'Ebner'}, {prepaid: expired})
|
||||
])
|
||||
@levels = new Levels(_.times(2, -> factories.makeLevel()))
|
||||
@classroom = factories.makeClassroom({}, { @courses, members: @students, levels: [@levels, new Levels()] })
|
||||
@courseInstances = new CourseInstances([
|
||||
|
@ -42,7 +51,7 @@ describe 'TeacherClassView', ->
|
|||
{level, creator: @finishedStudent})
|
||||
)
|
||||
sessions.push(factories.makeLevelSession(
|
||||
{state: {complete: false}},
|
||||
{state: {complete: true}},
|
||||
{level: @levels.first(), creator: @unfinishedStudent})
|
||||
)
|
||||
@levelSessions = new LevelSessions(sessions)
|
||||
|
@ -66,6 +75,9 @@ describe 'TeacherClassView', ->
|
|||
# it "shows the classroom's join code"
|
||||
|
||||
describe 'the Students tab', ->
|
||||
beforeEach ->
|
||||
@view.state.set('activeTab', '#students-tab')
|
||||
|
||||
# it 'shows all of the students'
|
||||
# it 'sorts correctly by Name'
|
||||
# it 'sorts correctly by Progress'
|
||||
|
@ -89,7 +101,37 @@ describe 'TeacherClassView', ->
|
|||
# it 'still shows the correct Course Overview progress'
|
||||
#
|
||||
|
||||
describe 'the Enrollment Status tab', ->
|
||||
beforeEach ->
|
||||
@view.state.set('activeTab', '#enrollment-status-tab')
|
||||
|
||||
|
||||
|
||||
|
||||
describe 'Enroll button', ->
|
||||
it 'calls enrollStudents with that user when clicked', ->
|
||||
spyOn(@view, 'enrollStudents')
|
||||
@view.$('.enroll-student-button:first').click()
|
||||
expect(@view.enrollStudents).toHaveBeenCalled()
|
||||
users = @view.enrollStudents.calls.argsFor(0)[0]
|
||||
expect(users.size()).toBe(1)
|
||||
expect(users.first().id).toBe(@view.students.first().id)
|
||||
|
||||
describe 'Revoke button', ->
|
||||
it 'opens a confirm modal once clicked', ->
|
||||
spyOn(window, 'confirm').and.returnValue(true)
|
||||
@view.$('.revoke-student-button:first').click()
|
||||
expect(window.confirm).toHaveBeenCalled()
|
||||
|
||||
describe 'once the prepaid is successfully revoked', ->
|
||||
beforeEach ->
|
||||
spyOn(window, 'confirm').and.returnValue(true)
|
||||
button = @view.$('.revoke-student-button:first')
|
||||
@revokedUser = @view.students.get(button.data('user-id'))
|
||||
@view.$('.revoke-student-button:first').click()
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({
|
||||
status: 200
|
||||
responseText: '{}'
|
||||
})
|
||||
|
||||
it 'updates the user and rerenders the page', ->
|
||||
if @view.$(".enroll-student-button[data-user-id='#{@revokedUser.id}']").length isnt 1
|
||||
fail('Could not find enroll student button for user whose enrollment was revoked')
|
||||
|
|
Loading…
Reference in a new issue