Various fixes and changes to teacher-accounts and teacher-dashboard

This commit is contained in:
phoenixeliot 2016-03-30 16:20:37 -07:00 committed by Scott Erickson
parent 58a5df7a4f
commit beb53d9f2f
60 changed files with 520 additions and 223 deletions

View file

@ -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

View file

@ -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()
document.location.reload()

View file

@ -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) ->

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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(' ')

View file

@ -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'

View file

@ -3,7 +3,7 @@
width: 50%
.age-range-select
width: 100px
width: 170px
display: inline-block
label

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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 !

View file

@ -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&ndash;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&ndash;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')

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
&nbsp;
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

View file

@ -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)

View file

@ -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')
| )

View file

@ -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")

View file

@ -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')

View file

@ -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+")

View file

@ -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+")

View file

@ -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

View file

@ -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")

View file

@ -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()

View file

@ -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')

View file

@ -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)

View file

@ -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'

View file

@ -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()

View file

@ -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: ->

View file

@ -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 })

View file

@ -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()

View file

@ -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')

View file

@ -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 })

View file

@ -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'

View file

@ -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

View file

@ -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)

View file

@ -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) {

View file

@ -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);

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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()
done()

View file

@ -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) ->

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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()