codecombat/app/templates/courses/teacher-class-view.jade
phoenixeliot 8496343a02 Improve student account recovery
This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account.

Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student.

Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter.

Squashed commit messages:

Add server endpoints for verifying email address

Add server endpoints for verifying email address (pt 2)

Add Server+Client endpoint for sending verification email

Add client view for verification links

Add Edit Student Modal for resetting passwords

Add specs for EditStudentModal

Tweak method name in EditStudentModal

Add edit student button to TeacherClassView

Fix up frontend for teacher password resetting

Add middleware for teacher password resetting

Improve button UX in EditStudentModal

Add JoinClassModal

Add welcome emails, use broad name

Use email without domain as fallback instead of full email

Fetch user on edit student modal open

Don't allow password reset if student email is verified

Set role to student on user signup with classCode

Tweak interface for joinClassModal

Add button to request verification email for yourself

Fix verify email template ID

Move text to en.coffee

Minor tweaks

Fix code review comments

Fix some tests, disable a broken one

Fix misc tests

Fix more tests

Refactor recovery email sending to auth

Fix overbroad sass

Add options to refactored recovery email function

Rename getByCode to fetchByCode

Fix error message

Fix up error handling in users middleware

Use .get instead of .toObject

Use findById

Fix more code review comments

Disable still-broken test
2016-05-24 14:07:28 -07:00

371 lines
14 KiB
Text

extends /templates/base-flat
block page_nav
include ./teacher-dashboard-nav.jade
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/update-account") Upgrade to teacher account
if classroom.loaded
.container
+breadcrumbs
if classroom.get('archived')
.row.center-block.text-center.m-t-3.m-b-3
.unarchive-btn.btn.btn-lg.btn-navy
span(data-i18n='teacher.unarchive_this_class')
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.m-t-5
.classroom-details.col-md-3
- var stats = state.get('classStats')
h4.m-b-2(data-i18n='teacher.class_overview')
.language.small-details
span(data-i18n='teacher.language')
span.spr :
span= classroom.capitalLanguage
.student-count.small-details
span(data-i18n='teacher.students')
span.spr :
span= classroom.get('members').length
.average-playtime.small-details
span(data-i18n='teacher.avg_playtime')
span.spr :
span= stats.averagePlaytime
.total-playtime.small-details
span(data-i18n='teacher.total_playtime')
span.spr :
span= stats.totalPlaytime
.average-complete.small-details
span(data-i18n='teacher.avg_completed')
span.spr :
span= stats.averageLevelsComplete
.total-complete.small-details
span(data-i18n='teacher.total_completed')
span.spr :
span= stats.totalLevelsComplete
.total-complete.small-details
span(data-i18n='teacher.created')
span.spr :
span= moment(classroom.created()).format('l')
if view.students && view.students.models.length > 0
button.export-student-progress-btn.btn.btn-lg.btn-primary
span(data-i18n='teacher.export_student_progress')
//- .concepts.small-details
//- if state.get('progressData')
//- div
//- span(data-i18n='teacher.concepts_covered')
//- span :
//- - console.log('concepts', view.conceptData)
//- - concepts = view.conceptData
//- each state, name in view.conceptData[view.classroom.id]
//- if state.get('started')
//- b.concept(class=state.get('completed') ? 'forest' : 'gold')
//- span(data-i18n='concepts.'+name)
.completeness-info.col-md-4
h4.m-b-2
 
