Partially fix ActivateLicensesModal.spec

[IN PROGRESS] Don't display deleted users

Move userID to classroom.deletedMembers on user delete (not retroactive)

Fix PDF links for course guides, remove old PDFs from repo

Remove deprecated SalesView

Remove underline for not-yet-linked student names

Only show class select when there's more than one

Ignore case when sorting student names

Use student.broadName instead of name for display and sorting

Fix initial load not showing progress after joining a course (hacky)

Fix text entry for enrollment number input

Fix enrollment statistics

Fix enrollment stats completely (and add back in per-class unenrolled count)

Add deletedMembers to classroom schema

More fixes to enrollment stats (don't count nonmember prepaids)

Don't use 0 as implicit false for openSpots

Update suggested number of credit to buy automatically

Fix classroom edit form ignoring cleared values

Add alert text when more users selected than enrollments available

Alert user when trying to assign course to unenrolled students

Alert user when assigning course to nobody

Add some tests for TeacherClassView bulk assign alerts

Fix TeacherClassView tests failing without demos

Use model/collection.fakeRequests :D

Remove unused comment

Fix handling of improperly sorted deleted users on clientside

Add test for moving deleted users to deletedMembers

Add script for moving all deleted classroom members to classroom.deletedMembers

Completely rewrite tallying up enrollment statistics

Fix some tests to not be dependent on logged-in user

Address PR comments

Fix default number of enrollments to buy

Fix i18n for not enough enrollments

Use custom error message for classroom name length
This commit is contained in:
phoenixeliot 2016-04-07 14:55:42 -07:00
parent e5734cbdd3
commit e2d08fa7cf
34 changed files with 345 additions and 565 deletions

View file

@ -6,6 +6,9 @@ module.exports = class Users extends CocoCollection
url: '/db/user'
fetchForClassroom: (classroom, options={}) ->
if options.removeDeleted
delete options.removeDeleted
@listenTo @, 'sync', @removeDeletedUsers
classroomID = classroom.id or classroom
limit = 10
skip = 0
@ -21,3 +24,8 @@ module.exports = class Users extends CocoCollection
jqxhrs.push(@fetch(options))
skip += limit
return jqxhrs
removeDeletedUsers: ->
@remove @filter (user) ->
user.get('deleted')
true

View file

@ -1061,7 +1061,6 @@
already_enrolled: "already enrolled"
licenses_remaining: "licenses remaining:"
insufficient_enrollments: "insufficient paid enrollments"
enroll_students: "Enroll Students"
get_enrollments: "Get More Enrollments"
change_language: "Change Course Language"
keep_using: "Keep Using"
@ -1286,10 +1285,14 @@
assign_to_selected_students: "Assign to Selected Students"
assigned: "Assigned"
enroll_selected_students: "Enroll Selected Students"
cant_assign_to_unenrolled: "Course cannot be assigned to students who are not enrolled."
no_students_selected: "No students were selected."
guides_coming_soon: "Guides coming soon!" # Courses
show_students_from: "Show students from" # Enroll students modal
enroll_the_following_students: "Enroll the following students"
all_students: "All Students"
enroll_students: "Enroll Students"
not_enough_enrollments: "Not enough Enrollments available."
enrollments_blurb_1: "Students taking Computer Science" # Enrollments page
enrollments_blurb_2: "require enrollments to access the courses."
credits_available: "Credits Available"
@ -1305,7 +1308,8 @@
how_to_enroll_blurb_2: "To bulk-enroll multiple students, select them using the checkboxes on the left side of the classroom page and click the \"Enroll Selected Students\" button."
how_to_enroll_blurb_3: "Once a student is enrolled, they will have access to all of the course content."
bulk_pricing_blurb: "Purchasing for more than 15 students? Get in touch with us for bulk pricing quotes."
total_unenrolled: "Total unenrolled"
classes:
archmage_title: "Archmage"
archmage_title_description: "(Coder)"

View file

@ -4,7 +4,9 @@ ClassroomSchema = c.object {title: 'Classroom', required: ['name']}
c.extendNamedProperties ClassroomSchema # name first
_.extend ClassroomSchema.properties,
name: { type: 'string', minLength: 1 }
members: c.array {title: 'Members'}, c.objectId()
deletedMembers: c.array {title: 'Deleted Members'}, c.objectId()
ownerID: c.objectId()
description: {type: 'string'}
code: c.shortString(title: "Unique code to redeem")

View file

@ -6,3 +6,9 @@
.well
max-height: 284px
overflow: scroll
.not-enough-enrollments
color: red
visibility: hidden
&.visible
visibility: visible

View file

@ -65,6 +65,7 @@
border-bottom: none
.bulk-assign-controls
position: relative
float: right
margin-bottom: -9999px
margin-top: 20px
@ -74,7 +75,15 @@
margin-left: 10px
.enroll-selected-students
margin-left: 56px
.cant-assign-to-unenrolled, .no-students-selected
position: absolute
top: -24px
color: red
font-size: 13px
visibility: hidden
&.visible
visibility: visible
.students-table
width: 100%
.student-info-col
@ -111,7 +120,7 @@
display: inline
.inline-student-name
white-space: nowrap
text-decoration: underline
// text-decoration: underline
li:not(:last-child):after
content: ', '

View file

@ -1,198 +0,0 @@
#sales-view
background-color: #F5F5F5
font-family: Helvetica, sans-serif
font-size: 15px
line-height: 20px
// TODO: better way to remove content styling?
#site-content-area
width: 100%
background-color: #F5F5F5
border: none
margin: auto
padding: 0px
min-height: inherit
.btn-contact-us
background-color: #3878DE
border: none
color: #FFFFFF
font-size: 18px
margin-top: 20px
padding: 20px
width: 330px
text-decoration: none
text-transform: uppercase
div
text-align: left
img
float: left
margin-right: 10px
.btn-create-account, .btn-enter-courses
background-color: #09AC48
color: #FFFFFF
display: inline-block
font-size: 18px
margin: 10px
padding: 24px
width: 400px
text-decoration: none
text-transform: uppercase
.btn-login-account
color: #FFFFFF
text-decoration: underline
.btn-setup-class
margin-top: 20px
text-transform: uppercase
width: 300px
.section-header
font-family: Merriweather
font-size: 23px
line-height: 29px
padding: 0px 24px 0px 24px
border-bottom: 1px solid lightgray
.section-subheader
font-family: Helvetica, sans-serif
font-size: 13px
color: #727272
line-height: 15px
.text-right
text-align: right
#top-page-content
background-image: url('/images/pages/sales/hero_background.png')
background-size: cover
color: #FFFFFF
td
vertical-align: top
.big-quote-mark
font-family: Merriweather
font-size: 130px
font-weight: 700
line-height: 130px
margin-right: 0px
opacity: 0.5
.hero-quote-container
margin: 20px 20px 20px 0px
width: 60%
.hero-quote
color: #FFFFFF
font-family: Merriweather
font-size: 38px
font-weight: 700
line-height: 48px
.hero-quote-attribution
font-family: Helvetica, sans-serif
font-style: italic
font-size: 15px
color: #FFFFFF
line-height: 20px
margin-right: 100px
text-align: right
#down-arrow
padding: 20px
#main-content
text-align: left
display: inline-block
width: 850px
#blurb1
padding: 0px 20px 0px 20px
.blurb-subtitle
font-size: 17px
font-weight: bold
#course-comparisons
font-size: 12px
margin: 20px
width: 90%
img
width: 100%
.img-caption
font-family: Helvetica, sans-serif
font-style: italic
font-size: 11px
color: #727272
line-height: 13px
.img-face
background-color: #f0e5c7
border-radius: 50%
height: 100px
width: 100px
.img-game
width: 100%
.teacher-quote
font-family: Merriweather
font-weight: 300
font-size: 15px
line-height: 20px
padding-top: 5px
.teacher-name
font-family: Helvetica, sans-serif
font-weight: 700
font-size: 16px
line-height: 19px
.teacher-location
font-family: Helvetica, sans-serif
font-style: italic
font-size: 12px
line-height: 14px
#quote1-container
background-image: url('/images/pages/sales/quote1.png')
background-repeat: no-repeat
background-size: 100% auto
height: 265px
padding: 12px
margin-right: 10px
.hero-quote-attribution
margin-top: 60px
#quote2-container
background-image: url('/images/pages/sales/quote2.png')
background-repeat: no-repeat
background-size: 100% auto
height: 265px
padding: 20px 12px 12px 12px
margin-left: 10px
.teacher-quote
margin-top: 60px
.hero-quote-attribution
margin-top: 20px
.twitter-attribution
font-family: Helvetica
font-weight: 700
font-size: 11px
line-height: 14px
color: black
display: inline-block
text-decoration: none

View file

@ -3,20 +3,21 @@ extends /templates/core/modal-base-flat
block modal-header-content
.clearfix
.text-center
h1(data-i18n="courses.enroll_students")
h1(data-i18n="teacher.enroll_students")
h2(data-i18n="courses.grants_lifetime_access")
if view.classroom
p= view.classroom.get('name')
block modal-body-content
.text-center
span(data-i18n='teacher.show_students_from')
span.spr :
select
each classroom in view.classrooms.models
option(selected=(classroom.id === view.classroom.id), value=classroom.id)
= classroom.get('name')
//- option(selected=!view.classroom, value='all-classrooms' data-i18n='teacher.all_students')
if view.classrooms.length > 1
.text-center
span(data-i18n='teacher.show_students_from')
span.spr :
select
each classroom in view.classrooms.models
option(selected=(classroom.id === view.classroom.id), value=classroom.id)
= classroom.get('name')
//- option(selected=!view.classroom, value='all-classrooms' data-i18n='teacher.all_students')
form.form
span(data-i18n="teacher.enroll_the_following_students")
span :
@ -40,6 +41,8 @@ block modal-body-content
span.spr(data-i18n="courses.enrollment_credits_available")
span#total-available= view.prepaids.totalAvailable()
p.small-details.not-enough-enrollments
span(data-i18n='teacher.not_enough_enrollments')
p
button#activate-licenses-btn.btn.btn-lg.btn-primary(type="submit")
span.spr(data-i18n="courses.enroll")

View file

@ -51,27 +51,37 @@ mixin enrollmentStats
span(data-i18n='teacher.credits_available')
span.spr :
= view.prepaids.totalAvailable()
//- .small-details
//- span(data-i18n='teacher.total_unique_students')
//- span.spr :
//- = view.members.length
//- .small-details
//- span(data-i18n='teacher.total_enrolled_students')
//- span.spr :
//- = view.prepaids.totalRedeemers()
//- .small-details
//- span(data-i18n='teacher.unenrolled_students')
//- span.spr :
//- = (view.members.length - view.prepaids.totalRedeemers())
.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]
.small-details
span(data-i18n='teacher.total_unenrolled')
span.spr :
= view.totalNotEnrolled
//- .enroll-students.btn.btn-lg.btn-navy
//- | Enroll Students
//- 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')
input#students-input.text-center.enrollment-count(value=15 type='number')
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

