mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-26 22:13:32 -04:00
Merge branch 'dashboard-bugfixes'
This commit is contained in:
commit
79d4fc3126
46 changed files with 417 additions and 632 deletions
app
assets/docs
CodeCombatCoursesGettingStartedGuide.pdfCodeCombatHourofCodeGettingStartedGuide.pdfCodeCombatReferencePoster.pdfCodeCombatTeacherGuideCourse1.pdfCodeCombatTeacherGuideCourse2.pdf
collections
locale
schemas/models
styles
templates
views
scripts/mongodb/migrations
server
spec/server/functional
test/app
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -6,3 +6,9 @@
|
|||
.well
|
||||
max-height: 284px
|
||||
overflow: scroll
|
||||
|
||||
.not-enough-enrollments
|
||||
color: red
|
||||
visibility: hidden
|
||||
&.visible
|
||||
visibility: visible
|
||||
|
|
|
@ -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: ', '
|
||||
|
|
|
@ -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
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 :
|
||||
|
|
|
@ -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
|
||||
| (
|
||||
|
|
|
@ -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
|
|
@ -194,7 +194,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
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Campaign = require 'models/Campaign';
|
||||
Campaigns = require 'collections/Campaigns';
|
||||
Campaign = require 'models/Campaign'
|
||||
Campaigns = require 'collections/Campaigns'
|
||||
|
||||
module.exports = new Campaigns([
|
||||
new Campaign({
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
Classroom = require 'models/Classroom';
|
||||
Classrooms = require 'collections/Classrooms';
|
||||
|
||||
module.exports = new Classrooms([
|
||||
{
|
||||
_id: "classroom0",
|
||||
name: "Teacher Zero's Other Classroom"
|
||||
ownerID: "teacher0",
|
||||
aceConfig:
|
||||
language: 'python'
|
||||
members: []
|
||||
}
|
||||
|
||||
{
|
||||
_id: "classroom1",
|
||||
name: "Teacher Zero's Classroomiest Classroom"
|
||||
members: [
|
||||
"student0",
|
||||
"student1",
|
||||
"student2",
|
||||
"student3",
|
||||
],
|
||||
ownerID: "teacher0",
|
||||
aceConfig:
|
||||
language: 'python'
|
||||
}
|
||||
|
||||
{
|
||||
_id: "classroom_archived",
|
||||
name: "Teacher Zero's Archived Classroom"
|
||||
members: [
|
||||
"student0",
|
||||
"student4",
|
||||
],
|
||||
ownerID: "teacher0",
|
||||
aceConfig:
|
||||
language: 'python'
|
||||
archived: true
|
||||
}
|
||||
])
|
17
test/app/fixtures/classrooms/active-classroom.coffee
Normal file
17
test/app/fixtures/classrooms/active-classroom.coffee
Normal file
|
@ -0,0 +1,17 @@
|
|||
Classroom = require 'models/Classroom'
|
||||
|
||||
module.exports = new Classroom(
|
||||
{
|
||||
_id: "active-classroom",
|
||||
name: "Teacher Zero's Classroomiest Classroom"
|
||||
members: [
|
||||
"student0",
|
||||
"student1",
|
||||
"student2",
|
||||
"student3",
|
||||
],
|
||||
ownerID: "teacher0",
|
||||
aceConfig:
|
||||
language: 'python'
|
||||
}
|
||||
)
|
16
test/app/fixtures/classrooms/archived-classroom.coffee
Normal file
16
test/app/fixtures/classrooms/archived-classroom.coffee
Normal file
|
@ -0,0 +1,16 @@
|
|||
Classroom = require 'models/Classroom'
|
||||
|
||||
module.exports = new Classroom(
|
||||
{
|
||||
_id: "classroom_archived",
|
||||
name: "Teacher Zero's Archived Classroom"
|
||||
members: [
|
||||
"student0",
|
||||
"student3",
|
||||
],
|
||||
ownerID: "teacher0",
|
||||
aceConfig:
|
||||
language: 'python'
|
||||
archived: true
|
||||
}
|
||||
)
|
8
test/app/fixtures/classrooms/classrooms.coffee
Normal file
8
test/app/fixtures/classrooms/classrooms.coffee
Normal file
|
@ -0,0 +1,8 @@
|
|||
Classroom = require 'models/Classroom'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
|
||||
module.exports = new Classrooms([
|
||||
require './active-classroom'
|
||||
require './empty-classroom'
|
||||
require './archived-classroom'
|
||||
])
|
12
test/app/fixtures/classrooms/empty-classroom.coffee
Normal file
12
test/app/fixtures/classrooms/empty-classroom.coffee
Normal file
|
@ -0,0 +1,12 @@
|
|||
Classroom = require 'models/Classroom'
|
||||
|
||||
module.exports = new Classroom(
|
||||
{
|
||||
_id: "classroom0",
|
||||
name: "Teacher Zero's Other Classroom"
|
||||
ownerID: "teacher0",
|
||||
aceConfig:
|
||||
language: 'python'
|
||||
members: []
|
||||
}
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
Classroom = require 'models/Classroom'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
|
||||
module.exports = new Classrooms([
|
||||
require './active-classroom'
|
||||
require './empty-classroom'
|
||||
])
|
|
@ -1,9 +1,11 @@
|
|||
CourseInstances = require 'collections/CourseInstances';
|
||||
CourseInstances = require 'collections/CourseInstances'
|
||||
|
||||
module .exports = new CourseInstances([
|
||||
{
|
||||
_id: "instance0"
|
||||
courseID: "course0",
|
||||
classroomID: "classroom0"
|
||||
courseID: "course0",
|
||||
classroomID: "active-classroom"
|
||||
ownerID: "teacher1"
|
||||
members: (require 'test/app/fixtures/students').map('id')
|
||||
},
|
||||
])
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Courses = require 'collections/Courses';
|
||||
Courses = require 'collections/Courses'
|
||||
|
||||
module.exports = new Courses(
|
||||
[
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Prepaids = require 'collections/Prepaids';
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
|
||||
module.exports = new Prepaids([
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Users = require 'collections/Users';
|
||||
Users = require 'collections/Users'
|
||||
|
||||
module.exports = new Users(
|
||||
[
|
||||
|
@ -21,20 +21,5 @@ module.exports = new Users(
|
|||
_id: "student3"
|
||||
name: "Student Three"
|
||||
}
|
||||
|
||||
{
|
||||
_id: "student4"
|
||||
name: "Student Four"
|
||||
}
|
||||
|
||||
{
|
||||
_id: "student5"
|
||||
name: "Student Five"
|
||||
}
|
||||
|
||||
{
|
||||
_id: "student6"
|
||||
name: "Student Six"
|
||||
}
|
||||
]
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
User = require 'models/User';
|
||||
User = require 'models/User'
|
||||
|
||||
module.exports = new User(
|
||||
{
|
||||
|
|
|
@ -6,14 +6,14 @@ CourseInstances = require 'collections/CourseInstances'
|
|||
Classrooms = require 'collections/Classrooms'
|
||||
|
||||
# These got broken by changes to fixtures :(
|
||||
xdescribe 'CoursesHelper', ->
|
||||
describe 'CoursesHelper', ->
|
||||
|
||||
describe 'calculateAllProgress', ->
|
||||
|
||||
beforeEach ->
|
||||
# classrooms, courses, campaigns, courseInstances, students
|
||||
@classrooms = require 'test/app/fixtures/classrooms'
|
||||
@classroom = @classrooms.models[0]
|
||||
@classroom = require 'test/app/fixtures/classrooms/active-classroom'
|
||||
@classrooms = new Classrooms([ @classroom ])
|
||||
@courses = require 'test/app/fixtures/courses'
|
||||
@course = @courses.models[0]
|
||||
@campaigns = require 'test/app/fixtures/campaigns'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
74
test/app/views/teachers/TeacherClassView.spec.coffee
Normal file
74
test/app/views/teachers/TeacherClassView.spec.coffee
Normal 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'
|
||||
#
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue