From 4c4b3019255414b2618417e0e91a0ffba54902aa Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 5 Nov 2015 17:26:31 -0800 Subject: [PATCH] Add functionality to TeacherCoursesView --- app/styles/courses/teacher-courses-view.sass | 19 ++- .../courses/teacher-courses-view.jade | 62 +++++++- app/views/courses/TeacherCoursesView.coffee | 148 +++++++++++++++++- 3 files changed, 223 insertions(+), 6 deletions(-) diff --git a/app/styles/courses/teacher-courses-view.sass b/app/styles/courses/teacher-courses-view.sass index 67b181e27..770710c90 100644 --- a/app/styles/courses/teacher-courses-view.sass +++ b/app/styles/courses/teacher-courses-view.sass @@ -1 +1,18 @@ -//#teacher-courses-view +#teacher-courses-view + margin-bottom: 50px + + #fixed-area + position: fixed + bottom: 0 + left: 0 + right: 0 + + .well + margin-bottom: 0 + padding: 5px + + .col-sm-5 + padding-top: 8px + + .progress + margin-bottom: 0 \ No newline at end of file diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index afb007e32..2c03fbe1e 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -6,9 +6,20 @@ block content a.spl(href='mailto:team@codecombat.com') team@codecombat.com hr + + p Create a class and add students to it. + + - var totalRedeemers = view.prepaids.totalRedeemers(); + - var totalMaxRedeemers = view.prepaids.totalMaxRedeemers(); + + .text-right + span.spr Available paid seats: #{totalRedeemers}/#{totalMaxRedeemers} + a.btn.btn-default.btn-xs(href="/courses/purchase") Add for classroom in view.classrooms.models h2= classroom.get('name') + + - var courseInstances = view.courseInstances.where({classroomID: classroom.id}) if classroom.saving .progress @@ -17,13 +28,32 @@ block content table.table tr th Student - for course in view.courses.models - th= course.get('name') + for courseInstance in courseInstances + th + if courseInstance.course + | #{courseInstance.course.get('name')} if !_.size(classroom.get('members')) tr td(colspan=1+view.courses.size()) em No students in this class yet. + + for member in classroom.get('members') || [] + - var user = view.members.get(member); + if !user + - continue; + tr + td= user.get('name') + for courseInstance in courseInstances + td + if _.contains(courseInstance.get('members'), user.id) + span.glyphicon.glyphicon-ok + else + input.course-instance-membership-checkbox( + type='checkbox' + data-course-instance-id=courseInstance.id + data-user-id=user.id + ) button.add-students-btn.btn.btn-sm(data-classroom-id=classroom.id) Add Students @@ -33,4 +63,30 @@ block content .col-sm-3.col-sm-offset-3 button#create-new-class-btn.btn.btn-default.btn-block Create New Class .col-sm-3 - input#new-classroom-name-input.form-control(placeholder='new class name') \ No newline at end of file + input#new-classroom-name-input.form-control(placeholder='new class name') + + #fixed-area + .container + .row.well + if view.state === 'saving-changes' + p Saving changes + - var total = view.membershipAdditions.originalSize + view.usersToRedeem.originalSize; + - var left = view.membershipAdditions.size() + view.usersToRedeem.size(); + - var pct = Math.max(10, (100 * (total - left) / total)).toFixed(1) + '%'; + .progress.progress-striped.active + .progress-bar(style="width: #{pct}") + else + - var seatsLeft = totalMaxRedeemers - totalRedeemers - view.usersToRedeem.size(); + if seatsLeft < 0 + .alert.alert-danger + span.spr You do not have enough seats to accommodate all students you have selected. + a(href="/courses/purchase") Buy more seats. + else + .col-sm-2 + button#save-changes-btn.btn.btn-primary.btn-block(disabled=!view.numCourseInstancesToAddTo) Save Changes + .col-sm-5 + | Students to add to courses: #{view.numCourseInstancesToAddTo || 0} + .col-sm-5 + | Seats to expend: #{view.usersToRedeem.size()} (will have #{seatsLeft} seats left) + +block footer \ No newline at end of file diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index 4c7aa7614..4a2e252a7 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -1,8 +1,11 @@ app = require 'core/application' AuthModal = require 'views/core/AuthModal' CocoCollection = require 'collections/CocoCollection' +CocoModel = require 'models/CocoModel' Course = require 'models/Course' Classroom = require 'models/Classroom' +User = require 'models/User' +Prepaid = require 'models/Prepaid' CourseInstance = require 'models/CourseInstance' RootView = require 'views/core/RootView' template = require 'templates/courses/teacher-courses-view' @@ -16,16 +19,38 @@ module.exports = class TeacherCoursesView extends RootView events: 'click #create-new-class-btn': 'onClickCreateNewclassButton' 'click .add-students-btn': 'onClickAddStudentsButton' + 'click .course-instance-membership-checkbox': 'onClickCourseInstanceMembershipCheckbox' + 'click #save-changes-btn': 'onClickSaveChangesButton' constructor: (options) -> super(options) @courses = new CocoCollection([], { url: "/db/course", model: Course}) @supermodel.loadCollection(@courses, 'courses') @classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom }) - @listenToOnce @classrooms, 'sync', @onCourseInstancesLoaded + @classrooms.comparator = '_id' + @listenToOnce @classrooms, 'sync', @onceClassroomsSync @supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}}) + @courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance }) + @courseInstances.comparator = 'courseID' + @supermodel.loadCollection(@courseInstances, 'course_instances', {data: {ownerID: me.id}}) + @members = new CocoCollection([], { model: User }) + @prepaids = new CocoCollection([], { url: "/db/prepaid", model: Prepaid }) + sum = (numbers) -> _.reduce(numbers, (a, b) -> a + b) + @prepaids.totalMaxRedeemers = -> sum((prepaid.get('maxRedeemers') for prepaid in @models)) + @prepaids.totalRedeemers = -> sum((_.size(prepaid.get('redeemers')) for prepaid in @models)) + @prepaids.comparator = '_id' + @supermodel.loadCollection(@prepaids, 'prepaids', {data: {creator: me.id}}) + @listenTo @members, 'sync', @render + @usersToRedeem = new CocoCollection([], { model: User }) @ + onceClassroomsSync: -> + for classroom in @classrooms.models + @members.fetch({ + remove: false + url: "/db/classroom/#{classroom.id}/members" + }) + onClickCreateNewclassButton: -> name = @$('#new-classroom-name-input').val() return unless name @@ -42,4 +67,123 @@ module.exports = class TeacherCoursesView extends RootView classroomID = $(e.target).data('classroom-id') classroom = @classrooms.get(classroomID) modal = new InviteToClassroomModal({classroom: classroom}) - @openModalView(modal) \ No newline at end of file + @openModalView(modal) + + onLoaded: -> + super() + @linkCourseIntancesToCourses() + @fillMissingCourseInstances() + + linkCourseIntancesToCourses: -> + for courseInstance in @courseInstances.models + courseInstance.course = @courses.get(courseInstance.get('courseID')) + + fillMissingCourseInstances: -> + # TODO: Give teachers control over which courses are enabled for a given class. + # Add/remove course instances and columns in the view to match. + for classroom in @classrooms.models + for course in @courses.models + courseInstance = @courseInstances.findWhere({classroomID: classroom.id, courseID: course.id}) + if not courseInstance + courseInstance = new CourseInstance({ + classroomID: classroom.id + courseID: course.id + }) + # TODO: figure out a better way to get around triggering validation errors for properties + # that the server will end up filling in, like an empty members array, ownerID + courseInstance.save(null, {validate: false}) + courseInstance.course = course + @courseInstances.add(courseInstance) + @listenToOnce courseInstance, 'sync', @fillMissingCourseInstances + return + + onClickCourseInstanceMembershipCheckbox: -> + usersToRedeem = {} + checkedBoxes = @$('.course-instance-membership-checkbox:checked') + _.each checkedBoxes, (el) => + $el = $(el) + userID = $el.data('user-id') + return if usersToRedeem[userID] + user = @members.get(userID) + return if user.get('coursePrepaidID') + courseInstanceID = $el.data('course-instance-id') + courseInstance = @courseInstances.get(courseInstanceID) + return if courseInstance.course.get('free') + usersToRedeem[userID] = user + + @usersToRedeem = new CocoCollection(_.values(usersToRedeem), {model: User}) + @numCourseInstancesToAddTo = checkedBoxes.length + @renderSelectors '#fixed-area' + + onClickSaveChangesButton: -> + @$('.course-instance-membership-checkbox').attr('disabled', true) + checkedBoxes = @$('.course-instance-membership-checkbox:checked') + raw = _.map checkedBoxes, (el) => + $el = $(el) + userID = $el.data('user-id') + courseInstanceID = $el.data('course-instance-id') + courseInstance = @courseInstances.get(courseInstanceID) + return { + courseInstance: courseInstance + userID: userID + } + @membershipAdditions = new CocoCollection(raw, { model: User }) # TODO: Allow collections not to have models defined? + @membershipAdditions.originalSize = @membershipAdditions.size() + @usersToRedeem.originalSize = @usersToRedeem.size() + @state = 'saving-changes' + @renderSelectors '#fixed-area' + @redeemUsers() + + redeemUsers: -> + if not @usersToRedeem.size() + @addMemberships() + return + + user = @usersToRedeem.first() + prepaid = @prepaids.find (prepaid) -> prepaid.openSpots() + $.ajax({ + method: 'POST' + url: _.result(prepaid, 'url') + '/redeemers' + data: { userID: user.id } + context: @ + success: -> + @usersToRedeem.remove(user) + @renderSelectors '#fixed-area' + @redeemUsers() + error: (jqxhr, textStatus, errorThrown) -> + if jqxhr.status is 402 + @state = 'error' + @stateMessage = arguments[2] + else + @state = 'error' + @stateMessage = "#{jqxhr.status}: #{jqxhr.responseText}" + @renderSelectors '#fixed-area' + }) + + addMemberships: -> + if not @membershipAdditions.size() + @renderSelectors '#fixed-area' + document.location.reload() + return + + membershipAddition = @membershipAdditions.first() + courseInstance = membershipAddition.get('courseInstance') + userID = membershipAddition.get('userID') + $.ajax({ + method: 'POST' + url: _.result(courseInstance, 'url') + '/members' + data: { userID: userID } + context: @ + success: -> + @membershipAdditions.remove(membershipAddition) + @renderSelectors '#fixed-area' + @addMemberships() + error: (jqxhr, textStatus, errorThrown) -> + if jqxhr.status is 402 + @state = 'error' + @stateMessage = arguments[2] + else + @state = 'error' + @stateMessage = "#{jqxhr.status}: #{jqxhr.responseText}" + @renderSelectors '#fixed-area' + }) \ No newline at end of file