View file

@ -134,9 +134,9 @@ mixin inlineUserList(users)
each student in users
li
//- a(href='TODO')
//- = student.get('name')
//- = student.broadName()
span.inline-student-name
= student.get('name')
= student.broadName()
mixin addStudentsButton
.add-students.text-center
@ -176,7 +176,7 @@ mixin studentRow(student)
.student-info
if student.get('deleted')
em (deleted)
div.student-name= student.get('name')
div.student-name= student.broadName()
div.student-email.small-details= student.get('email')
td.hidden
a.edit-student-button(data-student-id=student.id)
@ -244,7 +244,7 @@ mixin courseOverview
mixin studentLevelsRow(student)
.student-levels-row.alternating-background
div.student-info
div.student-name= student.get('name')
div.student-name= student.broadName()
div.student-email.small-details= student.get('email')
div.student-levels-progress
- var course = view.selectedCourse
@ -284,6 +284,10 @@ mixin copyCodes
mixin bulkAssignControls
.bulk-assign-controls.form-inline
.no-students-selected.small-details(class=view.assigningToNobody ? 'visible' : '')
span(data-i18n='teacher.no_students_selected')
.cant-assign-to-unenrolled.small-details(class=view.assigningToUnenrolled ? 'visible' : '')
span(data-i18n='teacher.cant_assign_to_unenrolled')
span.small
span(data-i18n='teacher.bulk_assign')
span :

View file

@ -82,8 +82,12 @@ mixin course-info(course)
if view.guideLinks[course.id]
//- a.btn.btn-primary(href=view.guideLinks[course.id] class=(me.isAnonymous() ? 'disabled' : ''))
//- span(data-i18n="courses.print_guide")
a.btn.btn-primary(href=view.guideLinks[course.id] class=(me.isAnonymous() ? 'disabled' : ''))
a.btn.btn-primary(href=view.guideLinks[course.id].python class=(me.isAnonymous() ? 'disabled' : ''))
span(data-i18n="courses.view_guide_online")
| — Python
a.btn.btn-primary(href=view.guideLinks[course.id].javascript class=(me.isAnonymous() ? 'disabled' : ''))
span(data-i18n="courses.view_guide_online")
| — JavaScript
else
i.small
| (

View file

@ -1,220 +0,0 @@
extends /templates/base
//- Do NOT localize / i18n
block content
#top-page-content
br
br
.text-right
button.btn-contact-us(href='/teachers/quote')
img(src='/images/pages/sales/chat_icon.png')
div contact us for a quote
table
tr
td
.big-quote-mark “
td
.hero-quote-container
.hero-quote CodeCombat is without question the most engaging platform for learning programming languages.”
.hero-quote-attribution - Jonathan P., Elementary Computers Teacher
if me.isAnonymous()
.text-center
a.btn-create-account set up a free class
.text-center
span.spr Already have an account?
a.btn-login-account Log in here.
else
.text-center
a.btn-enter-courses(href="/teachers/classes") set up a free class
p.text-center
a(href='#getting-started')
img#down-arrow(src='/images/pages/sales/down_arrow.png')
a(name="getting-started")
.text-center
#main-content
br
p.text-center
span.section-header What are CodeCombat Courses?
p.text-center.section-subheader An entire semesters' worth of computer science curriculum for teachers who want to bring programming into their 4th-12th grade classes.
br
.row
.col-md-5
img#img-game(src='/images/pages/sales/screen1.png')
.text-center
i
small Students learn by writing code to complete levels in the game.
.col-md-7
#blurb1
//- TODO: why don't jade inline tags work here?
//- http://stackoverflow.com/questions/10953326/what-is-a-concise-way-to-create-inline-elements-in-jade
p
strong.spr CodeCombat
span.spr is a platform for students to learn programming all while playing a game with their classmates. We believe in encouraging
strong.spr real, typed code
span to support healthy learning curve with programming.
p
span.spr CodeCombat's Courses consist of levels that have been specifically playtested to work best in a classroom setting, designed to be used by teachers with
strong.spr no prior coding experience necessary.
span Current Courses are available in JavaScript and Python, with solution guides provided for both.
br
br
.row
.col-md-7
#blurb2
p.text-center.blurb-subtitle Designed with Teachers in Mind
ul
li Intuitive dashboard to track student progress
li Course-specific teacher guides with full solutions to each levels
li Teachers' forum and community-generated lesson plans.
li Optional leaderboards to encourage friendly competitions
li
span.spr Auto-generate student progress reports
i (coming soon!)
li
span.spr Multiple administrative accounts
i (coming soon!)
.col-md-5
img.img-game(src='/images/pages/sales/screen2.png')
.text-center
i
small Teachers can monitor progress, manage classrooms and more.
if me.isAnonymous()
br
.text-center
a.btn-create-account set up a free class
br
br
p.text-center
span.section-header Students (and parents) love us!
br
table
tr
td
#quote1-container
.teacher-quote
span.spr “My class had been struggling with the basics of Python all year. Today they were having fun and getting into it
strong - I think they forgot that they were actually learning something.”
.row
.col-md-4
.col-md-8
.hero-quote-attribution
.teacher-name Tim M.
a(href='https://twitter.com/timmaki',target='_blank') @timmaki
.teacher-location Director of Technology, Tilton School
td
#quote2-container
.row
.col-md-8
.hero-quote-attribution
.teacher-name.text-right Darlease M.
.teacher-location.text-right Technology Coordinator,
.teacher-location.text-right Global Learning Charter Public School
.col-md-4
.teacher-quote
span.spr “My girls, who were apprehensive about taking a coding class, are some of my top students.
strong They work together and explain the code to each other to make sure each understands.”
br
.text-center.section-subheader Students play CodeCombat during #HourOfCode 2015
table(cellpadding=4)
tr
td
img(src='/images/pages/sales/classroom3.png')
.text-right
a(href='https://twitter.com/flinng/status/674238468747354112')
img(src='/images/twitter_icon.png')
span.twitter-attribution @flinng
td
img(src='/images/pages/sales/classroom4.png')
.text-right
a(href='https://twitter.com/HikariKishi/status/674359511566577664')
img(src='/images/twitter_icon.png')
span.twitter-attribution @HikariKishi
td
img(src='/images/pages/sales/classroom2.png')
.text-right
a(href='https://twitter.com/Coderdojovno/status/675743290461941760')
img(src='/images/twitter_icon.png')
span.twitter-attribution @Coderdojovno
table(cellpadding=4)
tr
td
img(src='/images/pages/sales/classroom6.png')
.text-right
a(href='https://twitter.com/teachercoulter/status/674734565487828992')
img(src='/images/twitter_icon.png')
span.twitter-attribution @teachercoulter
td
img(src='/images/pages/sales/classroom1.png')
.text-right
a(href='https://twitter.com/CentreHigh/status/674643613360324608')
img(src='/images/twitter_icon.png')
span.twitter-attribution @CentreHigh
td
img(src='/images/pages/sales/classroom5.png')
.text-right
a(href='https://twitter.com/MrsYassen_GRE/status/674747949902090244')
img(src='/images/twitter_icon.png')
span.twitter-attribution @MrsYassen_GRE
br
p.blurb-subtitle Facts about CodeCombat:
.row
.col-md-6
ul
li Students collaborate and help each other solve levels.
li Appeals to all genders and a wide range of age groups.
.col-md-6
ul
li Typed code gives students an advantage over block-based programs.
li Success with both high-performing and low-performing students, as well as ESL.
br
.row
.col-md-8
p.text-center
span.section-header What are paid courses?
p The one-hour long "Introduction to Computer Science" course will always be free for an unlimited number of students. Paid enrollments allow each student access to Computer Science 2, 3, and 4, which is an additional 15 hours of content on top of the free content. Paid enrollments never expire.
#course-comparisons
img(src='/images/pages/sales/content_table.png')
br
p Per-student pricing allows us to better support flexibility for both large and small schools. Contact us if you are interested in purchasing paid course enrollments for your classroom or school. Free trials are also available upon request.
.col-md-4
.well
p.text-center.blurb-subtitle Resources for Teachers
p
a(href='http://codecombat.com/docs/CodeCombatCoursesGettingStartedGuide.pdf')
img(src='/images/Adobe_PDF_file_icon_32x32.png')
span Getting Started Guide
p
a(href='http://codecombat.com/docs/CodeCombatTeacherGuideCourse1.pdf')
img(style='float:left;', src='/images/Adobe_PDF_file_icon_32x32.png')
span Introduction to Computer Science Teacher's Guide
br
p
i
small Solution guides and additional lesson plans available for paid courses.
p
i
small.spr Contact
a(href='mailto:team@codecombat.com') team@codecombat.com
small.spl with additional requests.
br
br
p.text-center
button.btn-contact-us contact us for a quote
br

View file

@ -186,7 +186,6 @@ block content
p(data-i18n="teachers_quote.finish_signup_p")
#social-network-signups
span(data-i18n="teachers_quote.signup_with")
button#facebook-signup-btn.btn.btn-facebook.btn-lg.m-x-1
span.spr(data-i18n="teachers_quote.signup_with")
| Facebook

View file

@ -1,37 +0,0 @@
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
RootView = require 'views/core/RootView'
template = require 'templates/sales-view'
CreateAccountModal = require 'views/core/CreateAccountModal'
module.exports = class SalesView extends RootView
id: 'sales-view'
template: template
events:
'click .btn-contact-us': 'onClickContactUs'
'click .btn-create-account': 'onClickSignup'
'click .btn-login-account': 'onClickLogin'
'click #down-arrow': 'onClickDownArrow'
getTitle: ->
'CodeCombat'
onClickContactUs: (e) ->
app.router.navigate '/teachers/quote', trigger: true
onClickLogin: (e) ->
@openModalView new AuthModal() if me.get('anonymous')
window.tracker?.trackEvent 'Started Login', category: 'Sales', label: 'Sales Login', ['Mixpanel']
onClickSignup: (e) ->
@openModalView new CreateAccountModal() if me.get('anonymous')
window.tracker?.trackEvent 'Started Signup', category: 'Sales', label: 'Sales Create', ['Mixpanel']
logoutRedirectURL: false
onClickDownArrow: (e) ->
$('#page-container').animate({
scrollTop: $('[name="' + $(e.target).closest('a').attr('href').substr(1) + '"]').offset().top
}, 300)
false

View file

@ -29,7 +29,7 @@ module.exports = class ActivateLicensesModal extends ModalView
success: =>
@classrooms.each (classroom) =>
classroom.users = new Users()
jqxhrs = classroom.users.fetchForClassroom(classroom)
jqxhrs = classroom.users.fetchForClassroom(classroom, { removeDeleted: true })
@supermodel.trackRequests(jqxhrs)
})
@supermodel.trackCollection(@classrooms)
@ -46,10 +46,8 @@ module.exports = class ActivateLicensesModal extends ModalView
numToActivate = @$('input[name="user"]:checked:not(:disabled)').length
@$('#total-selected-span').text(numToActivate)
remaining = @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() - numToActivate
@$('#licenses-remaining-span').text(remaining)
depleted = remaining < 0
@$('#not-depleted-span').toggleClass('hide', depleted)
@$('#depleted-span').toggleClass('hide', !depleted)
@$('.not-enough-enrollments').toggleClass('visible', depleted)
@$('#activate-licenses-btn').toggleClass('disabled', depleted).toggleClass('btn-success', not depleted).toggleClass('btn-default', depleted)
replaceStudentList: (e) ->
@ -97,8 +95,8 @@ module.exports = class ActivateLicensesModal extends ModalView
return
user = @usersToRedeem.first()
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots())
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots()) unless prepaid
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'