if state.get('earliestIncompleteLevel')
div.small-details
span(data-i18n='teacher.earliest_incomplete')
span :
+longLevelName(state.get('earliestIncompleteLevel'))
+inlineUserList(state.get('earliestIncompleteLevel').users)
if state.get('latestCompleteLevel')
div.small-details.m-t-3
span(data-i18n='teacher.latest_complete')
span :
+longLevelName(state.get('latestCompleteLevel'))
+inlineUserList(state.get('latestCompleteLevel').users)
.adding-students.col-md-5
h4.m-b-2
span(data-i18n='teacher.adding_students')
span :
+copyCodes
+addStudentsButton
ul#student-info-tabs.nav.nav-tabs.m-t-5(role='tablist')
li(class=(state.get('activeTab')==="#students-tab" ? 'active' : ''))
a.students-tab-btn(href='#students-tab')
.small-details.text-center(data-i18n='teacher.students')
.tab-spacer
li(class=(state.get('activeTab')==="#course-progress-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#course-progress-tab')
.small-details.text-center(data-i18n='teacher.course_progress')
.tab-filler
.tab-content
if state.get('activeTab')=='#students-tab'
+studentsTab
else
+courseProgressTab
mixin breadcrumbs
.breadcrumbs
a(data-i18n='teacher.my_classes' href='/teachers/classes')
span.spl.spr >
//- TODO: Use .glyphicon-menu-right when we update bootstrap
span
= view.classroom.get('name')
mixin longLevelName(data)
if data
div.level-name
span.spr Course
span= data.courseNumber
span.spr , Level
span= data.levelNumber
span.spr :
span= data.levelName
else
div.level-name(data-i18n='teacher.not_applicable')
mixin inlineUserList(users)
if users
ul.inline-student-list.small
each student in users
li
//- a(href='TODO')
//- = student.broadName()
span.inline-student-name
= student.broadName()
mixin addStudentsButton
.add-students.text-center
a.add-students-btn.btn.btn-lg.btn-primary(data-classroom-id=view.classroom.id)
span(data-i18n='teacher.add_students_manually')
mixin studentsTab
#students-tab
+bulkAssignControls
table.students-table
thead
th.checkbox-col.select-all
span Select All
.checkbox-flat
input(type='checkbox' id='checkbox-all-students')
label.checkmark(for='checkbox-all-students')
th
+sortButtons
tbody
each student in state.get('students').models
+studentRow(student)
mixin sortButtons
.sort-buttons.small
span(data-i18n='teacher.sort_by')
span.spr :
button.sort-button.sort-by-name(data-i18n='general.name')
button.sort-button.sort-by-progress(data-i18n='teacher.progress')
mixin studentRow(student)
tr.student-row.alternating-background
td.checkbox-col.student-checkbox
.checkbox-flat
input(type='checkbox' id='checkbox-student-' + student.id, data-student-id=student.id)
label.checkmark(for='checkbox-student-' + student.id)
td.student-info-col
.student-info
if student.get('deleted')
em (deleted)
div.student-name= student.broadName()
div.student-email.small-details= student.get('email')
td.hidden
a.edit-student-button(data-student-id=student.id)
span.glyphicon.glyphicon-edit
span(data-i18n='teacher.edit')
td.latest-level-col.small
div
i
span(data-i18n='teacher.latest_completed')
span :
div
+longLevelName(student.latestCompleteLevel)
td
if state.get('progressData')
each trimCourse, index in view.classroom.get('courses')
- var course = view.courses.get(trimCourse._id);
- var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
if instance && instance.hasMember(student)
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, user: student })
- var levelsTotal = trimCourse.levels.length
//- - var level = ???
+studentCourseProgressDot(progress, levelsTotal, level, 'CS' + (index+1))
unless student.isEnrolled()
+enrollStudentButton(student)
//- td
//- span.view-class-arrow.glyphicon.glyphicon-chevron-right
td
.pull-right
a.edit-student-link.small.center-block.text-center.m-r-2(data-student-id=student.id)
div.glyphicon.glyphicon-remove
div(data-i18n='teacher.edit')
a.remove-student-link.small.center-block.text-center.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)
span(data-i18n='teacher.enroll_student')
mixin courseProgressTab
#course-progress-tab.m-t-3
if view.courses
.text-center
span(data-i18n='teacher.select_course')
span.spr :
select.course-select
each trimCourse in view.classroom.get('courses')
- var course = view.courses.get(trimCourse._id);
option(value=course.id selected=(course===state.get('selectedCourse')))
= course.get('name')
if state.get('progressData')
.render-on-course-sync
+courseOverview
.student-levels-table
+sortButtons
each student in state.get('students').models
if _.contains(state.get('selectedCourse').members, student.id)
+studentLevelsRow(student)
//- TODO: If any students aren't assigned the course
.unassigned-students.render-on-course-sync
if state.get('selectedCourse') && state.get('selectedCourse').members.length < state.get('students').length
h2
span(data-i18n='TODO')
| Students who have not been assigned
|
span= state.get('selectedCourse').get('name')
for student in state.get('students').models
unless _.contains(state.get('selectedCourse').members, student.id)
.row.unassigned-student-row.alternating-background
.student-name.col-sm-3
= student.broadName()
.student-email.small-details.col-sm-3
= student.get('email')
.col-sm-4
.latest-completed.truncate.small
i.m-r-1
span(data-i18n='TODO')
| Latest completed
| :
+longLevelName(student.latestCompleteLevel)
.col-sm-2
if student.isEnrolled()
.assign-student-button.btn.btn-md.btn-navy.pull-right(data-user-id=student.id data-course-id=state.get('selectedCourse').id)
span(data-i18n='TODO')
| Assign Course
else
.enroll-student-button.btn.btn-md.btn-navy.pull-right(data-user-id=student.id)
span(data-i18n='TODO')
| Enroll Student
mixin courseOverview
- var course = state.get('selectedCourse')
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
.course-overview-row
.course-title.student-name
span= course.get('name')
span :
span(data-i18n='teacher.course_overview')
.course-overview-progress
each level, index in levels
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level })
+allStudentsLevelProgressDot(progress, level, index+1)
mixin studentLevelsRow(student)
.student-levels-row.alternating-background
div.student-info
div.student-name= student.broadName()
div.student-email.small-details= student.get('email')
div.student-levels-progress
- var course = state.get('selectedCourse')
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
each level, index in levels
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student })
+studentLevelProgressDot(progress, level, index+1)
mixin studentCourseProgressDot(progress, levelsTotal, level, label)
//- TODO: Refactor with TeacherClassesView jade
//- TODO: Give classes abbreviations instead of using index?
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
- _.assign(progress, { levelsTotal: levelsTotal })
.progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentCourseProgressDotTemplate(progress))
+progressDotLabel(label)
mixin allStudentsLevelProgressDot(progress, level, levelNumber)
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
- levelName = level.get('name')
- context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, numStudents: view.students.length })
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.allStudentsLevelProgressDotTemplate(context))
+progressDotLabel(levelNumber)
mixin studentLevelProgressDot(progress, level, levelNumber)
//- TODO: Refactor with TeacherClassesView jade
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
- levelName = level.get('name')
- context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber })
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentLevelProgressDotTemplate(context))
+progressDotLabel(levelNumber)
mixin progressDotLabel(label)
.dot-label.text-center
.dot-label-inner
= label
mixin copyCodes
div.copy-button-group.form-inline.m-b-3
.form-group
input.text-h4.semibold#join-code-input(value=state.get('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.m-b-3
.form-group
input.form-control.text-h4.semibold#join-url-input(value=state.get('joinURL'))
button#copy-url-btn.form-control.btn.btn-lg.btn-forest
span(data-i18n='teacher.copy_class_url')
div.text-center.small(data-i18n='teacher.class_join_url_blurb')
mixin bulkAssignControls
.bulk-assign-controls.form-inline
.no-students-selected.small-details(class=state.get('errors').assigningToNobody ? 'visible' : '')
span(data-i18n='teacher.no_students_selected')
.cant-assign-to-unenrolled.small-details(class=state.get('errors').assigningToUnenrolled ? 'visible' : '')
span(data-i18n='teacher.cant_assign_to_unenrolled')
span.small
span(data-i18n='teacher.bulk_assign')
span :
select.bulk-course-select.form-control
each trimCourse in _.rest(view.classroom.get('courses'))
- var course = view.courses.get(trimCourse._id)
option(value=course.id selected=(course===state.get('selectedCourse')))
= course.get('name')
button.btn.btn-primary-alt.assign-to-selected-students
span(data-i18n='teacher.assign_to_selected_students')
button.btn.btn-primary-alt.enroll-selected-students
span(data-i18n='teacher.enroll_selected_students')