diff --git a/app/assets/docs/CodeCombatCoursesGettingStartedGuide.pdf b/app/assets/docs/CodeCombatCoursesGettingStartedGuide.pdf deleted file mode 100644 index ca1436170..000000000 Binary files a/app/assets/docs/CodeCombatCoursesGettingStartedGuide.pdf and /dev/null differ diff --git a/app/assets/docs/CodeCombatHourofCodeGettingStartedGuide.pdf b/app/assets/docs/CodeCombatHourofCodeGettingStartedGuide.pdf deleted file mode 100644 index 7dafb31e5..000000000 Binary files a/app/assets/docs/CodeCombatHourofCodeGettingStartedGuide.pdf and /dev/null differ diff --git a/app/assets/docs/CodeCombatReferencePoster.pdf b/app/assets/docs/CodeCombatReferencePoster.pdf deleted file mode 100644 index f33090e3e..000000000 Binary files a/app/assets/docs/CodeCombatReferencePoster.pdf and /dev/null differ diff --git a/app/assets/docs/CodeCombatTeacherGuideCourse1.pdf b/app/assets/docs/CodeCombatTeacherGuideCourse1.pdf deleted file mode 100644 index 35bfea8cc..000000000 Binary files a/app/assets/docs/CodeCombatTeacherGuideCourse1.pdf and /dev/null differ diff --git a/app/assets/docs/CodeCombatTeacherGuideCourse2.pdf b/app/assets/docs/CodeCombatTeacherGuideCourse2.pdf deleted file mode 100644 index 32dbae708..000000000 Binary files a/app/assets/docs/CodeCombatTeacherGuideCourse2.pdf and /dev/null differ diff --git a/app/collections/Users.coffee b/app/collections/Users.coffee index da270c61a..4fb75d666 100644 --- a/app/collections/Users.coffee +++ b/app/collections/Users.coffee @@ -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 diff --git a/app/locale/en.coffee b/app/locale/en.coffee index c7b513f54..2803f8649 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -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)" diff --git a/app/schemas/models/classroom.schema.coffee b/app/schemas/models/classroom.schema.coffee index a38db25b8..b5a29560c 100644 --- a/app/schemas/models/classroom.schema.coffee +++ b/app/schemas/models/classroom.schema.coffee @@ -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") diff --git a/app/styles/courses/activate-licenses-modal.sass b/app/styles/courses/activate-licenses-modal.sass index 28f05f85e..625734dfe 100644 --- a/app/styles/courses/activate-licenses-modal.sass +++ b/app/styles/courses/activate-licenses-modal.sass @@ -6,3 +6,9 @@ .well max-height: 284px overflow: scroll + + .not-enough-enrollments + color: red + visibility: hidden + &.visible + visibility: visible diff --git a/app/styles/courses/teacher-class-view.sass b/app/styles/courses/teacher-class-view.sass index 932cbc8e8..802207ec5 100644 --- a/app/styles/courses/teacher-class-view.sass +++ b/app/styles/courses/teacher-class-view.sass @@ -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: ', ' diff --git a/app/styles/sales-view.sass b/app/styles/sales-view.sass deleted file mode 100644 index 33dc210c0..000000000 --- a/app/styles/sales-view.sass +++ /dev/null @@ -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 diff --git a/app/templates/courses/activate-licenses-modal.jade b/app/templates/courses/activate-licenses-modal.jade index 2a46095c9..591ef66f9 100644 --- a/app/templates/courses/activate-licenses-modal.jade +++ b/app/templates/courses/activate-licenses-modal.jade @@ -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") diff --git a/app/templates/courses/enrollments-view.jade b/app/templates/courses/enrollments-view.jade index 29a848c9d..47779b36d 100644 --- a/app/templates/courses/enrollments-view.jade +++ b/app/templates/courses/enrollments-view.jade @@ -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 diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 52746391a..79467dc24 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -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 : diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index 7d33eb6bd..853b99681 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -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 | ( diff --git a/app/templates/sales-view.jade b/app/templates/sales-view.jade deleted file mode 100644 index 12715e85f..000000000 --- a/app/templates/sales-view.jade +++ /dev/null @@ -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 diff --git a/app/templates/teachers/request-quote-view.jade b/app/templates/teachers/request-quote-view.jade index 645771c77..b70177758 100644 --- a/app/templates/teachers/request-quote-view.jade +++ b/app/templates/teachers/request-quote-view.jade @@ -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 diff --git a/app/views/SalesView.coffee b/app/views/SalesView.coffee deleted file mode 100644 index be5e5ab86..000000000 --- a/app/views/SalesView.coffee +++ /dev/null @@ -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 diff --git a/app/views/courses/ActivateLicensesModal.coffee b/app/views/courses/ActivateLicensesModal.coffee index 6e2539245..d847c4a32 100644 --- a/app/views/courses/ActivateLicensesModal.coffee +++ b/app/views/courses/ActivateLicensesModal.coffee @@ -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' diff --git a/app/views/courses/ClassroomSettingsModal.coffee b/app/views/courses/ClassroomSettingsModal.coffee index 3abcff452..d42a20bd6 100644 --- a/app/views/courses/ClassroomSettingsModal.coffee +++ b/app/views/courses/ClassroomSettingsModal.coffee @@ -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 \ No newline at end of file + @listenToOnce @classroom, 'sync', @hide diff --git a/app/views/courses/ClassroomView.coffee b/app/views/courses/ClassroomView.coffee index ec3316df3..45d19cdcf 100644 --- a/app/views/courses/ClassroomView.coffee +++ b/app/views/courses/ClassroomView.coffee @@ -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' diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index f62945e1f..68a55f59a 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -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() diff --git a/app/views/courses/EnrollmentsView.coffee b/app/views/courses/EnrollmentsView.coffee index a63fc8695..4f1ee3bfd 100644 --- a/app/views/courses/EnrollmentsView.coffee +++ b/app/views/courses/EnrollmentsView.coffee @@ -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' diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index 066cd575c..fe6fe93a3 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -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()) diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index 4e0517687..3ba95d3e9 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -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 } diff --git a/scripts/mongodb/migrations/2016-04-15-move-deleted-classroom-members.js b/scripts/mongodb/migrations/2016-04-15-move-deleted-classroom-members.js new file mode 100644 index 000000000..67504e72f --- /dev/null +++ b/scripts/mongodb/migrations/2016-04-15-move-deleted-classroom-members.js @@ -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 }, + }, + ); + } + }); +}); diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 13b5e357f..ef6ad0ec6 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -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) diff --git a/server/middleware/users.coffee b/server/middleware/users.coffee index 0744df455..c1add7f00 100644 --- a/server/middleware/users.coffee +++ b/server/middleware/users.coffee @@ -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() diff --git a/server/routes/index.coffee b/server/routes/index.coffee index abea5d999..df98d9c19 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -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 diff --git a/spec/server/functional/user.spec.coffee b/spec/server/functional/user.spec.coffee index 2b5e8fb54..c4ca07c49 100644 --- a/spec/server/functional/user.spec.coffee +++ b/spec/server/functional/user.spec.coffee @@ -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' diff --git a/test/app/fixtures/campaigns.coffee b/test/app/fixtures/campaigns.coffee index 101494934..eaada39b0 100644 --- a/test/app/fixtures/campaigns.coffee +++ b/test/app/fixtures/campaigns.coffee @@ -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({ diff --git a/test/app/fixtures/classrooms.coffee b/test/app/fixtures/classrooms.coffee deleted file mode 100644 index 4229443e6..000000000 --- a/test/app/fixtures/classrooms.coffee +++ /dev/null @@ -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 - } -]) diff --git a/test/app/fixtures/classrooms/active-classroom.coffee b/test/app/fixtures/classrooms/active-classroom.coffee new file mode 100644 index 000000000..326391c32 --- /dev/null +++ b/test/app/fixtures/classrooms/active-classroom.coffee @@ -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' + } +) diff --git a/test/app/fixtures/classrooms/archived-classroom.coffee b/test/app/fixtures/classrooms/archived-classroom.coffee new file mode 100644 index 000000000..eaa0d05d6 --- /dev/null +++ b/test/app/fixtures/classrooms/archived-classroom.coffee @@ -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 + } +) diff --git a/test/app/fixtures/classrooms/classrooms.coffee b/test/app/fixtures/classrooms/classrooms.coffee new file mode 100644 index 000000000..720f7901b --- /dev/null +++ b/test/app/fixtures/classrooms/classrooms.coffee @@ -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' +]) diff --git a/test/app/fixtures/classrooms/empty-classroom.coffee b/test/app/fixtures/classrooms/empty-classroom.coffee new file mode 100644 index 000000000..f806d6eea --- /dev/null +++ b/test/app/fixtures/classrooms/empty-classroom.coffee @@ -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: [] + } +) diff --git a/test/app/fixtures/classrooms/unarchived-classrooms.coffee b/test/app/fixtures/classrooms/unarchived-classrooms.coffee new file mode 100644 index 000000000..49101bfeb --- /dev/null +++ b/test/app/fixtures/classrooms/unarchived-classrooms.coffee @@ -0,0 +1,7 @@ +Classroom = require 'models/Classroom' +Classrooms = require 'collections/Classrooms' + +module.exports = new Classrooms([ + require './active-classroom' + require './empty-classroom' +]) diff --git a/test/app/fixtures/course-instances.coffee b/test/app/fixtures/course-instances.coffee index 9dc33727d..3b0414468 100644 --- a/test/app/fixtures/course-instances.coffee +++ b/test/app/fixtures/course-instances.coffee @@ -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') }, ]) diff --git a/test/app/fixtures/courses.coffee b/test/app/fixtures/courses.coffee index b9a24892f..2ad8e3b07 100644 --- a/test/app/fixtures/courses.coffee +++ b/test/app/fixtures/courses.coffee @@ -1,4 +1,4 @@ -Courses = require 'collections/Courses'; +Courses = require 'collections/Courses' module.exports = new Courses( [ diff --git a/test/app/fixtures/prepaids.coffee b/test/app/fixtures/prepaids.coffee index c1e6f4697..82b7c41a1 100644 --- a/test/app/fixtures/prepaids.coffee +++ b/test/app/fixtures/prepaids.coffee @@ -1,4 +1,4 @@ -Prepaids = require 'collections/Prepaids'; +Prepaids = require 'collections/Prepaids' module.exports = new Prepaids([ { diff --git a/test/app/fixtures/students.coffee b/test/app/fixtures/students.coffee index fafa4ca83..e631b657a 100644 --- a/test/app/fixtures/students.coffee +++ b/test/app/fixtures/students.coffee @@ -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" - } ] ) diff --git a/test/app/fixtures/teacher.coffee b/test/app/fixtures/teacher.coffee index b02837813..2e852763e 100644 --- a/test/app/fixtures/teacher.coffee +++ b/test/app/fixtures/teacher.coffee @@ -1,4 +1,4 @@ -User = require 'models/User'; +User = require 'models/User' module.exports = new User( { diff --git a/test/app/lib/CoursesHelper.spec.coffee b/test/app/lib/CoursesHelper.spec.coffee index 250be7110..ece911977 100644 --- a/test/app/lib/CoursesHelper.spec.coffee +++ b/test/app/lib/CoursesHelper.spec.coffee @@ -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' diff --git a/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee b/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee index 75ae097be..3e0d30696 100644 --- a/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee +++ b/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee @@ -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' diff --git a/test/app/views/teachers/ActivateLicensesModal.spec.coffee b/test/app/views/teachers/ActivateLicensesModal.spec.coffee index 741e5a1d9..518f32786 100644 --- a/test/app/views/teachers/ActivateLicensesModal.spec.coffee +++ b/test/app/views/teachers/ActivateLicensesModal.spec.coffee @@ -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' diff --git a/test/app/views/teachers/TeacherClassView.spec.coffee b/test/app/views/teachers/TeacherClassView.spec.coffee new file mode 100644 index 000000000..e20b749ac --- /dev/null +++ b/test/app/views/teachers/TeacherClassView.spec.coffee @@ -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' + # + + + + +