View file

@ -28,7 +28,7 @@ module.exports = class ClassroomSettingsModal extends ModalView
e.preventDefault()
form = @$('form')
forms.clearFormAlerts(form)
attrs = forms.formToObject(form)
attrs = forms.formToObject(form, ignoreEmptyString: false)
if attrs.language
attrs.aceConfig = { language: attrs.language }
delete attrs.language
@ -38,6 +38,9 @@ module.exports = class ClassroomSettingsModal extends ModalView
@classroom.set(attrs)
schemaErrors = @classroom.getValidationErrors()
if schemaErrors
for error in schemaErrors
if error.schemaPath is "/properties/name/minLength"
error.message = 'Please enter a class name.'
forms.applyErrorsToForm(form, schemaErrors)
return
@ -49,4 +52,4 @@ module.exports = class ClassroomSettingsModal extends ModalView
@stopListening @classroom, 'sync', @hide
button.text(@oldButtonText).attr('disabled', false)
errors.showNotyNetworkError(jqxhr)
@listenToOnce @classroom, 'sync', @hide
@listenToOnce @classroom, 'sync', @hide

View file

@ -116,8 +116,8 @@ 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())
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots()) unless prepaid
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'

View file

@ -134,7 +134,7 @@ module.exports = class CoursesView extends RootView
@errorMessage = "#{jqxhr.responseText}"
@renderSelectors '#join-class-form'
onJoinClassroomSuccess: (newClassroom, jqxhr, options) ->
onJoinClassroomSuccess: (newClassroom, data, options) ->
application.tracker?.trackEvent 'Joined classroom', {
category: 'Courses'
classCode: @classCode
@ -158,13 +158,17 @@ module.exports = class CoursesView extends RootView
courseInstance.sessions = new Backbone.Collection()
@courseInstances.add(courseInstance)
$.when(jqxhrs...).done =>
@state = null
@render()
location.hash = ''
f = -> location.hash = '#just-added-text'
# quick and dirty scroll to just-added classroom
setTimeout(f, 10)
# This is a hack to work around previous hacks
# TODO: Do joinWithCode properly (before page load)
# TODO: Do data flow properly (so going to the class URL works and we don't need to just refresh)
location.search = ""
# @state = null
# @render()
# location.hash = ''
# f = -> location.hash = '#just-added-text'
# # quick and dirty scroll to just-added classroom
# setTimeout(f, 10)
onClickChangeLanguageLink: ->
application.tracker?.trackEvent 'Student clicked change language', category: 'Courses'
modal = new ChangeCourseLanguageModal()

View file

@ -9,6 +9,7 @@ RootView = require 'views/core/RootView'
stripeHandler = require 'core/services/stripe'
template = require 'templates/courses/enrollments-view'
User = require 'models/User'
Users = require 'collections/Users'
utils = require 'core/utils'
Products = require 'collections/Products'
@ -24,8 +25,8 @@ module.exports = class EnrollmentsView extends RootView
@supermodel.trackCollection(@ownedClassrooms)
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
@fromClassroom = utils.getQueryVariable('from-classroom')
@members = new CocoCollection([], { model: User })
@listenTo @members, 'sync', @membersSync
@members = new Users()
# @listenTo @members, 'sync add remove', @calculateEnrollmentStats
@classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom })
@classrooms.comparator = '_id'
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
@ -44,6 +45,7 @@ module.exports = class EnrollmentsView extends RootView
# 'click .enroll-students': 'onClickEnrollStudents'
onLoaded: ->
@calculateEnrollmentStats()
@pricePerStudent = @products.findWhere({name: 'course'}).get('amount')
me.setRole 'teacher'
super()
@ -53,28 +55,54 @@ module.exports = class EnrollmentsView extends RootView
onceClassroomsSync: ->
for classroom in @classrooms.models
@members.fetch({
remove: false
url: "/db/classroom/#{classroom.id}/members"
})
@supermodel.trackRequests @members.fetchForClassroom(classroom, {remove: false, removeDeleted: true})
membersSync: ->
calculateEnrollmentStats: ->
@removeDeletedStudents()
@memberEnrolledMap = {}
for user in @members.models
@memberEnrolledMap[user.id] = user.get('coursePrepaidID')?
@classroomNotEnrolledMap = {}
@totalNotEnrolled = 0
@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
@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
), {}
@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
), {}
true
removeDeletedStudents: (e) ->
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?()
_.remove(classroom.get('members'), (memberID) =>
not @members.get(memberID) or @members.get(memberID)?.get('deleted')
)
true
onInputStudentsInput: ->
@numberOfStudents = Math.max(parseInt(@$('#students-input').val()) or 0, 0)
@updatePrice()
input = @$('#students-input').val()
if input isnt "" and (parseFloat(input) isnt parseInt(input) or _.isNaN parseInt(input))
@$('#students-input').val(@numberOfStudents)
else
@numberOfStudents = Math.max(parseInt(@$('#students-input').val()) or 0, 0)
@updatePrice()
updatePrice: ->
@renderSelectors '#price-form-group'

