mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Stuff
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:
parent
e5734cbdd3
commit
e2d08fa7cf
34 changed files with 345 additions and 565 deletions
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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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…
Reference in a new issue