mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Add export student progress csv to class view
Works on Chrome and Firefox, not so great on Safari, untested on IE and Edge.
This commit is contained in:
parent
8c7bfc0d04
commit
b70e9bbcfe
4 changed files with 98 additions and 62 deletions
|
@ -1349,6 +1349,7 @@
|
|||
how_to_enroll_blurb_3: "Once a student is enrolled, they will have access to all of the course content."
|
||||
bulk_pricing_blurb: "Purchasing for more than 25 students? Contact us to discuss next steps."
|
||||
total_unenrolled: "Total unenrolled"
|
||||
export_student_progress: "Export Student Progress (CSV)"
|
||||
|
||||
classes:
|
||||
archmage_title: "Archmage"
|
||||
|
|
|
@ -253,4 +253,6 @@
|
|||
height: 50px
|
||||
width: 210px
|
||||
float: right
|
||||
|
||||
|
||||
.export-student-progress-btn
|
||||
margin-top: 10px
|
|
@ -12,9 +12,9 @@ block content
|
|||
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.
|
||||
| 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
|
||||
|
@ -22,47 +22,51 @@ block content
|
|||
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 = view.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 view.progressData
|
||||
//- div
|
||||
|
@ -74,7 +78,7 @@ block content
|
|||
//- if state.started
|
||||
//- b.concept(class=state.completed ? 'forest' : 'gold')
|
||||
//- span(data-i18n='concepts.'+name)
|
||||
|
||||
|
||||
.completeness-info.col-md-4
|
||||
h4.m-b-2
|
||||
|
||||
|
@ -84,21 +88,21 @@ block content
|
|||
span :
|
||||
+longLevelName(view.earliestIncompleteLevel)
|
||||
+inlineUserList(view.earliestIncompleteLevel.users)
|
||||
|
||||
|
||||
if view.latestCompleteLevel
|
||||
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.m-b-2
|
||||
span(data-i18n='teacher.adding_students')
|
||||
span :
|
||||
+copyCodes
|
||||
+addStudentsButton
|
||||
|
||||
|
||||
ul.nav.nav-tabs.m-t-5(role='tablist')
|
||||
li.active
|
||||
a(href='#students-tab' data-toggle='tab')
|
||||
|
@ -108,11 +112,11 @@ block content
|
|||
a(href='#course-progress-tab' data-toggle='tab')
|
||||
.small-details.text-center(data-i18n='teacher.course_progress')
|
||||
.tab-filler
|
||||
|
||||
|
||||
.tab-content
|
||||
+studentsTab
|
||||
+courseProgressTab
|
||||
|
||||
|
||||
mixin breadcrumbs
|
||||
.breadcrumbs
|
||||
a(data-i18n='teacher.my_classes' href='/teachers/classes')
|
||||
|
@ -120,7 +124,7 @@ mixin breadcrumbs
|
|||
//- TODO: Use .glyphicon-menu-right when we update bootstrap
|
||||
span
|
||||
= view.classroom.get('name')
|
||||
|
||||
|
||||
mixin longLevelName(data)
|
||||
if data
|
||||
div.level-name
|
||||
|
@ -132,7 +136,7 @@ mixin longLevelName(data)
|
|||
span= data.levelName
|
||||
else
|
||||
div.level-name(data-i18n='teacher.not_applicable')
|
||||
|
||||
|
||||
mixin inlineUserList(users)
|
||||
if users
|
||||
ul.inline-student-list.small
|
||||
|
@ -147,7 +151,7 @@ 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.tab-pane.active
|
||||
+bulkAssignControls
|
||||
|
@ -170,7 +174,7 @@ mixin sortButtons
|
|||
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
|
||||
|
@ -213,7 +217,7 @@ mixin studentRow(student)
|
|||
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.tab-pane.m-t-3
|
||||
if view.courses
|
||||
|
@ -232,14 +236,14 @@ mixin courseProgressTab
|
|||
+sortButtons
|
||||
each student in view.students.models
|
||||
+studentLevelsRow(student)
|
||||
|
||||
|
||||
mixin courseOverview
|
||||
- var course = view.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 :
|
||||
span(data-i18n='teacher.course_overview')
|
||||
.course-overview-progress
|
||||
each level, index in levels
|
||||
|
@ -257,7 +261,7 @@ mixin studentLevelsRow(student)
|
|||
each level, index in levels
|
||||
- var progress = view.progressData.get({ classroom: view.classroom, course: course, level: level, user: student })
|
||||
+progressDot(progress, index+1)
|
||||
|
||||
|
||||
mixin progressDot(progress, label)
|
||||
//- TODO: Refactor with TeacherClassesView jade
|
||||
//- TODO: Give classes abbreviations instead of using index?
|
||||
|
@ -278,7 +282,7 @@ mixin copyCodes
|
|||
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=view.joinURL)
|
||||
|
@ -303,4 +307,4 @@ mixin bulkAssignControls
|
|||
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')
|
||||
span(data-i18n='teacher.enroll_selected_students')
|
|
@ -8,6 +8,7 @@ RemoveStudentModal = require 'views/courses/RemoveStudentModal'
|
|||
|
||||
Classroom = require 'models/Classroom'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
Levels = require 'collections/Levels'
|
||||
LevelSessions = require 'collections/LevelSessions'
|
||||
User = require 'models/User'
|
||||
Users = require 'collections/Users'
|
||||
|
@ -18,7 +19,7 @@ CourseInstances = require 'collections/CourseInstances'
|
|||
module.exports = class TeacherClassView extends RootView
|
||||
id: 'teacher-class-view'
|
||||
template: template
|
||||
|
||||
|
||||
events:
|
||||
'click .edit-classroom': 'onClickEditClassroom'
|
||||
'click .add-students-btn': 'onClickAddStudents'
|
||||
|
@ -30,6 +31,7 @@ module.exports = class TeacherClassView extends RootView
|
|||
'click .enroll-student-button': 'onClickEnroll'
|
||||
'click .assign-to-selected-students': 'onClickBulkAssign'
|
||||
'click .enroll-selected-students': 'onClickBulkEnroll'
|
||||
'click .export-student-progress-btn': 'onClickExportStudentProgress'
|
||||
'click .select-all': 'onClickSelectAll'
|
||||
'click .student-checkbox': 'onClickStudentCheckbox'
|
||||
'change .course-select': 'onChangeCourseSelect'
|
||||
|
@ -37,14 +39,14 @@ module.exports = class TeacherClassView extends RootView
|
|||
initialize: (options, classroomID) ->
|
||||
super(options)
|
||||
@progressDotTemplate = require 'templates/courses/progress-dot'
|
||||
|
||||
|
||||
@sortAttribute = 'name'
|
||||
@sortDirection = 1
|
||||
|
||||
|
||||
@classroom = new Classroom({ _id: classroomID })
|
||||
@classroom.fetch()
|
||||
@supermodel.trackModel(@classroom)
|
||||
|
||||
|
||||
@listenTo @classroom, 'sync', ->
|
||||
@students = new Users()
|
||||
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
|
||||
|
@ -52,47 +54,51 @@ module.exports = class TeacherClassView extends RootView
|
|||
@supermodel.trackCollection(@students)
|
||||
@listenTo @students, 'sync', @sortByName
|
||||
@listenTo @students, 'sort', @renderSelectors.bind(@, '.students-table', '.student-levels-table')
|
||||
|
||||
|
||||
@classroom.sessions = new LevelSessions()
|
||||
requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom)
|
||||
@supermodel.trackRequests(requests)
|
||||
|
||||
|
||||
@courses = new Courses()
|
||||
@courses.fetch()
|
||||
@supermodel.trackCollection(@courses)
|
||||
|
||||
|
||||
@courseInstances = new CourseInstances()
|
||||
@courseInstances.fetchForClassroom(classroomID)
|
||||
@supermodel.trackCollection(@courseInstances)
|
||||
|
||||
@levels = new Levels()
|
||||
@levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}})
|
||||
@supermodel.trackCollection(@levels)
|
||||
|
||||
onLoaded: ->
|
||||
@removeDeletedStudents()
|
||||
|
||||
|
||||
@classCode = @classroom.get('codeCamel') or @classroom.get('code')
|
||||
@joinURL = document.location.origin + "/courses?_cc=" + @classCode
|
||||
|
||||
|
||||
@earliestIncompleteLevel = helper.calculateEarliestIncomplete(@classroom, @courses, @courseInstances, @students)
|
||||
@latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, @students)
|
||||
for student in @students.models
|
||||
# TODO: this is a weird hack
|
||||
studentsStub = new Users([ student ])
|
||||
student.latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, studentsStub)
|
||||
|
||||
|
||||
classroomsStub = new Classrooms([ @classroom ])
|
||||
@progressData = helper.calculateAllProgress(classroomsStub, @courses, @courseInstances, @students)
|
||||
# @conceptData = helper.calculateConceptsCovered(classroomsStub, @courses, @campaigns, @courseInstances, @students)
|
||||
|
||||
|
||||
@selectedCourse = @courses.first()
|
||||
super()
|
||||
|
||||
|
||||
copyCode: ->
|
||||
@$('#join-code-input').val(@classCode).select()
|
||||
@tryCopy()
|
||||
|
||||
|
||||
copyURL: ->
|
||||
@$('#join-url-input').val(@joinURL).select()
|
||||
@tryCopy()
|
||||
|
||||
|
||||
tryCopy: ->
|
||||
try
|
||||
document.execCommand('copy')
|
||||
|
@ -100,13 +106,13 @@ module.exports = class TeacherClassView extends RootView
|
|||
catch err
|
||||
message = 'Oops, unable to copy'
|
||||
noty text: message, layout: 'topCenter', type: 'error', killer: false
|
||||
|
||||
|
||||
onClickEditClassroom: (e) ->
|
||||
classroom = @classroom
|
||||
modal = new ClassroomSettingsModal({ classroom: classroom })
|
||||
@openModalView(modal)
|
||||
@listenToOnce modal, 'hide', @render
|
||||
|
||||
|
||||
onClickRemoveStudentLink: (e) ->
|
||||
user = @students.get($(e.currentTarget).data('student-id'))
|
||||
modal = new RemoveStudentModal({
|
||||
|
@ -126,34 +132,34 @@ module.exports = class TeacherClassView extends RootView
|
|||
modal = new InviteToClassroomModal({ classroom: @classroom })
|
||||
@openModalView(modal)
|
||||
@listenToOnce modal, 'hide', @render
|
||||
|
||||
|
||||
removeDeletedStudents: () ->
|
||||
_.remove(@classroom.get('members'), (memberID) =>
|
||||
not @students.get(memberID) or @students.get(memberID)?.get('deleted')
|
||||
)
|
||||
true
|
||||
|
||||
|
||||
sortByName: (e) ->
|
||||
if @sortValue is 'name'
|
||||
@sortDirection = -@sortDirection
|
||||
else
|
||||
@sortValue = 'name'
|
||||
@sortDirection = 1
|
||||
|
||||
|
||||
dir = @sortDirection
|
||||
@students.comparator = (student1, student2) ->
|
||||
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
|
||||
@students.sort()
|
||||
|
||||
|
||||
sortByProgress: (e) ->
|
||||
if @sortValue is 'progress'
|
||||
@sortDirection = -@sortDirection
|
||||
else
|
||||
@sortValue = 'progress'
|
||||
@sortDirection = 1
|
||||
|
||||
|
||||
dir = @sortDirection
|
||||
|
||||
|
||||
@students.comparator = (student) ->
|
||||
#TODO: I would like for this to be in the Level model,
|
||||
# but it doesn't know about its own courseNumber
|
||||
|
@ -162,13 +168,13 @@ module.exports = class TeacherClassView extends RootView
|
|||
return -dir
|
||||
return dir * ((1000 * level.courseNumber) + level.levelNumber)
|
||||
@students.sort()
|
||||
|
||||
|
||||
getSelectedStudentIDs: ->
|
||||
@$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
|
||||
$(checkbox).data('student-id')
|
||||
|
||||
|
||||
ensureInstance: (courseID) ->
|
||||
|
||||
|
||||
onClickEnroll: (e) ->
|
||||
userID = $(e.currentTarget).data('user-id')
|
||||
user = @students.get(userID)
|
||||
|
@ -177,7 +183,7 @@ module.exports = class TeacherClassView extends RootView
|
|||
@openModalView(modal)
|
||||
modal.once 'redeem-users', -> document.location.reload()
|
||||
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
||||
|
||||
|
||||
onClickBulkEnroll: ->
|
||||
courseID = @$('.bulk-course-select').val()
|
||||
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
||||
|
@ -187,7 +193,30 @@ module.exports = class TeacherClassView extends RootView
|
|||
@openModalView(modal)
|
||||
modal.once 'redeem-users', -> document.location.reload()
|
||||
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
||||
|
||||
|
||||
onClickExportStudentProgress: ->
|
||||
# TODO: Does not yield .csv download on Safari, and instead opens a new tab with the .csv contents
|
||||
csvContent = "data:text/csv;charset=utf-8,Username, Email, Playtime, Concepts\n"
|
||||
for student in @students.models
|
||||
concepts = []
|
||||
for course, index in @courses.models
|
||||
instance = @courseInstances.findWhere({ courseID: course.id, classroomID: @classroom.id })
|
||||
if instance && instance.hasMember(student)
|
||||
# TODO: @levels collection is for the classroom, and not per-course
|
||||
for level, index in @levels.models
|
||||
progress = @progressData.get({ classroom: @classroom, course: course, level: level, user: student })
|
||||
concepts.push(level.get('concepts') ? []) if progress?.completed
|
||||
concepts = _.union(_.flatten(concepts))
|
||||
conceptsString = _.map(concepts, (c) -> $.i18n.t("concepts." + c)).join(', ')
|
||||
playtime = 0
|
||||
for session in @classroom.sessions.models when session.get('creator') is student.id
|
||||
playtime += session.get('playtime') or 0
|
||||
playtimeString = moment.duration(playtime, 'seconds').humanize()
|
||||
csvContent += "#{student.get('name')},#{student.get('email')},#{playtimeString},\"#{conceptsString}\"\n"
|
||||
csvContent = csvContent.substring(0, csvContent.length - 1)
|
||||
encodedUri = encodeURI(csvContent)
|
||||
window.open(encodedUri)
|
||||
|
||||
onClickBulkAssign: ->
|
||||
courseID = @$('.bulk-course-select').val()
|
||||
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
||||
|
@ -196,12 +225,12 @@ module.exports = class TeacherClassView extends RootView
|
|||
user = @students.get(userID)
|
||||
user.isEnrolled()
|
||||
).toArray()
|
||||
|
||||
|
||||
@assigningToUnenrolled = _.any selectedIDs, (userID) =>
|
||||
not @students.get(userID).isEnrolled()
|
||||
|
||||
|
||||
@$('.cant-assign-to-unenrolled').toggleClass('visible', @assigningToUnenrolled)
|
||||
|
||||
|
||||
@assigningToNobody = selectedIDs.length is 0
|
||||
@$('.no-students-selected').toggleClass('visible', @assigningToNobody)
|
||||
|
||||
|
@ -224,11 +253,11 @@ module.exports = class TeacherClassView extends RootView
|
|||
}
|
||||
}
|
||||
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')
|
||||
|
@ -239,7 +268,7 @@ module.exports = class TeacherClassView extends RootView
|
|||
@$('.select-all input').prop('checked', true)
|
||||
checkboxes.prop('checked', true)
|
||||
null
|
||||
|
||||
|
||||
onClickStudentCheckbox: (e) ->
|
||||
e.preventDefault()
|
||||
# $(e.target).$()
|
||||
|
@ -248,7 +277,7 @@ module.exports = class TeacherClassView extends RootView
|
|||
# checkboxes.prop('checked', false)
|
||||
checkboxes = @$('.student-checkbox input')
|
||||
@$('.select-all input').prop('checked', _.all(checkboxes, 'checked'))
|
||||
|
||||
|
||||
onChangeCourseSelect: (e) ->
|
||||
@selectedCourse = @courses.get($(e.currentTarget).val())
|
||||
@renderSelectors('.render-on-course-sync')
|
||||
|
|
Loading…
Reference in a new issue