View file

@ -48,7 +48,7 @@ module.exports = class TeacherClassView extends RootView
@listenTo @classroom, 'sync', ->
@students = new Users()
jqxhrs = @students.fetchForClassroom(@classroom)
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
if jqxhrs.length > 0
@supermodel.trackCollection(@students)
@listenTo @students, 'sync', @sortByName
@ -71,6 +71,7 @@ module.exports = class TeacherClassView extends RootView
@supermodel.trackCollection(@courseInstances)
onLoaded: ->
@removeDeletedStudents()
@classCode = @classroom.get('codeCamel') or @classroom.get('code')
@joinURL = document.location.origin + "/courses?_cc=" + @classCode
@ -130,6 +131,12 @@ module.exports = class TeacherClassView extends RootView
modal = new InviteToClassroomModal({ classroom: @classroom })
@openModalView(modal)
@listenToOnce modal, 'hide', @render
removeDeletedStudents: () ->
_.remove(@classroom.get('members'), (memberID) =>
not @students.get(memberID) or @students.get(memberID)?.get('deleted')
)
true
sortByName: (e) ->
if @sortValue is 'name'
@ -140,7 +147,7 @@ module.exports = class TeacherClassView extends RootView
dir = @sortDirection
@students.comparator = (student1, student2) ->
return (if student1.get('name') < student2.get('name') then -dir else dir)
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
@students.sort()
sortByProgress: (e) ->
@ -162,7 +169,7 @@ module.exports = class TeacherClassView extends RootView
@students.sort()
getSelectedStudentIDs: ->
$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
@$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
$(checkbox).data('student-id')
ensureInstance: (courseID) ->
@ -177,7 +184,7 @@ module.exports = class TeacherClassView extends RootView
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
onClickBulkEnroll: ->
courseID = $('.bulk-course-select').val()
courseID = @$('.bulk-course-select').val()
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
userIDs = @getSelectedStudentIDs().toArray()
selectedUsers = new Users(@students.get(userID) for userID in userIDs)
@ -187,12 +194,21 @@ module.exports = class TeacherClassView extends RootView
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
onClickBulkAssign: ->
courseID = $('.bulk-course-select').val()
courseID = @$('.bulk-course-select').val()
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
members = @getSelectedStudentIDs().filter((index, userID) =>
selectedIDs = @getSelectedStudentIDs()
members = selectedIDs.filter((index, userID) =>
user = @students.get(userID)
user.isEnrolled()
).toArray()
@assigningToUnenrolled = _.any selectedIDs, (userID) =>
not @students.get(userID).isEnrolled()
@$('.cant-assign-to-unenrolled').toggleClass('visible', @assigningToUnenrolled)
@assigningToNobody = selectedIDs.length is 0
@$('.no-students-selected').toggleClass('visible', @assigningToNobody)
if courseInstance
courseInstance.addMembers members, {
@ -220,12 +236,12 @@ module.exports = class TeacherClassView extends RootView
onClickSelectAll: (e) ->
e.preventDefault()
checkboxes = $('.student-checkbox input')
checkboxes = @$('.student-checkbox input')
if _.all(checkboxes, 'checked')
$('.select-all input').prop('checked', false)
@$('.select-all input').prop('checked', false)
checkboxes.prop('checked', false)
else
$('.select-all input').prop('checked', true)
@$('.select-all input').prop('checked', true)
checkboxes.prop('checked', true)
null
@ -235,8 +251,8 @@ module.exports = class TeacherClassView extends RootView
checkbox = $(e.currentTarget).find('input')
checkbox.prop('checked', not checkbox.prop('checked'))
# checkboxes.prop('checked', false)
checkboxes = $('.student-checkbox input')
$('.select-all input').prop('checked', _.all(checkboxes, 'checked'))
checkboxes = @$('.student-checkbox input')
@$('.select-all input').prop('checked', _.all(checkboxes, 'checked'))
onChangeCourseSelect: (e) ->
@selectedCourse = @courses.get($(e.currentTarget).val())

View file

@ -27,9 +27,15 @@ module.exports = class TeacherCoursesView extends RootView
guideLinks:
{
"560f1a9f22961295f9427742": 'http://codecombat.com/docs/CodeCombatTeacherGuideCourse1.pdf'
"5632661322961295f9428638": 'https://docs.google.com/a/codecombat.com/viewer?a=v&pid=sites&srcid=Y29kZWNvbWJhdC5jb218dGVhY2hlci1ndWlkZXN8Z3g6NGEzMDFhZTZmMTg4YmRmZQ'
"56462f935afde0c6fd30fc8c": 'https://docs.google.com/a/codecombat.com/viewer?a=v&pid=sites&srcid=Y29kZWNvbWJhdC5jb218dGVhY2hlci1ndWlkZXN8Z3g6NzY0Nzc1NWRjMTk4MGRiMQ'
"560f1a9f22961295f9427742":
python: 'http://files.codecombat.com/teacherguides/CodeCombat_TeacherGuide_intro_python.pdf'
javascript: 'http://files.codecombat.com/teacherguides/CodeCombat_TeacherGuide_intro_javascript.pdf'
"5632661322961295f9428638":
python: 'http://files.codecombat.com/teacherguides/CodeCombat_TeacherGuide_course-2_python.pdf'
javascript: 'http://files.codecombat.com/teacherguides/CodeCombat_TeacherGuide_course-2_javascript.pdf'
"56462f935afde0c6fd30fc8c":
python: 'http://files.codecombat.com/teacherguides/CodeCombat_TeacherGuide_course-3_python.pdf'
javascript: 'http://files.codecombat.com/teacherguides/CodeCombat_TeacherGuide_course-3_javascript.pdf'
"56462f935afde0c6fd30fc8d": null
"569ed916efa72b0ced971447": null
}

View file

@ -0,0 +1,16 @@
var classrooms = db.classrooms.find();
classrooms.forEach(function (classroom) {
printjson(classroom.members);
classroom.members.forEach(function (userID) {
var user = db.users.findOne({ _id: userID }, { deleted: true });
if (user.deleted) {
db.classrooms.update(
{ _id: classroom._id },
{
$addToSet: { deletedMembers: userID },
$pull: { members: userID },
},
);
}
});
});

View file

@ -59,6 +59,7 @@ module.exports =
memberIDs = memberIDs.slice(memberSkip, memberSkip + memberLimit)
members = yield User.find({ _id: { $in: memberIDs }}).select(parse.getProjectFromReq(req))
# members = yield User.find({ _id: { $in: memberIDs }, deleted: { $ne: true }}).select(parse.getProjectFromReq(req))
memberObjects = (member.toObject({ req: req, includedPrivates: ["name", "email"] }) for member in members)
res.status(200).send(memberObjects)

View file

@ -1,9 +1,13 @@
_ = require 'lodash'
co = require 'co'
errors = require '../commons/errors'
wrap = require 'co-express'
Promise = require 'bluebird'
parse = require '../commons/parse'
request = require 'request'
mongoose = require 'mongoose'
User = require '../models/User'
Classroom = require '../models/Classroom'
module.exports =
@ -36,3 +40,15 @@ module.exports =
user = yield User.findOne({facebookID: fbID})
throw new errors.NotFound('No user with that Facebook ID') unless user
res.status(200).send(user.toObject({req: req}))
removeFromClassrooms: wrap (req, res, next) ->
userID = mongoose.Types.ObjectId(req.user.id)
yield Classroom.update(
{ members: userID }
{
$addToSet: { deletedMembers: userID }
$pull: { members: userID }
}
{ multi: true }
)
next()

View file

@ -56,6 +56,7 @@ module.exports.setup = (app) ->
app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers)
app.delete('/db/user/:handle', mw.users.removeFromClassrooms)
app.get('/db/user', mw.users.fetchByGPlusID, mw.users.fetchByFacebookID)
app.get '/db/products', require('./db/product').get

View file

@ -310,22 +310,35 @@ describe 'GET /db/user', ->
xit 'can fetch another user with restricted fields'
describe 'DELETE /db/user', ->
it 'can delete a user', (done) ->
loginNewUser (user1) ->
beforeDeleted = new Date()
request.del {uri: "#{getURL(urlUser)}/#{user1.id}"}, (err, res) ->
expect(err).toBeNull()
return done() if err
User.findById user1.id, (err, user1) ->
expect(err).toBeNull()
return done() if err
expect(user1.get('deleted')).toBe(true)
expect(user1.get('dateDeleted')).toBeGreaterThan(beforeDeleted)
expect(user1.get('dateDeleted')).toBeLessThan(new Date())
for key, value of user1.toObject()
continue if key in ['_id', 'deleted', 'dateDeleted']
expect(_.isEmpty(value)).toEqual(true)
done()
it 'can delete a user', utils.wrap (done) ->
user = yield utils.initUser()
yield utils.loginUser(user)
beforeDeleted = new Date()
[res, body] = yield request.delAsync {uri: "#{getURL(urlUser)}/#{user.id}"}
user = yield User.findById user.id
expect(user.get('deleted')).toBe(true)
expect(user.get('dateDeleted')).toBeGreaterThan(beforeDeleted)
expect(user.get('dateDeleted')).toBeLessThan(new Date())
for key, value of user.toObject()
continue if key in ['_id', 'deleted', 'dateDeleted']
expect(_.isEmpty(value)).toEqual(true)
done()
it 'moves user to classroom.deletedMembers', utils.wrap (done) ->
user = yield utils.initUser()
user2 = yield utils.initUser()
yield utils.loginUser(user)
classroom = new Classroom({
members: [user._id, user2._id]
})
yield classroom.save()
[res, body] = yield request.delAsync {uri: "#{getURL(urlUser)}/#{user.id}"}
classroom = yield Classroom.findById(classroom.id)
expect(classroom.get('members').length).toBe(1)
expect(classroom.get('deletedMembers').length).toBe(1)
expect(classroom.get('members')[0].toString()).toEqual(user2.id)
expect(classroom.get('deletedMembers')[0].toString()).toEqual(user.id)
done()
describe 'Statistics', ->
LevelSession = require '../../../server/models/LevelSession'

View file

@ -5,5 +5,7 @@ module .exports = new CourseInstances([
_id: "instance0"
courseID: "course0",
classroomID: "active-classroom"
ownerID: "teacher1"
members: (require 'test/app/fixtures/students').map('id')
},
])

