mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-01 03:16:56 -05:00
8496343a02
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
371 lines
14 KiB
Text
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')
|