diff --git a/app/collections/Users.coffee b/app/collections/Users.coffee index cbc7bd819..da270c61a 100644 --- a/app/collections/Users.coffee +++ b/app/collections/Users.coffee @@ -4,11 +4,20 @@ CocoCollection = require 'collections/CocoCollection' module.exports = class Users extends CocoCollection model: User url: '/db/user' - - fetchForClassroom: (classroom, options) -> - classroom = classroom.id or classroom - options = _.extend({ - url: "/db/classroom/#{classroom}/members" - }, options) - @fetch(options) + fetchForClassroom: (classroom, options={}) -> + classroomID = classroom.id or classroom + limit = 10 + skip = 0 + size = _.size(classroom.get('members')) + options.url = "/db/classroom/#{classroomID}/members" + options.data ?= {} + options.data.memberLimit = limit + options.remove = false + jqxhrs = [] + while skip < size + options = _.cloneDeep(options) + options.data.memberSkip = skip + jqxhrs.push(@fetch(options)) + skip += limit + return jqxhrs diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 133f9c5c7..509aeac80 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -1,10 +1,10 @@ go = (path, options) -> -> @routeDirectly path, arguments, options redirect = (path) -> -> @navigate(path, { trigger: true, replace: true }) - + module.exports = class CocoRouter extends Backbone.Router initialize: -> -# http://nerds.airbnb.com/how-to-add-google-analytics-page-tracking-to-57536 + # http://nerds.airbnb.com/how-to-add-google-analytics-page-tracking-to-57536 @bind 'route', @_trackPageView Backbone.Mediator.subscribe 'router:navigate', @onNavigate, @ @initializeSocialMediaServices = _.once @initializeSocialMediaServices @@ -156,7 +156,7 @@ module.exports = class CocoRouter extends Backbone.Router return @routeDirectly('teachers/RestrictedToTeachersView') if options.studentsOnly and me.isTeacher() return @routeDirectly('courses/RestrictedToStudentsView') - + path = 'play/CampaignView' if window.serverConfig.picoCTF and not /^(views)?\/?play/.test(path) path = "views/#{path}" if not _.string.startsWith(path, 'views/') ViewClass = @tryToLoadModule path @@ -204,13 +204,13 @@ module.exports = class CocoRouter extends Backbone.Router require('core/services/twitter')() renderSocialButtons: => -# TODO: Refactor remaining services to Handlers, use loadAPI success callback + # TODO: Refactor remaining services to Handlers, use loadAPI success callback @initializeSocialMediaServices() $('.share-buttons, .partner-badges').addClass('fade-in').delay(10000).removeClass('fade-in', 5000) application.facebookHandler.renderButtons() application.gplusHandler.renderButtons() twttr?.widgets?.load?() - + activateTab: -> base = _.string.words(document.location.pathname[1..], '/')[0] $("ul.nav li.#{base}").addClass('active') @@ -246,4 +246,4 @@ module.exports = class CocoRouter extends Backbone.Router Backbone.Mediator.publish 'router:navigated', route: fragment reload: -> - document.location.reload() \ No newline at end of file + document.location.reload() diff --git a/app/core/application.coffee b/app/core/application.coffee index 45e6729ec..73c6612f0 100644 --- a/app/core/application.coffee +++ b/app/core/application.coffee @@ -18,8 +18,8 @@ ctrlDefaultPrevented = [219, 221, 80, 83] preventBackspace = (event) -> if event.keyCode is 8 and not elementAcceptsKeystrokes(event.srcElement or event.target) event.preventDefault() - else if (key.ctrl or key.command) and not key.alt and event.keyCode in ctrlDefaultPrevented - console.debug "Prevented keystroke", key + else if (event.ctrlKey or event.metaKey) and not event.altKey and event.keyCode in ctrlDefaultPrevented + console.debug "Prevented keystroke", key, event event.preventDefault() elementAcceptsKeystrokes = (el) -> diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index b5fb0ccb6..0f3d86af7 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -342,6 +342,9 @@ module.exports = class LevelLoader extends CocoClass denormalizeSession: -> return if @headless or @sessionDenormalized or @spectateMode or @sessionless + # This is a way (the way?) PUT /db/level.sessions/undefined was happening + # See commit c242317d9 + return if not @session.id patch = 'levelName': @level.get('name') 'levelID': @level.get('slug') or @level.id diff --git a/app/lib/coursesHelper.coffee b/app/lib/coursesHelper.coffee index 5de87ec91..999cbcfa8 100644 --- a/app/lib/coursesHelper.coffee +++ b/app/lib/coursesHelper.coffee @@ -9,17 +9,20 @@ module.exports = instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id }) continue if not instance instance.numCompleted = 0 + instance.numStarted = 0 campaign = campaigns.get(course.get('campaignID')) for userID in instance.get('members') - allComplete = _.every campaign.getNonLadderLevels().models, (level) -> + levelCompletes = _.map campaign.getNonLadderLevels().models, (level) -> return true if level.isLadder() #TODO: Hella slow! Do the mapping first! session = _.find classroom.sessions.models, (session) -> session.get('creator') is userID and session.get('level').original is level.get('original') # sessionMap[userID][level].completed() session?.completed() - if allComplete + if _.every levelCompletes instance.numCompleted += 1 + if _.any levelCompletes + instance.numStarted += 1 calculateEarliestIncomplete: (classroom, courses, campaigns, courseInstances, students) -> # Loop through all the combinations of things, return the first one that somebody hasn't finished @@ -134,7 +137,7 @@ module.exports = campaign = campaigns.get(course.get('campaignID')) for level in campaign.getNonLadderLevels().models levelID = level.get('original') - progressData[classroom.id][course.id][levelID] = { completed: true, started: false } + progressData[classroom.id][course.id][levelID] = { completed: students.size() > 0, started: false } for user in students.models userID = user.id diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 7b94bdbe7..bd935cc74 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -129,7 +129,6 @@ jobs: "Jobs" schools: "Schools" educator_wiki: "Educator Wiki" - request_quote: "Request a Quote" get_involved: "Get Involved" open_source: "Open source (GitHub)" support: "Support" @@ -1229,7 +1228,8 @@ student_age_range_older: "Older than 18" student_age_range_to: "to" create_class: "Create Class" - + class_name: "Class Name" + teacher_account_restricted: "Your account is a teacher account, and so cannot access student content." teacher: @@ -1273,6 +1273,7 @@ course_progress: "Course Progress" not_applicable: "N/A" edit: "edit" + remove: "remove" latest_completed: "Latest Completed" sort_by: "Sort by" progress: "Progress" @@ -1285,6 +1286,7 @@ add_students_manually: "Add Students Manually" bulk_assign: "Bulk-assign" assign_to_selected_students: "Assign to Selected Students" + assigned: "Assigned" enroll_selected_students: "Enroll Selected Students" guides_coming_soon: "Guides coming soon!" # Courses @@ -1309,7 +1311,6 @@ 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 forever, even if they leave your class." bulk_pricing_blurb: "Purchasing for more than 15 students? Get in touch with us for bulk pricing quotes." - request_quote: "Request Quote" classes: archmage_title: "Archmage" diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index ca76b5255..f0b6d5c76 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -89,6 +89,15 @@ module.exports = class SuperModel extends Backbone.Model trackCollection: (collection, value) -> res = @addModelResource(collection, '', {}, value) res.listen() + + trackRequest: (jqxhr, value=1) -> + res = new Resource('', value) + res.jqxhr = jqxhr + jqxhr.done -> res.markLoaded() + jqxhr.fail -> res.markFailed() + @storeResource(res, value) + + trackRequests: (jqxhrs, value=1) -> @trackRequest(jqxhr) for jqxhr in jqxhrs # replace or overwrite shouldSaveBackups: (model) -> false diff --git a/app/models/User.coffee b/app/models/User.coffee index 9fabce05a..1f00681a5 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -17,6 +17,7 @@ module.exports = class User extends CocoModel isAnonymous: -> @get('anonymous', true) displayName: -> @get('name', true) broadName: -> + return '(deleted)' if @get('deleted') name = @get('name') return name if name name = _.filter([@get('firstName'), @get('lastName')]).join(' ') diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 70e3c718b..bbcb6bec8 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -327,7 +327,7 @@ _.extend UserSchema.properties, description: 'Prepaid which has paid for this user\'s course access' }) schoolName: {type: 'string'} - role: {type: 'string'} # unset, 'student', 'teacher', 'parent', 'technology coordinator', 'advisor', 'principal', 'superintendent', ... + role: {type: 'string', enum: ["God", "advisor", "parent", "principal", "student", "superintendent", "teacher", "technology coordinator"]} c.extendBasicProperties UserSchema, 'user' diff --git a/app/styles/courses/classroom-settings-modal.sass b/app/styles/courses/classroom-settings-modal.sass index 8303da232..b70b05a97 100644 --- a/app/styles/courses/classroom-settings-modal.sass +++ b/app/styles/courses/classroom-settings-modal.sass @@ -3,7 +3,7 @@ width: 50% .age-range-select - width: 100px + width: 170px display: inline-block label diff --git a/app/styles/courses/enrollments-view.sass b/app/styles/courses/enrollments-view.sass index 4bb81304d..51390e5ea 100644 --- a/app/styles/courses/enrollments-view.sass +++ b/app/styles/courses/enrollments-view.sass @@ -6,3 +6,13 @@ line-height: 18px border: thin gray solid border-radius: 5px + + #students-input + width: 100px + height: 50px + line-height: 30px + font-size: 30px + + &::-webkit-inner-spin-button, &::-webkit-outer-spin-button + -webkit-appearance: none + margin: 0 diff --git a/app/styles/courses/teacher-class-view.sass b/app/styles/courses/teacher-class-view.sass index 353646ac1..932cbc8e8 100644 --- a/app/styles/courses/teacher-class-view.sass +++ b/app/styles/courses/teacher-class-view.sass @@ -20,6 +20,10 @@ h3 ~ .edit-classroom color: black text-decoration: underline + + .classroom-details + .small-details + margin-bottom: 4px .concept & span @@ -141,6 +145,12 @@ .glyphicon color: $gray-light + .remove-student-link + color: $burgandy + font-weight: bold + text-decoration: underline + line-height: 16px + // Course Progress tab #course-progress-tab diff --git a/app/styles/style-flat.sass b/app/styles/style-flat.sass index 9f76fe56b..1da0f1fb8 100644 --- a/app/styles/style-flat.sass +++ b/app/styles/style-flat.sass @@ -164,6 +164,13 @@ $forest: #20572B border: $gold 3px solid width: 33px height: 33px + + // For teacher avatars + .border-burgandy + border-color: $burgandy + + .border-navy + border-color: $navy .user-level position: absolute @@ -229,6 +236,18 @@ $forest: #20572B .btn-lg font-size: 18px + .btn-gplus + color: white + background-color: #DD4B39 + img + height: 22px + + .btn-facebook + color: white + background-color: #3B5998 + img + height: 22px + // Dropdowns select height: 33px @@ -346,18 +365,6 @@ $forest: #20572B opacity: 1 - .btn-gplus - color: white - background-color: #DD4B39 - img - height: 22px - - .btn-facebook - color: white - background-color: #3B5998 - img - height: 22px - // Classes .text-navy @@ -378,7 +385,7 @@ $forest: #20572B $spacer: 1rem !default $spacer-x: $spacer !default $spacer-y: $spacer !default - $spacers: ( 0: ( x: 0, y: 0 ), 1: ( x: $spacer-x, y: $spacer-y ), 2: ( x: ($spacer-x * 1.5), y: ($spacer-y * 1.5) ), 3: ( x: ($spacer-x * 3), y: ($spacer-y * 3) ) ) !default + $spacers: ( 0: ( x: 0, y: 0 ), 1: ( x: $spacer-x, y: $spacer-y ), 2: ( x: ($spacer-x * 1.5), y: ($spacer-y * 1.5) ), 3: ( x: ($spacer-x * 3), y: ($spacer-y * 3) ), 4: ( x: ($spacer-x * 4), y: ($spacer-y * 4) ), 5: ( x: ($spacer-x * 5), y: ($spacer-y * 5) ) ) !default .m-x-auto margin-right: auto !important diff --git a/app/templates/base-flat.jade b/app/templates/base-flat.jade index 02aa82ecf..3cb408354 100644 --- a/app/templates/base-flat.jade +++ b/app/templates/base-flat.jade @@ -1,18 +1,5 @@ .style-flat block header - .container-fluid.text-center - .alert.alert-danger.lt-ie9 - strong(data-i18n="home.no_ie") - - if view.isIPadBrowser() || view.isMobile() - .alert.alert-danger.mobile - strong(data-i18n="home.no_mobile") - else if view.isOldBrowser() - .alert.alert-danger.old-browser - strong(data-i18n="home.old_browser") - br - span(data-i18n="home.old_browser_suffix") - nav#main-nav.navbar.navbar-default .container .row @@ -46,13 +33,13 @@ else li.dropdown a.dropdown-toggle(href="#", data-toggle="dropdown" role="button" aroa-haspopup="true" aria-expanded="false") - img.img-circle.img-circle-small.m-r-1(src=me.getPhotoURL()) + img.img-circle.img-circle-small.m-r-1(src=me.getPhotoURL() class=(me.isTeacher() ? 'border-navy' : '')) span.spr My Account ul.dropdown-menu li.user-dropdown-header.text-center span.user-level= me.level() a(href="/user/#{me.getSlugOrID()}") - img.img-circle(src=me.getPhotoURL()) + img.img-circle(src=me.getPhotoURL() class=(me.isTeacher() ? 'border-navy' : '')) h5=me.displayName() li a(href="/user/#{me.getSlugOrID()}", data-i18n="nav.profile") @@ -106,11 +93,11 @@ li strong(data-i18n="nav.schools") li - a(href="/courses/teachers", data-i18n="nav.teachers") + a(href="/teachers/classes", data-i18n="nav.teachers") li a(href="https://sites.google.com/a/codecombat.com/teacher-guides/", data-i18n="nav.educator_wiki") li - a(href="/teachers/quote", data-i18n="nav.request_quote") + a(href="/teachers/demo", data-i18n="teachers_quote.title") .col-sm-3 ul.list-unstyled diff --git a/app/templates/core/loading-error.jade b/app/templates/core/loading-error.jade index 23b0258ae..08b254d04 100644 --- a/app/templates/core/loading-error.jade +++ b/app/templates/core/loading-error.jade @@ -86,7 +86,7 @@ li a.login-btn(data-i18n="login.log_in") li - a(href="/courses/teachers", data-i18n="nav.create_a_class") + a(href="/teachers/classes", data-i18n="nav.create_a_class") .col-sm-3 ul.list-unstyled diff --git a/app/templates/courses/classroom-settings-modal.jade b/app/templates/courses/classroom-settings-modal.jade index e3f21b72e..9f38e23c2 100644 --- a/app/templates/courses/classroom-settings-modal.jade +++ b/app/templates/courses/classroom-settings-modal.jade @@ -9,7 +9,7 @@ block modal-header-content block modal-body-content form .form-group - label(data-i18n="general.name") + label(data-i18n="courses.class_name") input#name-input.form-control(name="name" type='text' value=view.classroom.get('name')) .form-group @@ -26,13 +26,13 @@ block modal-body-content select#programming-language-select.form-control(name="language" value=aceConfig.language disabled=languageDisabled) - var aceConfig = view.classroom.get('aceConfig') || {}; option(value="" data-i18n="courses.language_select") - option(value="python", data-i18n="courses.learn_p") - option(value="javascript", data-i18n="courses.learn_j") + option(value="python") Python + option(value="javascript") JavaScript .form-group label span(data-i18n="courses.avg_student_exp_label") - span.spl.text-muted(data-i18n="signup.optional") + i.spl.text-muted(data-i18n="signup.optional") .help-block.small.text-navy(data-i18n="courses.avg_student_exp_desc") select.form-control(name="averageStudentExp", value=view.classroom.get('averageStudentExp')) option(value="" data-i18n="courses.avg_student_exp_select") diff --git a/app/templates/courses/classroom-view.jade b/app/templates/courses/classroom-view.jade index 0066a3ef1..acbdd7e88 100644 --- a/app/templates/courses/classroom-view.jade +++ b/app/templates/courses/classroom-view.jade @@ -2,9 +2,16 @@ extends /templates/base block content + if !me.isAnonymous() && (me.isTeacher() || view.ownedClassrooms.size()) + .alert.alert-danger.text-center + // DNT: Temporary + h3 ATTENTION TEACHERS: + p We are transitioning to a new classroom management system; this page will soon be student-only. + a(href="/teachers/classes") Go to teachers area. + - var isOwner = view.classroom ? view.classroom.get('ownerID') === me.id : false; if isOwner - a(href="/courses/teachers", data-i18n="courses.back_classrooms") + a(href="/teachers/classes", data-i18n="courses.back_classrooms") else a(href="/courses", data-i18n="courses.back_courses") diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index b9b25f9d0..5df87a69a 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -2,8 +2,15 @@ extends /templates/base block content + if me.isTeacher() || view.ownedClassrooms.size() + .alert.alert-danger.text-center + // DNT: Temporary + h3 ATTENTION TEACHERS: + p We are transitioning to a new classroom management system; this page will soon be student-only. + a(href="/teachers/classes") Go to teachers area. + if view.teacherMode - a(href="/courses/teachers", data-i18n="courses.back_classrooms") + a(href="/teachers/classes", data-i18n="courses.back_classrooms") else a(href="/courses", data-i18n="courses.back_courses") br diff --git a/app/templates/courses/courses-view.jade b/app/templates/courses/courses-view.jade index f564a5ac5..bfb028bb8 100644 --- a/app/templates/courses/courses-view.jade +++ b/app/templates/courses/courses-view.jade @@ -1,13 +1,21 @@ extends /templates/base block content + + if me.isTeacher() || view.ownedClassrooms.size() + .alert.alert-danger.text-center + // DNT: Temporary + h3 ATTENTION TEACHERS: + p We are transitioning to a new classroom management system; this page will soon be student-only. + a(href="/teachers/classes") Go to teachers area. + h3.text-right if me.isAnonymous() a(href="/teachers/signup") span(data-i18n="courses.teachers_click") span ! else - a(href="/courses/teachers") + a(href="/teachers/classes") span(data-i18n="courses.teachers_click") span ! diff --git a/app/templates/courses/enrollments-view.jade b/app/templates/courses/enrollments-view.jade index daa4d6f40..97a553292 100644 --- a/app/templates/courses/enrollments-view.jade +++ b/app/templates/courses/enrollments-view.jade @@ -4,21 +4,47 @@ block page_nav include ./teacher-dashboard-nav.jade block content - .container - h3(data-i18n='teacher.enrollments') - h4 - span(data-i18n='teacher.enrollments_blurb_1') - span 2–8 - span(data-i18n='teacher.enrollments_blurb_2') + if me.isAnonymous() || (!me.isTeacher() && !view.classrooms.size()) + .access-restricted.container.text-center.m-y-3 + h5(data-i18n='teacher.access_restricted') + p(data-i18n='teacher.teacher_account_required') + if me.isAnonymous() + .login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in') + a.btn.btn-lg.btn-primary-alt(href="/teachers/signup" data-i18n='teacher.create_teacher_account') + else + a.btn.btn-lg.btn-primary(href="/teachers/convert" data-i18n="teachers_quote.convert_account_title") + button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") + + .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 + h5(data-i18n='teacher.what_is_a_teacher_account') + p(data-i18n='teacher.teacher_account_explanation') + + else + if !me.isTeacher() + .alert.alert-danger.text-center + .container + // DNT: Temporary + h3 ATTENTION: Please upgrade your account to a Teacher Account. + p + | We are transitioning to a new improved classroom management system for instructors. + | Please convert your account to ensure you retain access to your classrooms. + a.btn.btn-primary.btn-lg(href="/teachers/convert") Upgrade to teacher account - .row - .col-xs-4 - +enrollmentStats - .col-xs-4 - +addCredits - .col-xs-3.col-xs-offset-1 - +howToEnroll - +quoteSection + .container.m-t-5 + h3(data-i18n='teacher.enrollments') + h4 + span(data-i18n='teacher.enrollments_blurb_1') + span 2–8 + span(data-i18n='teacher.enrollments_blurb_2') + + .row.m-t-3 + .col-xs-4 + +enrollmentStats + .col-xs-4 + +addCredits + .col-xs-3.col-xs-offset-1 + +howToEnroll + +quoteSection mixin enrollmentStats h5 @@ -43,9 +69,9 @@ mixin enrollmentStats mixin addCredits .text-center h5(data-i18n='teacher.add_enrollment_credits') - div + div.m-t-1 input#students-input.text-center.enrollment-count(value=view.numberOfStudents type='number') - div + div.m-t-1 if view.state === 'purchasing' .purchase-now.btn.btn-lg.btn-forest.disabled span(data-i18n='teacher.purchasing') @@ -61,12 +87,12 @@ mixin howToEnroll .text-center b(data-i18n='teacher.how_to_enroll') ol - li(data-i18n='teacher.how_to_enroll_blurb_1') - li(data-i18n='teacher.how_to_enroll_blurb_2') - li(data-i18n='teacher.how_to_enroll_blurb_3') + li.m-t-1(data-i18n='teacher.how_to_enroll_blurb_1') + li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_2') + li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_3') mixin quoteSection - .text-center + .text-center.m-t-5 h4(data-i18n='teacher.bulk_pricing_blurb') - a.request-quote.btn.btn-lg.btn-navy(href='/teachers/quote') - span(data-i18n='teacher.request_quote') + a.request-quote.btn.btn-lg.btn-navy.m-t-2(href='/teachers/demo') + span(data-i18n='teachers_quote.title') diff --git a/app/templates/courses/hour-of-code-view.jade b/app/templates/courses/hour-of-code-view.jade index 6523dc86a..c37ecdce1 100644 --- a/app/templates/courses/hour-of-code-view.jade +++ b/app/templates/courses/hour-of-code-view.jade @@ -7,7 +7,7 @@ block content span(data-i18n="courses.teachers_click") span ! else - a(href="/courses/teachers") + a(href="/teachers/classes") span(data-i18n="courses.teachers_click") span ! br diff --git a/app/templates/courses/purchase-courses-view.jade b/app/templates/courses/purchase-courses-view.jade index 214f9846e..826f072b0 100644 --- a/app/templates/courses/purchase-courses-view.jade +++ b/app/templates/courses/purchase-courses-view.jade @@ -17,7 +17,7 @@ block content if view.fromClassroom a(href="/courses/"+view.fromClassroom, data-i18n="courses.return_to_class") else - a(href="/courses/teachers", data-i18n="courses.return_to_course_man") + a(href="/teachers/classes", data-i18n="courses.return_to_course_man") else diff --git a/app/templates/courses/student-sign-up-modal.jade b/app/templates/courses/student-sign-up-modal.jade index 04576543d..8454e228d 100644 --- a/app/templates/courses/student-sign-up-modal.jade +++ b/app/templates/courses/student-sign-up-modal.jade @@ -41,11 +41,6 @@ block modal-body-content #errors-alert.alert.alert-danger.hide .text-center - if view.willPlay - input#sign-up-btn.btn.btn-default(type="submit", data-i18n="[value]courses.start_playing", value="Start Playing") - p - a#skip-link(data-i18n="courses.skip_this") - else - input#sign-up-btn.btn.btn-default(data-i18n="[value]signup.sign_up", type="submit") + input#sign-up-btn.btn.btn-default(data-i18n="[value]signup.sign_up", type="submit") block modal-footer-content diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index e8084c0a3..1fb9ef091 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -5,18 +5,28 @@ block page_nav block content - var classroom = view.classroom + if !me.isTeacher() + .alert.alert-danger.text-center + .container + // DNT: Temporary + h3 ATTENTION: Please upgrade your account to a Teacher Account. + p + | We are transitioning to a new improved classroom management system for instructors. + | Please convert your account to ensure you retain access to your classrooms. + a.btn.btn-primary.btn-lg(href="/teachers/convert") Upgrade to teacher account + if classroom.loaded .container +breadcrumbs - h3= classroom.get('name') + h3.m-t-2= classroom.get('name') a.label.edit-classroom(data-classroom-id=classroom.id) span(data-i18n='teacher.edit_class_settings') h4= classroom.get('description') - .classroom-info-row.row + .classroom-info-row.row.m-t-5 .classroom-details.col-md-3 - var stats = view.classStats() - h4(data-i18n='teacher.class_overview') + h4.m-b-2(data-i18n='teacher.class_overview') .language.small-details span(data-i18n='teacher.language') @@ -61,28 +71,30 @@ block content //- span(data-i18n='concepts.'+name) .completeness-info.col-md-4 + h4.m-b-2 +   if view.earliestIncompleteLevel - div + div.small-details span(data-i18n='teacher.earliest_incomplete') span : +longLevelName(view.earliestIncompleteLevel) +inlineUserList(view.earliestIncompleteLevel.users) if view.latestCompleteLevel - div + div.small-details.m-t-3 span(data-i18n='teacher.latest_complete') span : +longLevelName(view.latestCompleteLevel) +inlineUserList(view.latestCompleteLevel.users) .adding-students.col-md-5 - h4 + h4.m-b-2 span(data-i18n='teacher.adding_students') span : +copyCodes +addStudentsButton - ul.nav.nav-tabs(role='tablist') + ul.nav.nav-tabs.m-t-5(role='tablist') li.active a(href='#students-tab' data-toggle='tab') .small-details.text-center(data-i18n='teacher.students') @@ -162,6 +174,8 @@ mixin studentRow(student) label.checkmark(for='checkbox-student-' + student.id) td.student-info-col .student-info + if student.get('deleted') + em (deleted) div.student-name= student.get('name') div.student-email.small-details= student.get('email') td.hidden @@ -186,6 +200,10 @@ mixin studentRow(student) +enrollStudentButton(student) //- td //- span.view-class-arrow.glyphicon.glyphicon-chevron-right + td + a.remove-student-link.small.center-block.text-center.pull-right.m-r-2(data-student-id=student.id) + div.glyphicon.glyphicon-remove + div(data-i18n='teacher.remove') mixin enrollStudentButton(student) a.enroll-student-button.btn.btn-lg.btn-primary(data-classroom-id=view.classroom.id data-user-id=student.id) @@ -227,7 +245,7 @@ mixin studentLevelsRow(student) .student-levels-row.alternating-background div.student-info div.student-name= student.get('name') - div.student-email.small-details emailaddress@school.edu + div.student-email.small-details= student.get('email') div.student-levels-progress - var course = view.selectedCourse - var campaign = view.campaigns.get(course.get('campaignID')) @@ -250,14 +268,14 @@ mixin progressDotLabel(label) = label mixin copyCodes - div.copy-button-group.form-inline + div.copy-button-group.form-inline.m-b-3 .form-group input.text-h4.semibold#join-code-input(value=view.classCode) button#copy-code-btn.form-control.btn.btn-lg.btn-forest span(data-i18n='teacher.copy_class_code') div.text-center.small(data-i18n='teacher.class_code_blurb') - div.copy-button-group.form-inline + div.copy-button-group.form-inline.m-b-3 .form-group input.form-control.text-h4.semibold#join-url-input(value=view.joinURL) button#copy-url-btn.form-control.btn.btn-lg.btn-forest diff --git a/app/templates/courses/teacher-classes-view.jade b/app/templates/courses/teacher-classes-view.jade index 4eb65769c..4fc51cf6a 100644 --- a/app/templates/courses/teacher-classes-view.jade +++ b/app/templates/courses/teacher-classes-view.jade @@ -4,24 +4,51 @@ block page_nav include ./teacher-dashboard-nav.jade block content - .container - h3(data-i18n='teacher.current_classes') + if !me.isTeacher() && !view.classrooms.size() + .access-restricted.container.text-center.m-y-3 + h5(data-i18n='teacher.access_restricted') + p(data-i18n='teacher.teacher_account_required') + if me.isAnonymous() + .login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in') + a.btn.btn-lg.btn-primary-alt(href="/teachers/signup" data-i18n='teacher.create_teacher_account') + else + a.btn.btn-lg.btn-primary(href="/teachers/convert" data-i18n="teachers_quote.convert_account_title") + button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") + + .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 + h5(data-i18n='teacher.what_is_a_teacher_account') + p(data-i18n='teacher.teacher_account_explanation') + + else + if !me.isTeacher() + .alert.alert-danger.text-center + .container + // DNT: Temporary + h3 ATTENTION: Please upgrade your account to a Teacher Account. + p + | We are transitioning to a new improved classroom management system for instructors. + | Please convert your account to ensure you retain access to your classrooms. + a.btn.btn-primary.btn-lg(href="/teachers/convert") Upgrade to teacher account + + .container + h3(data-i18n='teacher.current_classes') + + .classes.container + // Loop each class + each classroom in view.classrooms.models + unless classroom.get('archived') + +classRow(classroom) + + +createClassButton - .classes.container - // Loop each class - each classroom in view.classrooms.models - unless classroom.get('archived') - +classRow(classroom) + - var archivedClassrooms = view.classrooms.where({archived: true}); + if _.size(archivedClassrooms) + .container + h3(data-i18n='teacher.archived_classes') + p(data-i18n='teacher.archived_classes_blurb') - +createClassButton - - .container - h3(data-i18n='teacher.archived_classes') - h4(data-i18n='teacher.archived_classes_blurb') - - .classes.container - each classroom in view.classrooms.models - if classroom.get('archived') + .classes.container + each classroom in archivedClassrooms +archivedClassRow(classroom) mixin classRow(classroom) @@ -76,9 +103,11 @@ mixin progressDot(classroom, course, index) - var total = classroom.get('members').length - var complete = 0; - var dotClass = ''; + - var started = 0; if courseInstance - complete = courseInstance.numCompleted - - dotClass = complete === total ? 'forest' : 'gold'; + - started = courseInstance.numStarted + - dotClass = complete === total ? 'forest' : started ? 'gold' : ''; - var progressDotContext = {total: total, complete: complete}; .progress-dot(class=dotClass, data-title=view.progressDotTemplate(progressDotContext)) +progressDotLabel(index) diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index 5be4ad56b..2239a7780 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -4,6 +4,16 @@ block page_nav include ./teacher-dashboard-nav.jade block content + if !me.isTeacher() && view.ownedClassrooms.size() + .alert.alert-danger.text-center + .container + // DNT: Temporary + h3 ATTENTION: Please upgrade your account to a Teacher Account. + p + | We are transitioning to a new improved classroom management system for instructors. + | Please convert your account to ensure you retain access to your classrooms. + a.btn.btn-primary.btn-lg(href="/teachers/convert") Upgrade to teacher account + .container h1(data-i18n="courses.title") h2(data-i18n="courses.subtitle") @@ -61,13 +71,15 @@ mixin course-info(course) span(data-i18n="concepts." + concept) if course.get('concepts').indexOf(concept) !== course.get('concepts').length - 1 span.spr , - 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' : '')) - span(data-i18n="courses.view_guide_online") - else - i.small - | ( - span(data-i18n='teacher.guides_coming_soon') - | ) + + if me.isTeacher() && view.ownedClassrooms.size() + 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' : '')) + span(data-i18n="courses.view_guide_online") + else + i.small + | ( + span(data-i18n='teacher.guides_coming_soon') + | ) diff --git a/app/templates/new-home-view.jade b/app/templates/new-home-view.jade index ca333e175..e411be401 100644 --- a/app/templates/new-home-view.jade +++ b/app/templates/new-home-view.jade @@ -78,12 +78,12 @@ mixin box div button.teacher-btn.btn.btn-forest.btn-lg.btn-block(data-i18n="new_home.goto_classes") div - if false + if view.isTeacherWithDemo h6(data-i18n="new_home.check_out_wiki") a.btn.btn-primary.btn-lg.btn-block(href="https://sites.google.com/a/codecombat.com/teacher-guides/course-guides", data-i18n="new_home.educator_wiki") else h6(data-i18n="new_home.want_coco") - a.btn.btn-primary.btn-lg.btn-block(href="/teachers/convert", data-i18n="new_home.get_started") + a.btn.btn-primary.btn-lg.btn-block(href=view.demoRequestURL, data-i18n="new_home.get_started") else if view.justPlaysCourses() div @@ -258,7 +258,7 @@ block content span(data-i18n="new_home.agency") .request-demo-row.text-center - if me.isTeacher() + if view.isTeacherWithDemo h3(data-i18n="new_home.get_started_title") else h3(data-i18n="new_home.request_demo_title") @@ -277,14 +277,14 @@ block content .clearfix.hidden-xs small(data-i18n="new_home.teacher_screenshots_hint") - if me.isTeacher() + if view.isTeacherWithDemo h4(data-i18n="new_home.get_started_subtitle") div - a.btn.btn-primary.btn-lg(href="/courses/teachers", data-i18n="new_home.setup_a_class") + a.btn.btn-primary.btn-lg(href="/teachers/classes", data-i18n="new_home.setup_a_class") else h4(data-i18n="new_home.request_demo_subtitle") div - a.btn.btn-primary.btn-lg(href="/teachers/demo", data-i18n="new_home.request_demo") + a.btn.btn-primary.btn-lg(href=view.demoRequestURL, data-i18n="new_home.request_demo") if me.isAnonymous() .have-an-account span.spr(data-i18n="new_home.have_an_account") @@ -371,12 +371,12 @@ block content .request-demo-row.text-center h3(data-i18n="new_home.run_class") - if me.isTeacher() + if view.isTeacherWithDemo div - a.btn.btn-primary.btn-lg(href="/courses/teachers", data-i18n="new_home.setup_a_class") + a.btn.btn-primary.btn-lg(href="/teachers/classes", data-i18n="new_home.setup_a_class") else div - a.btn.btn-primary.btn-lg(href="/teachers/demo", data-i18n="new_home.request_demo") + a.btn.btn-primary.btn-lg(href=view.demoRequestURL, data-i18n="new_home.request_demo") if me.isAnonymous() .have-an-account span.spr(data-i18n="new_home.have_an_account") diff --git a/app/templates/sales-view.jade b/app/templates/sales-view.jade index 044f1ceeb..12715e85f 100644 --- a/app/templates/sales-view.jade +++ b/app/templates/sales-view.jade @@ -28,7 +28,7 @@ block content a.btn-login-account Log in here. else .text-center - a.btn-enter-courses(href="/courses/teachers") set up a free class + a.btn-enter-courses(href="/teachers/classes") set up a free class p.text-center a(href='#getting-started') diff --git a/app/templates/teachers/convert-to-teacher-account-view.jade b/app/templates/teachers/convert-to-teacher-account-view.jade index 1a24be1ea..bafdf37f2 100644 --- a/app/templates/teachers/convert-to-teacher-account-view.jade +++ b/app/templates/teachers/convert-to-teacher-account-view.jade @@ -122,14 +122,14 @@ block content label input(type="checkbox" name="educationLevel" value="Elementary") span(data-i18n="teachers_quote.elementary_school") - .checkbox - label - input(type="checkbox" name="educationLevel" value="High") - span(data-i18n="teachers_quote.high_school") .checkbox label input(type="checkbox" name="educationLevel" value="Middle") span(data-i18n="teachers_quote.middle_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="High") + span(data-i18n="teachers_quote.high_school") .checkbox label input(type="checkbox" name="educationLevel" value="College+") diff --git a/app/templates/teachers/create-teacher-account-view.jade b/app/templates/teachers/create-teacher-account-view.jade index 6bc0e7b28..0b7e9f4ca 100644 --- a/app/templates/teachers/create-teacher-account-view.jade +++ b/app/templates/teachers/create-teacher-account-view.jade @@ -141,14 +141,14 @@ block content label input(type="checkbox" name="educationLevel" value="Elementary") span(data-i18n="teachers_quote.elementary_school") - .checkbox - label - input(type="checkbox" name="educationLevel" value="High") - span(data-i18n="teachers_quote.high_school") .checkbox label input(type="checkbox" name="educationLevel" value="Middle") span(data-i18n="teachers_quote.middle_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="High") + span(data-i18n="teachers_quote.high_school") .checkbox label input(type="checkbox" name="educationLevel" value="College+") diff --git a/app/templates/teachers/request-quote-view.jade b/app/templates/teachers/request-quote-view.jade index 5385fa24a..8852838af 100644 --- a/app/templates/teachers/request-quote-view.jade +++ b/app/templates/teachers/request-quote-view.jade @@ -144,14 +144,14 @@ block content label input(type="checkbox" name="educationLevel" value="Elementary") span(data-i18n="teachers_quote.elementary_school") - .checkbox - label - input(type="checkbox" name="educationLevel" value="High") - span(data-i18n="teachers_quote.high_school") .checkbox label input(type="checkbox" name="educationLevel" value="Middle") span(data-i18n="teachers_quote.middle_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="High") + span(data-i18n="teachers_quote.high_school") .checkbox label input(type="checkbox" name="educationLevel" value="College+") @@ -202,7 +202,7 @@ block content .row .col-md-offset-2.col-md-4 .form-group - label.control-label(data-i18n="general.name") + label.control-label(data-i18n="general.username") input.form-control(name="name") .row diff --git a/app/templates/teachers/restricted-to-teachers-view.jade b/app/templates/teachers/restricted-to-teachers-view.jade index 691ccc710..52dde8622 100644 --- a/app/templates/teachers/restricted-to-teachers-view.jade +++ b/app/templates/teachers/restricted-to-teachers-view.jade @@ -9,7 +9,7 @@ block content p(data-i18n='teacher.teacher_account_required') if me.isAnonymous() .login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in') - .teacher-signup-button.btn.btn-lg.btn-primary-alt(data-i18n='teacher.create_teacher_account') + a.btn.btn-lg.btn-primary-alt(href="/teachers/signup" data-i18n='teacher.create_teacher_account') else a.btn.btn-lg.btn-primary(href="/teachers/convert" data-i18n="teachers_quote.convert_account_title") button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") diff --git a/app/views/NewHomeView.coffee b/app/views/NewHomeView.coffee index 62b9350d8..9b0490d3c 100644 --- a/app/views/NewHomeView.coffee +++ b/app/views/NewHomeView.coffee @@ -1,6 +1,8 @@ RootView = require 'views/core/RootView' template = require 'templates/new-home-view' CocoCollection = require 'collections/CocoCollection' +TrialRequest = require 'models/TrialRequest' +TrialRequests = require 'collections/TrialRequests' Course = require 'models/Course' utils = require 'core/utils' storage = require 'core/storage' @@ -39,6 +41,11 @@ module.exports = class NewHomeView extends RootView if @getQueryVariable 'hour_of_code' application.router.navigate "/hoc", trigger: true + if me.isTeacher() + @trialRequests = new TrialRequests() + @trialRequests.fetchOwn() + @supermodel.loadCollection(@trialRequests) + isHourOfCodeWeek = false # Temporary: default to /hoc flow during the main event week if isHourOfCodeWeek and (@isNewPlayer() or (@justPlaysCourses() and me.isAnonymous())) # Go/return straight to playing single-player HoC course on Play click @@ -53,6 +60,12 @@ module.exports = class NewHomeView extends RootView else @playURL = '/play' + onLoaded: -> + @trialRequest = @trialRequests.first() if @trialRequests?.size() + @isTeacherWithDemo = @trialRequest and @trialRequest.get('status') in ['approved', 'submitted'] + @demoRequestURL = if me.isTeacher() then '/teachers/convert' else '/teachers/demo' + super() + onClickPlayButton: (e) -> @playSound 'menu-button-click' e.preventDefault() diff --git a/app/views/TeachersView.coffee b/app/views/TeachersView.coffee index d9d95a733..f2e8cea37 100644 --- a/app/views/TeachersView.coffee +++ b/app/views/TeachersView.coffee @@ -20,7 +20,7 @@ module.exports = class TeachersView extends RootView application.router.navigate "/schools", trigger: true unless me.isAnonymous() _.defer -> - application.router.navigate "/courses/teachers", trigger: true + application.router.navigate "/teachers/courses", trigger: true onClickLogin: (e) -> @openModalView new AuthModal() if me.get('anonymous') diff --git a/app/views/courses/ActivateLicensesModal.coffee b/app/views/courses/ActivateLicensesModal.coffee index cf6bdd1d7..6e2539245 100644 --- a/app/views/courses/ActivateLicensesModal.coffee +++ b/app/views/courses/ActivateLicensesModal.coffee @@ -29,8 +29,8 @@ module.exports = class ActivateLicensesModal extends ModalView success: => @classrooms.each (classroom) => classroom.users = new Users() - classroom.users.fetchForClassroom(classroom) - @supermodel.trackCollection(classroom.users) + jqxhrs = classroom.users.fetchForClassroom(classroom) + @supermodel.trackRequests(jqxhrs) }) @supermodel.trackCollection(@classrooms) diff --git a/app/views/courses/ClassroomView.coffee b/app/views/courses/ClassroomView.coffee index 60b1f0c9e..ec3316df3 100644 --- a/app/views/courses/ClassroomView.coffee +++ b/app/views/courses/ClassroomView.coffee @@ -3,6 +3,7 @@ CocoCollection = require 'collections/CocoCollection' Course = require 'models/Course' CourseInstance = require 'models/CourseInstance' Classroom = require 'models/Classroom' +Classrooms = require 'collections/Classrooms' LevelSession = require 'models/LevelSession' Prepaids = require 'collections/Prepaids' RootView = require 'views/core/RootView' @@ -46,11 +47,14 @@ module.exports = class ClassroomView extends RootView @prepaids.comparator = '_id' @prepaids.fetchByCreator(me.id) @supermodel.loadCollection(@prepaids) - @users = new CocoCollection([], { url: "/db/classroom/#{classroomID}/members", model: User }) + @users = new CocoCollection([], { url: "/db/classroom/#{classroomID}/members?memberLimit=100", model: User }) @users.comparator = (user) => user.broadName().toLowerCase() @supermodel.loadCollection(@users) @listenToOnce @courseInstances, 'sync', @onCourseInstancesSync @sessions = new CocoCollection([], { model: LevelSession }) + @ownedClassrooms = new Classrooms() + @ownedClassrooms.fetchMine({data: {project: '_id'}}) + @supermodel.trackCollection(@ownedClassrooms) onCourseInstancesSync: -> @sessions = new CocoCollection([], { model: LevelSession }) @@ -79,7 +83,6 @@ module.exports = class ClassroomView extends RootView onLoaded: -> @teacherMode = me.isAdmin() or @classroom.get('ownerID') is me.id userSessions = @sessions.groupBy('creator') - @users.remove(@users.where({ deleted: true })) for user in @users.models user.sessions = new CocoCollection(userSessions[user.id], { model: LevelSession }) user.sessions.comparator = 'changed' diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index 2e9953585..92dbfe237 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -3,6 +3,7 @@ CocoCollection = require 'collections/CocoCollection' Course = require 'models/Course' CourseInstance = require 'models/CourseInstance' Classroom = require 'models/Classroom' +Classrooms = require 'collections/Classrooms' LevelSession = require 'models/LevelSession' RootView = require 'views/core/RootView' template = require 'templates/courses/course-details' @@ -23,6 +24,9 @@ module.exports = class CourseDetailsView extends RootView constructor: (options, @courseID, @courseInstanceID) -> super options + @ownedClassrooms = new Classrooms() + @ownedClassrooms.fetchMine({data: {project: '_id'}}) + @supermodel.trackCollection(@ownedClassrooms) @courseID ?= options.courseID @courseInstanceID ?= options.courseInstanceID @classroom = new Classroom() diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index c654480ca..f62945e1f 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -9,6 +9,7 @@ CourseInstance = require 'models/CourseInstance' CocoCollection = require 'collections/CocoCollection' Course = require 'models/Course' Classroom = require 'models/Classroom' +Classrooms = require 'collections/Classrooms' LevelSession = require 'models/LevelSession' Campaign = require 'models/Campaign' utils = require 'core/utils' @@ -33,6 +34,9 @@ module.exports = class CoursesView extends RootView @supermodel.loadCollection(@courseInstances) @classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom }) @supermodel.loadCollection(@classrooms, { data: {memberID: me.id} }) + @ownedClassrooms = new Classrooms() + @ownedClassrooms.fetchMine({data: {project: '_id'}}) + @supermodel.trackCollection(@ownedClassrooms) @courses = new CocoCollection([], { url: "/db/course", model: Course}) @supermodel.loadCollection(@courses) @campaigns = new CocoCollection([], { url: "/db/campaign", model: Campaign }) @@ -58,7 +62,7 @@ module.exports = class CoursesView extends RootView onLoaded: -> super() - if utils.getQueryVariable('_cc', false) + if utils.getQueryVariable('_cc', false) and not me.isAnonymous() @joinClass() onClickStartNewGameButton: -> diff --git a/app/views/courses/EnrollmentsView.coffee b/app/views/courses/EnrollmentsView.coffee index adf2a73d5..a63fc8695 100644 --- a/app/views/courses/EnrollmentsView.coffee +++ b/app/views/courses/EnrollmentsView.coffee @@ -1,6 +1,7 @@ app = require 'core/application' CreateAccountModal = require 'views/core/CreateAccountModal' Classroom = require 'models/Classroom' +Classrooms = require 'collections/Classrooms' CocoCollection = require 'collections/CocoCollection' Course = require 'models/Course' Prepaids = require 'collections/Prepaids' @@ -14,10 +15,13 @@ Products = require 'collections/Products' module.exports = class EnrollmentsView extends RootView id: 'enrollments-view' template: template - numberOfStudents: 30 + numberOfStudents: 15 pricePerStudent: 0 initialize: (options) -> + @ownedClassrooms = new Classrooms() + @ownedClassrooms.fetchMine({data: {project: '_id'}}) + @supermodel.trackCollection(@ownedClassrooms) @listenTo stripeHandler, 'received-token', @onStripeReceivedToken @fromClassroom = utils.getQueryVariable('from-classroom') @members = new CocoCollection([], { model: User }) diff --git a/app/views/courses/StudentSignUpModal.coffee b/app/views/courses/StudentSignUpModal.coffee index 3b7c03e12..76d2307ad 100644 --- a/app/views/courses/StudentSignUpModal.coffee +++ b/app/views/courses/StudentSignUpModal.coffee @@ -11,7 +11,6 @@ module.exports = class StudentSignUpModal extends ModalView template: template events: - 'click #sign-up-btn': 'onClickSignUpButton' 'submit form': 'onSubmitForm' 'click #skip-link': 'onClickSkipLink' @@ -31,10 +30,7 @@ module.exports = class StudentSignUpModal extends ModalView onSubmitForm: (e) -> e.preventDefault() @signupClassroomPrecheck() - - onClickSignUpButton: -> - @signupClassroomPrecheck() - + emailCheck: -> email = @$('#email').val() filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i # https://news.ycombinator.com/item?id=5763990 @@ -97,5 +93,9 @@ module.exports = class StudentSignUpModal extends ModalView classCode = @$('#class-code-input').val() if classCode url = "/courses?_cc="+classCode - application.router.navigate(url) - window.location.reload() + document.location.href = url + # This was a terrible hack to make navigating trigger when just adding query params + # application.router.navigate('/thisisahack') + # application.router.navigate(url, { trigger: true }) + else + window.location.reload() diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index dbf2ff9c5..066cd575c 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -4,6 +4,7 @@ helper = require 'lib/coursesHelper' ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal' InviteToClassroomModal = require 'views/courses/InviteToClassroomModal' ActivateLicensesModal = require 'views/courses/ActivateLicensesModal' +RemoveStudentModal = require 'views/courses/RemoveStudentModal' Classroom = require 'models/Classroom' Classrooms = require 'collections/Classrooms' @@ -26,6 +27,7 @@ module.exports = class TeacherClassView extends RootView 'click .sort-by-progress': 'sortByProgress' 'click #copy-url-btn': 'copyURL' 'click #copy-code-btn': 'copyCode' + 'click .remove-student-link': 'onClickRemoveStudentLink' 'click .enroll-student-button': 'onClickEnroll' 'click .assign-to-selected-students': 'onClickBulkAssign' 'click .enroll-selected-students': 'onClickBulkEnroll' @@ -46,15 +48,15 @@ module.exports = class TeacherClassView extends RootView @listenTo @classroom, 'sync', -> @students = new Users() - @students.fetchForClassroom(@classroom) - @supermodel.trackCollection(@students) + jqxhrs = @students.fetchForClassroom(@classroom) + if jqxhrs.length > 0 + @supermodel.trackCollection(@students) @listenTo @students, 'sync', @sortByName @listenTo @students, 'sort', @renderSelectors.bind(@, '.students-table', '.student-levels-table') @classroom.sessions = new LevelSessions() - if @classroom.get('members')?.length > 0 - @classroom.sessions.fetchForAllClassroomMembers(@classroom) - @supermodel.trackCollection(@classroom.sessions) + requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom) + @supermodel.trackRequests(requests) @courses = new Courses() @courses.fetch() @@ -69,7 +71,6 @@ module.exports = class TeacherClassView extends RootView @supermodel.trackCollection(@courseInstances) onLoaded: -> - console.log("loaded!") @classCode = @classroom.get('codeCamel') or @classroom.get('code') @joinURL = document.location.origin + "/courses?_cc=" + @classCode @@ -109,6 +110,21 @@ module.exports = class TeacherClassView extends RootView modal = new ClassroomSettingsModal({ classroom: classroom }) @openModalView(modal) @listenToOnce modal, 'hide', @render + + onClickRemoveStudentLink: (e) -> + user = @students.get($(e.currentTarget).data('student-id')) + modal = new RemoveStudentModal({ + classroom: @classroom + user: user + courseInstances: @courseInstances + }) + @openModalView(modal) + modal.once 'remove-student', @onStudentRemoved, @ + + onStudentRemoved: (e) -> + @students.remove(e.user) + @render() + application.tracker?.trackEvent 'Classroom removed student', category: 'Courses', classroomID: @classroom.id, userID: e.user.id onClickAddStudents: (e) => modal = new InviteToClassroomModal({ classroom: @classroom }) @@ -180,8 +196,7 @@ module.exports = class TeacherClassView extends RootView if courseInstance courseInstance.addMembers members, { - success: => - @render() unless @destroyed + success: @onBulkAssignSuccess } else courseInstance = new CourseInstance { @@ -194,12 +209,15 @@ module.exports = class TeacherClassView extends RootView courseInstance.save {}, { success: => courseInstance.addMembers members, { - success: => - @render() unless @destroyed + success: @onBulkAssignSuccess } } null + onBulkAssignSuccess: => + @render() unless @destroyed + noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000 + onClickSelectAll: (e) -> e.preventDefault() checkboxes = $('.student-checkbox input') diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index d5ba680d0..ef1a38e87 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -4,6 +4,7 @@ CocoCollection = require 'collections/CocoCollection' CocoModel = require 'models/CocoModel' Course = require 'models/Course' Classroom = require 'models/Classroom' +Classrooms = require 'collections/Classrooms' InviteToClassroomModal = require 'views/courses/InviteToClassroomModal' User = require 'models/User' CourseInstance = require 'models/CourseInstance' @@ -33,6 +34,9 @@ module.exports = class TeacherCoursesView extends RootView constructor: (options) -> super(options) + @ownedClassrooms = new Classrooms() + @ownedClassrooms.fetchMine({data: {project: '_id'}}) + @supermodel.trackCollection(@ownedClassrooms) @courses = new CocoCollection([], { url: "/db/course", model: Course}) @supermodel.loadCollection(@courses, 'courses') @classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom }) diff --git a/app/views/teachers/ConvertToTeacherAccountView.coffee b/app/views/teachers/ConvertToTeacherAccountView.coffee index f587192df..9bfca1a61 100644 --- a/app/views/teachers/ConvertToTeacherAccountView.coffee +++ b/app/views/teachers/ConvertToTeacherAccountView.coffee @@ -31,7 +31,7 @@ module.exports = class ConvertToTeacherAccountView extends RootView onLoaded: -> if @trialRequests.size() and me.isTeacher() - return application.router.navigate('/courses/teachers', { trigger: true, replace: true }) + return application.router.navigate('/teachers', { trigger: true, replace: true }) super() @@ -115,7 +115,7 @@ module.exports = class ConvertToTeacherAccountView extends RootView onTrialRequestSubmit: -> me.setRole @trialRequest.get('properties').role.toLowerCase(), true storage.remove(FORM_KEY) - application.router.navigate('/courses/teachers', {trigger: true}) + application.router.navigate('/teachers/classes', {trigger: true}) formSchema = { type: 'object' diff --git a/app/views/teachers/CreateTeacherAccountView.coffee b/app/views/teachers/CreateTeacherAccountView.coffee index cee2b24aa..5bb46ff2b 100644 --- a/app/views/teachers/CreateTeacherAccountView.coffee +++ b/app/views/teachers/CreateTeacherAccountView.coffee @@ -8,7 +8,7 @@ errors = require 'core/errors' User = require 'models/User' FORM_KEY = 'request-quote-form' -SIGNUP_REDIRECT = '/courses/teachers' +SIGNUP_REDIRECT = '/teachers/classes' module.exports = class CreateTeacherAccountView extends RootView id: 'create-teacher-account-view' @@ -134,6 +134,7 @@ module.exports = class CreateTeacherAccountView extends RootView onTrialRequestSubmit: -> storage.remove(FORM_KEY) attrs = _.pick(forms.formToObject(@$('form')), 'name', 'email', 'role') + attrs.role = attrs.role.toLowerCase() options = {} newUser = new User(attrs) if @gplusAttrs diff --git a/app/views/teachers/RequestQuoteView.coffee b/app/views/teachers/RequestQuoteView.coffee index dee4c622f..cb3cb3daa 100644 --- a/app/views/teachers/RequestQuoteView.coffee +++ b/app/views/teachers/RequestQuoteView.coffee @@ -8,7 +8,7 @@ errors = require 'core/errors' ConfirmModal = require 'views/editor/modal/ConfirmModal' FORM_KEY = 'request-quote-form' -SIGNUP_REDIRECT = '/courses/teachers' +SIGNUP_REDIRECT = '/teachers' module.exports = class RequestQuoteView extends RootView id: 'request-quote-view' @@ -131,6 +131,8 @@ module.exports = class RequestQuoteView extends RootView onTrialRequestSubmit: -> me.setRole @trialRequest.get('properties').role.toLowerCase(), true + defaultName = [@trialRequest.get('firstName'), @trialRequest.get('lastName')].join(' ') + @$('input[name="name"]').val(defaultName) storage.remove(FORM_KEY) @$('#request-form, #form-submit-success').toggleClass('hide') @scrollToTop(0) diff --git a/scripts/mongodb/migrations/2016-03-18-init-school-roles.js b/scripts/mongodb/migrations/2016-03-18-init-school-roles.js index efac703b1..c57c54545 100644 --- a/scripts/mongodb/migrations/2016-03-18-init-school-roles.js +++ b/scripts/mongodb/migrations/2016-03-18-init-school-roles.js @@ -1,17 +1,44 @@ -// Removes all users with a teacher-like role from classroom membership // Usage: copy and paste into mongo + +// Set all users with trial requests to a teacher or teacher-like role, depending on trial request. + +var hasTrialRequest = {}; + +db.trial.requests.find().forEach(function(trialRequest) { + var role = trialRequest.properties.role || 'teacher'; + var user = db.users.findOne({_id: trialRequest.applicant}, {role:1, name:1, email:1}); + print(JSON.stringify(user), JSON.stringify(trialRequest.properties), role); + if (!user.role) { + print(db.users.update({_id: trialRequest.applicant}, {$set: {role: role}})); + } + hasTrialRequest[user._id.str] = true; +}); + var teacherRoles = ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent']; +// Unset all teacher-like roles for users without a trial request. +// AND removes all remaining users with a teacher-like role from classroom membership (after conversion period) + db.users.find({'role': {$in: teacherRoles}}, {_id: 1, name: 1, email: 1, role: 1}).forEach(function(user) { print('Updating user', JSON.stringify(user)); - print(db.classrooms.find({members: user._id}, {name: 1}).toArray().length); - print(db.classrooms.update({members: user._id}, {$pull: {members: user._id}}, {multi: true})); + if (!hasTrialRequest.user._id.str) { + print('\tunset role'); + //db.users.update({_id: user._id}, {$unset: {role: ''}}); + } + else { + var count = db.classrooms.count({members: user._id}, {name: 1}); + if (count) { + print('\tWill remove from classrooms'); + //print(db.classrooms.update({members: user._id}, {$pull: {members: user._id}}, {multi: true})); + } + else { + print('\tRole correct, in no classrooms. No action') + } + } }); - -// Finds all members of classrooms, sets their role to 'student' if they do not already have a role -// Usage: copy and paste into mongo +// Find all members of classrooms, set their role to 'student' if they do not already have a role db.classrooms.find({}, {members: 1}).forEach(function(classroom) { if(!classroom.members) { diff --git a/scripts/mongodb/migrations/2016-04-04-identify-mixed-roles.js b/scripts/mongodb/migrations/2016-04-04-identify-mixed-roles.js new file mode 100644 index 000000000..4d385d60c --- /dev/null +++ b/scripts/mongodb/migrations/2016-04-04-identify-mixed-roles.js @@ -0,0 +1,38 @@ + +// Usage: paste into mongodb + +// In separating student and teacher accounts, need to see +// * Who has trial requests +// * Who owns a classroom +// * Who is in a classroom +// +// People who do not have a trial request and are both in a classroom +// and own a classroom are the most up in the air. + +var creators = {}; +var members = {}; +db.classrooms.find({}, {ownerID:1, members:1}).forEach(function(classroom) { + if(classroom.ownerID) { creators[classroom.ownerID.str] = false; } + if(classroom.members) { + for (var index in classroom.members) { + members[classroom.members[index].str] = true; + } + } +}); + +db.trial.requests.find({}, {applicant:1}).forEach(function(trialRequest) { + if(!trialRequest.applicant) { return; } + creators[trialRequest.applicant.str] = true; +}); + +var isMemberAndNoTrialRequestCount = 0; +var noTrialRequestCount = 0; +for(var userID in creators) { + if (!creators[userID]) { + noTrialRequestCount += 1; + if (members[userID]) { + isMemberAndNoTrialRequestCount += 1; + } + } +} +print('count', count); diff --git a/server/classrooms/classroom_handler.coffee b/server/classrooms/classroom_handler.coffee index 8f138d5b4..bc727724a 100644 --- a/server/classrooms/classroom_handler.coffee +++ b/server/classrooms/classroom_handler.coffee @@ -52,6 +52,7 @@ ClassroomHandler = class ClassroomHandler extends Handler @sendSuccess(res, cleandocs) joinClassroomAPI: (req, res, classroomID) -> + return @sendUnauthorizedError(res, 'Cannot join a classroom while anonymous') if req.user.isAnonymous() return @sendBadInputError(res, 'Need an object with a code') unless req.body?.code return @sendForbiddenError(res, 'Cannot join a classroom as a teacher') if req.user.isTeacher() code = req.body.code.toLowerCase() diff --git a/server/middleware/auth.coffee b/server/middleware/auth.coffee index 3e053f9cf..66a308f23 100644 --- a/server/middleware/auth.coffee +++ b/server/middleware/auth.coffee @@ -20,7 +20,7 @@ module.exports = checkLoggedIn: -> return (req, res, next) -> - if not req.user + if (not req.user) or (req.user.isAnonymous()) return next new errors.Unauthorized('You must be logged in.') next() diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 96f8026cd..708a22f2b 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -22,7 +22,6 @@ module.exports = unless _.isUndefined(options.archived) # Handles when .archived is true, vs false-or-null sanitizedOptions.archived = { $ne: not (options.archived is 'true') } - console.log sanitizedOptions dbq = Classroom.find _.merge sanitizedOptions, { ownerID: mongoose.Types.ObjectId(ownerID) } dbq.select(parse.getProjectFromReq(req)) classrooms = yield dbq @@ -37,11 +36,11 @@ module.exports = throw new errors.NotFound('Classroom not found.') if not classroom throw new errors.Forbidden('You do not own this classroom.') unless req.user.isAdmin() or classroom.get('ownerID').equals(req.user._id) members = classroom.get('members') or [] - members = members.slice(memberSkip, memberLimit) + members = members.slice(memberSkip, memberSkip + memberLimit) dbqs = [] select = 'state.complete level creator playtime' for member in members - dbqs.push(LevelSession.find({creator: member.toHexString(), team: {$exists: false}}).select(select).exec()) + dbqs.push(LevelSession.find({creator: member.toHexString()}).select(select).exec()) results = yield dbqs sessions = _.flatten(results) res.status(200).send(sessions) @@ -57,7 +56,7 @@ module.exports = unless req.user.isAdmin() or isOwner or isMember throw new errors.Forbidden('You do not own this classroom.') memberIDs = classroom.get('members') or [] - memberIDs = memberIDs.slice(memberSkip, memberLimit) + memberIDs = memberIDs.slice(memberSkip, memberSkip + memberLimit) members = yield User.find({ _id: { $in: memberIDs }}).select(parse.getProjectFromReq(req)) memberObjects = (member.toObject({ req: req, includedPrivates: ["name", "email"] }) for member in members) diff --git a/server/middleware/course-instances.coffee b/server/middleware/course-instances.coffee index e557c2ad2..7defb5530 100644 --- a/server/middleware/course-instances.coffee +++ b/server/middleware/course-instances.coffee @@ -34,7 +34,9 @@ module.exports = unless _.all(userIDs, (userID) -> _.contains classroomMembers, userID) throw new errors.Forbidden('Users must be members of classroom') - unless classroom.get('ownerID').equals(req.user._id) + ownsClassroom = classroom.get('ownerID').equals(req.user._id) + addingSelf = userIDs.length is 1 and userIDs[0] is req.user.id + unless ownsClassroom or addingSelf throw new errors.Forbidden('You must own the classroom to add members') # Only the enrolled users diff --git a/server/prepaids/prepaid_handler.coffee b/server/prepaids/prepaid_handler.coffee index a50bba5e8..147777af4 100644 --- a/server/prepaids/prepaid_handler.coffee +++ b/server/prepaids/prepaid_handler.coffee @@ -108,6 +108,7 @@ PrepaidHandler = class PrepaidHandler extends Handler return @sendForbiddenError(res) user.set('coursePrepaidID', prepaid.get('_id')) + user.set('role', 'student') if not user.get('role') user.save (err, user) => return @sendDatabaseError(res, err) if err # return prepaid with new redeemer added locally diff --git a/server/routes/index.coffee b/server/routes/index.coffee index cebb83e74..1bba6f10f 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -26,6 +26,7 @@ module.exports.setup = (app) -> app.get('/db/classroom', mw.classrooms.getByOwner) app.get('/db/classroom/:handle/member-sessions', mw.classrooms.fetchMemberSessions) app.get('/db/classroom/:handle/members', mw.classrooms.fetchMembers) # TODO: Use mw.auth? + app.get('/db/classroom/:handle', mw.auth.checkLoggedIn()) # TODO: Finish migrating route, adding now so 401 is returned Course = require '../models/Course' app.get('/db/course', mw.rest.get(Course)) diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index a470b5e1d..32a9a3aa3 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -10,7 +10,7 @@ requestAsync = Promise.promisify(request, {multiArgs: true}) classroomsURL = getURL('/db/classroom') describe 'GET /db/classroom?ownerID=:id', -> - + beforeEach utils.wrap (done) -> yield utils.clearModels([User, Classroom]) @user1 = yield utils.initUser() @@ -20,19 +20,19 @@ describe 'GET /db/classroom?ownerID=:id', -> yield utils.loginUser(@user2) @classroom2 = yield new Classroom({name: 'Classroom 2', ownerID: @user2.get('_id') }).save() done() - + it 'returns an array of classrooms with the given owner', utils.wrap (done) -> [res, body] = yield request.getAsync getURL('/db/classroom?ownerID='+@user2.id), { json: true } expect(res.statusCode).toBe(200) expect(body.length).toBe(1) expect(body[0].name).toBe('Classroom 2') done() - + it 'returns 403 when a non-admin tries to get classrooms for another user', utils.wrap (done) -> [res, body] = yield request.getAsync getURL('/db/classroom?ownerID='+@user1.id), { json: true } expect(res.statusCode).toBe(403) done() - + describe 'GET /db/classroom/:id', -> it 'clears database users and classrooms', (done) -> @@ -54,7 +54,7 @@ describe 'GET /db/classroom/:id', -> done() describe 'POST /db/classroom', -> - + it 'clears database users and classrooms', (done) -> clearModels [User, Classroom], (err) -> throw err if err @@ -71,7 +71,7 @@ describe 'POST /db/classroom', -> expect(body.members.length).toBe(0) expect(body.ownerID).toBe(user1.id) done() - + it 'does not work for anonymous users', (done) -> logoutUser -> data = { name: 'Classroom 2' } @@ -85,8 +85,8 @@ describe 'POST /db/classroom', -> request.post {uri: classroomsURL, json: data }, (err, res, body) -> expect(res.statusCode).toBe(403) done() - - + + describe 'PUT /db/classroom', -> it 'clears database users and classrooms', (done) -> @@ -107,7 +107,7 @@ describe 'PUT /db/classroom', -> expect(body.name).toBe('Classroom 3') expect(body.description).toBe('New Description') done() - + it 'is not allowed if you are just a member', (done) -> loginNewUser (user1) -> user1.set('role', 'teacher') @@ -125,7 +125,7 @@ describe 'PUT /db/classroom', -> request.put { uri: url, json: data }, (err, res, body) -> expect(res.statusCode).toBe(403) done() - + describe 'POST /db/classroom/~/members', -> it 'clears database users and classrooms', (done) -> @@ -173,7 +173,7 @@ describe 'POST /db/classroom/~/members', -> Classroom.findById classroomID, (err, classroom) -> expect(classroom.get('members').length).toBe(0) done() - + it 'does not work if the user is anonymous', utils.wrap (done) -> yield utils.clearModels([User, Classroom]) teacher = yield utils.initUser({role: 'teacher'}) @@ -234,9 +234,9 @@ describe 'POST /db/classroom/:id/invite-members', -> expect(res.statusCode).toBe(200) done() - + describe 'GET /db/classroom/:handle/member-sessions', -> - + beforeEach utils.wrap (done) -> yield utils.clearModels([User, Classroom, LevelSession, Level]) @artisan = yield utils.initUser() @@ -262,18 +262,18 @@ describe 'GET /db/classroom/:handle/member-sessions', -> expect(res.statusCode).toBe(200) expect(body.length).toBe(4) done() - + it 'does not work if you are not the owner of the classroom', utils.wrap (done) -> yield utils.loginUser(@student1) [res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions"), { json: true } expect(res.statusCode).toBe(403) done() - + it 'does not work if you are not logged in', utils.wrap (done) -> [res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions"), { json: true } expect(res.statusCode).toBe(401) done() - + it 'accepts memberSkip and memberLimit GET parameters', utils.wrap (done) -> yield utils.loginUser(@teacher) [res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions?memberLimit=1"), { json: true } @@ -285,9 +285,9 @@ describe 'GET /db/classroom/:handle/member-sessions', -> expect(body.length).toBe(2) expect(session.creator).toBe(@student2.id) for session in body done() - + describe 'GET /db/classroom/:handle/members', -> - + beforeEach utils.wrap (done) -> yield utils.clearModels([User, Classroom]) @teacher = yield utils.initUser() @@ -296,25 +296,25 @@ describe 'GET /db/classroom/:handle/members', -> @classroom = yield new Classroom({name: 'Classroom', ownerID: @teacher._id, members: [@student1._id, @student2._id] }).save() @emptyClassroom = yield new Classroom({name: 'Empty Classroom', ownerID: @teacher._id, members: [] }).save() done() - + it 'does not work if you are not the owner of the classroom', utils.wrap (done) -> yield utils.loginUser(@student1) [res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions"), { json: true } expect(res.statusCode).toBe(403) done() - + it 'does not work if you are not logged in', utils.wrap (done) -> [res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions"), { json: true } expect(res.statusCode).toBe(401) done() - + it 'works on an empty classroom', utils.wrap (done) -> yield utils.loginUser(@teacher) [res, body] = yield request.getAsync getURL("/db/classroom/#{@emptyClassroom.id}/members?name=true&email=true"), { json: true } expect(res.statusCode).toBe(200) expect(body).toEqual([]) done() - + it 'returns all members with name and email', utils.wrap (done) -> yield utils.loginUser(@teacher) [res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/members?name=true&email=true"), { json: true } @@ -324,4 +324,4 @@ describe 'GET /db/classroom/:handle/members', -> expect(user.name).toBeDefined() expect(user.email).toBeDefined() expect(user.passwordHash).toBeUndefined() - done() \ No newline at end of file + done() diff --git a/spec/server/functional/prepaid.spec.coffee b/spec/server/functional/prepaid.spec.coffee index c9214fd5b..328b03cf9 100644 --- a/spec/server/functional/prepaid.spec.coffee +++ b/spec/server/functional/prepaid.spec.coffee @@ -71,6 +71,7 @@ describe '/db/prepaid', -> expect(prepaid.get('redeemers').length).toBe(1) User.findById otherUser.id, (err, user) -> expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true) + expect(user.get('role')).toBe('student') done() it 'does not allow more redeemers than maxRedeemers', (done) -> diff --git a/spec/server/utils.coffee b/spec/server/utils.coffee index a6f87a2c7..97140883e 100644 --- a/spec/server/utils.coffee +++ b/spec/server/utils.coffee @@ -56,6 +56,10 @@ module.exports = mw = options = _.extend({permissions: ['artisan']}, options) return @initUser(options) + becomeAnonymous: Promise.promisify (done) -> + request.post mw.getURL('/auth/logout'), -> + request.get mw.getURL('/auth/whoami'), done + logout: Promise.promisify (done) -> request.post mw.getURL('/auth/logout'), done diff --git a/test/app/models/SuperModel.spec.coffee b/test/app/models/SuperModel.spec.coffee index 0efe1236f..d4d5affa3 100644 --- a/test/app/models/SuperModel.spec.coffee +++ b/test/app/models/SuperModel.spec.coffee @@ -3,6 +3,25 @@ User = require 'models/User' ComponentsCollection = require 'collections/ComponentsCollection' describe 'SuperModel', -> + + describe '.trackRequest(jqxhr, value)', -> + it 'takes a jqxhr and tracks its progress', (done) -> + s = new SuperModel() + jqxhrA = $.get('/db/a') + reqA = jasmine.Ajax.requests.mostRecent() + jqxhrB = $.get('/db/b') + reqB = jasmine.Ajax.requests.mostRecent() + s.trackRequest(jqxhrA, 1) + s.trackRequest(jqxhrB, 3) + expect(s.progress).toBe(0) + reqA.respondWith({status: 200, responseText: '[]'}) + _.defer -> + expect(s.progress).toBe(0.25) + reqB.respondWith({status: 200, responseText: '[]'}) + _.defer -> + expect(s.progress).toBe(1) + done() + describe 'progress (property)', -> it 'is finished by default', -> s = new SuperModel() diff --git a/test/app/views/teachers/ConvertToTeacherAccountView.spec.coffee b/test/app/views/teachers/ConvertToTeacherAccountView.spec.coffee index ad97ceea5..bcd1d1536 100644 --- a/test/app/views/teachers/ConvertToTeacherAccountView.spec.coffee +++ b/test/app/views/teachers/ConvertToTeacherAccountView.spec.coffee @@ -41,6 +41,7 @@ describe 'ConvertToTeacherAccountView (/teachers/convert)', -> } beforeEach -> + spyOn(application.router, 'navigate') me.clear() me.set({ _id: '1234' @@ -54,11 +55,13 @@ describe 'ConvertToTeacherAccountView (/teachers/convert)', -> jasmine.demoEl(view.$el) spyOn(storage, 'load').and.returnValue({ lastName: 'Saved Changes' }) + + afterEach (done) -> + _.defer(done) # let everything finish loading, keep navigate spied on describe 'when the user already has a TrialRequest and is a teacher', -> beforeEach (done) -> - spyOn(application.router, 'navigate') spyOn(me, 'isTeacher').and.returnValue(true) request = jasmine.Ajax.requests.mostRecent() request.respondWith({ @@ -73,15 +76,17 @@ describe 'ConvertToTeacherAccountView (/teachers/convert)', -> }) _.defer done # Let SuperModel finish - it 'redirects to /courses/teachers', -> + # TODO: re-enable when student and teacher areas are enforced + xit 'redirects to /teachers/courses', -> expect(application.router.navigate).toHaveBeenCalled() args = application.router.navigate.calls.argsFor(0) - expect(args[0]).toBe('/courses/teachers') + expect(args[0]).toBe('/teachers/courses') describe 'when the user has role "student"', -> beforeEach -> me.set('role', 'student') + jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, responseText: JSON.stringify('[]') }) view.render() it 'shows a warning that they will convert to a teacher account', -> @@ -109,6 +114,8 @@ describe 'ConvertToTeacherAccountView (/teachers/convert)', -> expect(request.method).toBe('POST') describe '"Log out" link', -> + beforeEach -> + jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, responseText: JSON.stringify('[]') }) it 'logs out the user and redirects them to /teachers/signup', -> spyOn(me, 'logout') @@ -117,6 +124,7 @@ describe 'ConvertToTeacherAccountView (/teachers/convert)', -> describe 'submitting the form', -> beforeEach -> + jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, responseText: JSON.stringify('[]') }) form = view.$('form') forms.objectToForm(form, successForm, {overwriteExisting: true}) form.submit() @@ -128,8 +136,7 @@ describe 'ConvertToTeacherAccountView (/teachers/convert)', -> attrs = JSON.parse(request.params) expect(attrs.properties?.firstName).toBe('Mr') - it 'redirects to /courses/teachers', -> - spyOn(application.router, 'navigate') + it 'redirects to /teachers/classes', -> request = jasmine.Ajax.requests.mostRecent() request.respondWith({ status: 201 @@ -137,10 +144,9 @@ describe 'ConvertToTeacherAccountView (/teachers/convert)', -> }) expect(application.router.navigate).toHaveBeenCalled() args = application.router.navigate.calls.argsFor(0) - expect(args[0]).toBe('/courses/teachers') + expect(args[0]).toBe('/teachers/classes') it 'sets a teacher role', -> - spyOn(application.router, 'navigate') request = jasmine.Ajax.requests.mostRecent() request.respondWith({ status: 201 diff --git a/test/app/views/teachers/CreateTeacherAccountView.spec.coffee b/test/app/views/teachers/CreateTeacherAccountView.spec.coffee index f24b3164a..71d2cc941 100644 --- a/test/app/views/teachers/CreateTeacherAccountView.spec.coffee +++ b/test/app/views/teachers/CreateTeacherAccountView.spec.coffee @@ -234,7 +234,7 @@ describe 'CreateTeacherAccountView', -> responseText: JSON.stringify(_.extend({_id:'fraghlarghl'}, JSON.parse(request.params))) }) - it 'redirects to "/courses/teachers"', -> + it 'redirects to "/teachers/courses"', -> expect(application.router.navigate).toHaveBeenCalled() expect(application.router.reload).toHaveBeenCalled() diff --git a/test/app/views/teachers/RequestQuoteView.spec.coffee b/test/app/views/teachers/RequestQuoteView.spec.coffee index d6db14b22..94cabe511 100644 --- a/test/app/views/teachers/RequestQuoteView.spec.coffee +++ b/test/app/views/teachers/RequestQuoteView.spec.coffee @@ -137,6 +137,9 @@ describe 'RequestQuoteView', -> beforeEach -> application.facebookHandler.fakeAPI() application.gplusHandler.fakeAPI() + + it 'fills the username field with the given first and last names', -> + expect(view.$('input[name="name"]').val()).toBe('A B') it 'includes a facebook button which will sign them in immediately', -> view.$('#facebook-signup-btn').click()