View file

@ -9,6 +9,8 @@ NewItemView = require 'views/play/level/modal/NewItemView'
ProgressView = require 'views/play/level/modal/ProgressView'
describe 'CourseVictoryModal', ->
beforeEach ->
me.clear()
it 'will eventually be the only victory modal'

View file

@ -9,12 +9,12 @@ xdescribe 'ActivateLicensesModal', ->
me = require 'test/app/fixtures/teacher'
prepaids = require 'test/app/fixtures/prepaids'
classrooms = require 'test/app/fixtures/classrooms' # TODO: Don't use archived ones
classrooms = require 'test/app/fixtures/classrooms/unarchived-classrooms'
users = require 'test/app/fixtures/students'
responses = {
'/db/prepaid': prepaids.toJSON()
'/db/classroom': classrooms.toJSON()
'/db/users': users.toJSON() # TODO: Respond with different ones for different classrooms
# '/members': users.toJSON() # TODO: Respond with different ones for different classrooms
}
makeModal = (options) ->
@ -24,11 +24,17 @@ xdescribe 'ActivateLicensesModal', ->
@classroom, @users, @selectedUsers
})
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('classroom1')
@classroom = classrooms.get('active-classroom')
@users = require 'test/app/fixtures/students'
afterEach ->
@ -84,26 +90,26 @@ xdescribe 'ActivateLicensesModal', ->
#
#
# describe 'enroll button', ->
# beforeEach (done) ->
# makeModal.bind(this)(done)
#
#
# it 'should display the correct total number of credits', ->
# expect(@modal.$('#total-available').html()).toBe('2')
#
#
# it 'should be disabled when teacher doesn\'t have enough enrollments', ->
# expect(@modal.$('#total-available').html()).toBe('2')
#
#
#
#
#
#
# describe 'when enrolling only a single student', ->
# describe 'the list of students', ->
# it 'should only have the one student selected'
#
#
# describe 'when bulk-enrolling students', ->
# describe 'the list of students', ->
# it 'should have the right students selected'
#
#
# describe 'selecting more students', ->
# it 'should increase the student counter'

View file

@ -0,0 +1,74 @@
TeacherClassView = require 'views/courses/TeacherClassView'
storage = require 'core/storage'
forms = require 'core/forms'
describe '/teachers/classes/:handle', ->
describe 'TeacherClassView', ->
# describe 'when logged out', ->
# it 'responds with 401 error'
# it 'shows Log In and Create Account buttons'
@view = null
# describe "when you don't own the class", ->
# it 'responds with 403 error'
# it 'shows Log Out button'
describe 'when logged in', ->
beforeEach (done) ->
me = require 'test/app/fixtures/teacher'
@classroom = require 'test/app/fixtures/classrooms/active-classroom'
@students = require 'test/app/fixtures/students'
@courses = require 'test/app/fixtures/courses'
@campaigns = require 'test/app/fixtures/campaigns'
@courseInstances = require 'test/app/fixtures/course-instances'
@levelSessions = require 'test/app/fixtures/level-sessions-partially-completed'
@view = new TeacherClassView()
@view.classroom.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@classroom) })
@view.courses.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@courses) })
@view.campaigns.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@campaigns) })
@view.courseInstances.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@courseInstances) })
@view.students.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@students) })
@view.classroom.sessions.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@levelSessions) })
jasmine.demoEl(@view.$el)
_.defer done
it 'has contents', ->
expect(@view.$el.children().length).toBeGreaterThan(0)
# it "shows the classroom's name and description"
# it "shows the classroom's join code"
describe 'the Students tab', ->
# it 'shows all of the students'
# it 'sorts correctly by Name'
# it 'sorts correctly by Progress'
describe 'bulk-assign controls', ->
it 'shows alert when assigning course 2 to unenrolled students', ->
expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(false)
@view.$('.student-row .checkbox-flat').click()
@view.$('.assign-to-selected-students').click()
expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(true)
it 'shows alert when assigning but no students are selected', ->
expect(@view.$('.no-students-selected').hasClass('visible')).toBe(false)
@view.$('.assign-to-selected-students').click()
expect(@view.$('.no-students-selected').hasClass('visible')).toBe(true)
# describe 'the Course Progress tab', ->
# it 'shows the correct Course Overview progress'
#
# describe 'when viewing another course'
# it 'still shows the correct Course Overview progress'
#