mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 15:48:11 -05:00
Fix bugquest bugs
Hide TeachersContactModal after sending message Fix GET /db/level/:handle/session, more extensively test Fix EnrollmentView number of students input to stop losing focus on input Fix EnrollmentsView syntax Fix ActivateLicensesModal "Get Enrollments" button when already in the enrollments page Update EnrollmentsView with new credit numbers when ActivateLicensesModal closes Hide search box in TeacherClassView "Enrollment Status" tab Tweak EnrollmentsView styling Fix EnrollmentsView tests Fix AnalyticsView Make EnrollmentsView more explicitly handle undefined and empty array prepaid groups Remove log CoursesView handles JoinClassModal cancel Re-enable EditStudentModal set password button when the form changes Fix course instance tests, next level endpoint bug Fix EditStudentModal tests
This commit is contained in:
parent
3d705e5d70
commit
8dbc86ca04
19 changed files with 111 additions and 46 deletions
|
@ -32,9 +32,9 @@
|
|||
border-radius: 5px
|
||||
|
||||
#students-input
|
||||
width: 180px
|
||||
width: 220px
|
||||
height: 80px
|
||||
font-size: 60px
|
||||
font-size: 50px
|
||||
|
||||
&::-webkit-inner-spin-button, &::-webkit-outer-spin-button
|
||||
-webkit-appearance: none
|
||||
|
|
|
@ -48,14 +48,14 @@ block content
|
|||
.row.m-t-3
|
||||
if anyPrepaids
|
||||
#prepaids-col.col-md-9
|
||||
if available
|
||||
if _.size(available) > 0
|
||||
h5.m-b-1(data-i18n="teacher.available_credits")
|
||||
.row
|
||||
for prepaid in available
|
||||
.col-sm-4.col-xs-6
|
||||
+prepaidCard(prepaid)
|
||||
|
||||
if pending
|
||||
if _.size(pending) > 0
|
||||
h5.m-b-1.m-t-3(data-i18n="teacher.pending_credits")
|
||||
.row
|
||||
for prepaid in pending
|
||||
|
|
|
@ -378,14 +378,17 @@ mixin bulkAssignControls
|
|||
span(data-i18n='teacher.enroll_selected_students')
|
||||
|
||||
mixin enrollmentStatusTab
|
||||
form.form-inline.text-center.m-t-3
|
||||
#search-form-group.form-group
|
||||
label(for="student-search") Search for student:
|
||||
input#student-search.form-control.m-l-1(type="search")
|
||||
span.glyphicon.glyphicon-search.form-control-feedback
|
||||
// TODO: Have search input in all tabs
|
||||
|
||||
table.table#enrollment-status-table.table-condensed
|
||||
//form.form-inline.text-center.m-t-3
|
||||
// #search-form-group.form-group
|
||||
// label(for="student-search") Search for student:
|
||||
// input#student-search.form-control.m-l-1(type="search")
|
||||
// span.glyphicon.glyphicon-search.form-control-feedback
|
||||
|
||||
table.table#enrollment-status-table.table-condensed.m-t-3
|
||||
thead
|
||||
// Checkbox code works, but don't need it yet.
|
||||
//th.checkbox-col.select-all
|
||||
.checkbox-flat
|
||||
input(type='checkbox' id='checkbox-all-students')
|
||||
|
|
|
@ -7,28 +7,28 @@ block modal-header-content
|
|||
block modal-body-content
|
||||
p Send us a message and our classroom success team will be in touch to help find the best solution for your students' needs!
|
||||
form
|
||||
- var sending = view.state.get('sendingState') === 'sending';
|
||||
- var sending = view.state.get('sendingState') === 'sending'
|
||||
- var sent = view.state.get('sendingState') === 'sent';
|
||||
- var values = view.state.get('formValues');
|
||||
- var errors = view.state.get('formErrors');
|
||||
|
||||
.form-group(class=errors.email ? 'has-error' : '')
|
||||
label.control-label(for="email" data-i18n="general.email")
|
||||
+formErrors(errors.email)
|
||||
input.form-control(name="email", type="email", value=values.email || '', tabindex=1, disabled=sending)
|
||||
input.form-control(name="email", type="email", value=values.email || '', tabindex=1, disabled=sending || sent)
|
||||
|
||||
.form-group(class=errors.message ? 'has-error' : '')
|
||||
label.control-label(for="message" data-i18n="general.message")
|
||||
+formErrors(errors.message)
|
||||
textarea.form-control(name="message", tabindex=1 disabled=sending)= values.message
|
||||
textarea.form-control(name="message", tabindex=1 disabled=sending || sent)= values.message
|
||||
|
||||
if view.state.get('sendingState') === 'error'
|
||||
.alert.alert-danger Could not send message.
|
||||
|
||||
if view.state.get('sendingState') === 'sent'
|
||||
if sent
|
||||
.alert.alert-success Message sent!
|
||||
|
||||
.text-right
|
||||
- var sent = view.state.get('sendingState') === 'sent';
|
||||
button#submit-btn.btn.btn-navy.btn-lg(type='submit' disabled=sending || sent) Submit
|
||||
|
||||
block modal-footer
|
||||
|
|
|
@ -290,7 +290,7 @@ module.exports = class AnalyticsView extends RootView
|
|||
prepaidUserMap = {}
|
||||
for user in data.students
|
||||
continue unless studentPaidStatusMap[user._id]
|
||||
if prepaidID = user.coursePrepaidID or user.course.coursePrepaid?._id # TODO: make sure this works for coursePrepaid
|
||||
if prepaidID = user.coursePrepaidID or user.coursePrepaid?._id
|
||||
studentPaidStatusMap[user._id] = 'paid'
|
||||
prepaidUserMap[prepaidID] ?= []
|
||||
prepaidUserMap[prepaidID].push(user._id)
|
||||
|
|
|
@ -16,6 +16,7 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
'change input[type="checkbox"][name="user"]': 'updateSelectedStudents'
|
||||
'change select.classroom-select': 'replaceStudentList'
|
||||
'submit form': 'onSubmitForm'
|
||||
'click #get-more-licenses-btn': 'onClickGetMoreLicensesButton'
|
||||
|
||||
getInitialState: (options) ->
|
||||
selectedUsers = options.selectedUsers or options.users
|
||||
|
@ -113,3 +114,6 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
|
||||
finishRedeemUsers: ->
|
||||
@trigger 'redeem-users', @state.get('selectedUsers')
|
||||
|
||||
onClickGetMoreLicensesButton: ->
|
||||
@hide?() # In case this is opened in /teachers/enrollments itself, otherwise the button does nothing
|
||||
|
|
|
@ -102,6 +102,9 @@ module.exports = class CoursesView extends RootView
|
|||
@openModalView modal
|
||||
@listenTo modal, 'join:success', @onJoinClassroomSuccess
|
||||
@listenTo modal, 'join:error', @onJoinClassroomError
|
||||
@listenTo modal, 'hidden', ->
|
||||
@state = null
|
||||
@renderSelectors '#join-class-form'
|
||||
|
||||
onJoinClassroomError: (classroom, jqxhr, options) ->
|
||||
@state = null
|
||||
|
|
|
@ -44,8 +44,9 @@ module.exports = class EnrollmentsView extends RootView
|
|||
@prepaids.comparator = '_id'
|
||||
@supermodel.trackRequest @prepaids.fetchByCreator(me.id)
|
||||
@debouncedRender = _.debounce @render, 0
|
||||
@listenTo @prepaids, 'all', -> @state.set('prepaidGroups', @prepaids.groupBy((p) -> p.status()))
|
||||
@listenTo @prepaids, 'sync', @updatePrepaidGroups
|
||||
@listenTo(@state, 'all', @debouncedRender)
|
||||
@listenTo(me, 'change:enrollmentRequestSent', @debouncedRender)
|
||||
|
||||
onceClassroomsSync: ->
|
||||
for classroom in @classrooms.models
|
||||
|
@ -56,6 +57,9 @@ module.exports = class EnrollmentsView extends RootView
|
|||
@state.set('totalCourses', @courses.size())
|
||||
super()
|
||||
|
||||
updatePrepaidGroups: ->
|
||||
@state.set('prepaidGroups', @prepaids.groupBy((p) -> p.status()))
|
||||
|
||||
calculateEnrollmentStats: ->
|
||||
@removeDeletedStudents()
|
||||
|
||||
|
@ -95,10 +99,13 @@ module.exports = class EnrollmentsView extends RootView
|
|||
if input isnt "" and (parseFloat(input) isnt parseInt(input) or _.isNaN parseInt(input))
|
||||
@$('#students-input').val(@state.get('numberOfStudents'))
|
||||
else
|
||||
@state.set('numberOfStudents', Math.max(parseInt(@$('#students-input').val()) or 0, 0))
|
||||
@state.set({'numberOfStudents': Math.max(parseInt(@$('#students-input').val()) or 0, 0)}, {silent: true}) # do not re-render
|
||||
|
||||
numberOfStudentsIsValid: -> 0 < @get('numberOfStudents') < 100000
|
||||
|
||||
onClickEnrollStudentsButton: ->
|
||||
modal = new ActivateLicensesModal({ selectedUsers: @notEnrolledUsers, users: @members })
|
||||
@openModalView(modal)
|
||||
modal.once 'hidden', =>
|
||||
@prepaids.add(modal.prepaids.models, { merge: true })
|
||||
@debouncedRender() # Because one changed model does not a collection update make
|
||||
|
|
|
@ -272,7 +272,6 @@ module.exports = class TeacherClassView extends RootView
|
|||
@students.sort()
|
||||
|
||||
onKeyPressStudentSearch: (e) ->
|
||||
console.log 'emit event'
|
||||
@state.set('searchTerm', $(e.target).val())
|
||||
|
||||
getSelectedStudentIDs: ->
|
||||
|
|
|
@ -37,4 +37,9 @@ module.exports = class EditStudentModal extends ModalView
|
|||
@classroom.setStudentPassword(@user, @state.get('newPassword'))
|
||||
|
||||
onChangeNewPasswordInput: (e) ->
|
||||
@state.set { newPassword: $(e.currentTarget).val() }, { silent: true }
|
||||
@state.set {
|
||||
newPassword: $(e.currentTarget).val()
|
||||
emailSent: false
|
||||
passwordChanged: false
|
||||
}, { silent: true }
|
||||
@renderSelectors('.change-password-btn')
|
||||
|
|
|
@ -10,7 +10,6 @@ module.exports = class TeachersContactModal extends ModalView
|
|||
|
||||
events:
|
||||
'submit form': 'onSubmitForm'
|
||||
'change form': 'onChangeForm'
|
||||
|
||||
initialize: (options={}) ->
|
||||
@state = new State({
|
||||
|
@ -40,10 +39,6 @@ module.exports = class TeachersContactModal extends ModalView
|
|||
@state.set('formValues', { email, message })
|
||||
super()
|
||||
|
||||
onChangeForm: ->
|
||||
# Want to re-render without losing form focus. TODO: figure out how in state system.
|
||||
@$('#submit-btn').attr('disabled', false)
|
||||
|
||||
onSubmitForm: (e) ->
|
||||
e.preventDefault()
|
||||
return if @state.get('sendingState') is 'sending'
|
||||
|
@ -64,7 +59,12 @@ module.exports = class TeachersContactModal extends ModalView
|
|||
contact.send({
|
||||
data
|
||||
context: @
|
||||
success: -> @state.set({ sendingState: 'sent' })
|
||||
success: ->
|
||||
@state.set({ sendingState: 'sent' })
|
||||
me.set('enrollmentRequestSent', true)
|
||||
setTimeout(=>
|
||||
@hide?()
|
||||
, 3000)
|
||||
error: -> @state.set({ sendingState: 'error' })
|
||||
})
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ module.exports =
|
|||
throw new errors.NotFound('Level original ObjectId not found in Classroom courses')
|
||||
|
||||
if not nextLevelOriginal
|
||||
res.status(200).send({})
|
||||
return res.status(200).send({})
|
||||
|
||||
dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)})
|
||||
dbq.sort({ 'version.major': -1, 'version.minor': -1 })
|
||||
|
@ -137,7 +137,7 @@ module.exports =
|
|||
for courseInstance in courseInstances
|
||||
if members = courseInstance.get('members')
|
||||
userIDs.push(userID) for userID in members
|
||||
users = yield User.find({_id: {$in: userIDs}}, {coursePrepaid: 1})
|
||||
users = yield User.find({_id: {$in: userIDs}}, {coursePrepaid: 1, coursePrepaidID: 1})
|
||||
|
||||
prepaidIDs = []
|
||||
for user in users
|
||||
|
|
|
@ -50,12 +50,13 @@ module.exports =
|
|||
courseID = null
|
||||
classroomMap = {}
|
||||
classroomMap[classroom.id] = classroom for classroom in classrooms
|
||||
levelOriginal = level.get('original')
|
||||
for courseInstance in courseInstances
|
||||
classroom = classroomMap[courseInstance.get('classroomID').toString()]
|
||||
courseID = courseInstance.get('courseID')
|
||||
classroomCourse = _.find(classroom.get('courses'), (c) -> c._id.equals(courseID))
|
||||
for courseLevel in classroomCourse.levels
|
||||
if courseLevel.original.equals(level._id)
|
||||
if courseLevel.original.equals(levelOriginal)
|
||||
classroomWithLevel = classroom
|
||||
break
|
||||
break if classroomWithLevel
|
||||
|
@ -67,7 +68,7 @@ module.exports =
|
|||
unless course.get('free') or req.user.isEnrolled()
|
||||
throw new errors.PaymentRequired('You must be enrolled to access this content')
|
||||
|
||||
lang = classroomWithLevel.get('aceConfig').language
|
||||
lang = classroomWithLevel.get('aceConfig')?.language
|
||||
attrs.codeLanguage = lang if lang
|
||||
|
||||
else
|
||||
|
|
|
@ -73,7 +73,7 @@ module.exports.setup = (app) ->
|
|||
app.get('/db/course/:handle', mw.rest.getByHandle(Course))
|
||||
app.get('/db/course/:handle/levels/:levelOriginal/next', mw.courses.fetchNextLevel)
|
||||
|
||||
app.get('/db/course_instance/-/recent', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchRecent)
|
||||
app.post('/db/course_instance/-/recent', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchRecent)
|
||||
app.get('/db/course_instance/:handle/levels/:levelOriginal/next', mw.courseInstances.fetchNextLevel)
|
||||
app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers)
|
||||
app.get('/db/course_instance/:handle/classroom', mw.auth.checkLoggedIn(), mw.courseInstances.fetchClassroom)
|
||||
|
|
|
@ -361,7 +361,7 @@ describe 'GET /db/course_instance/:handle/classroom', ->
|
|||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
describe 'GET /db/course_instance/-/recent', ->
|
||||
describe 'POST /db/course_instance/-/recent', ->
|
||||
|
||||
url = getURL('/db/course_instance/-/recent')
|
||||
|
||||
|
@ -383,7 +383,7 @@ describe 'GET /db/course_instance/-/recent', ->
|
|||
done()
|
||||
|
||||
it 'returns all non-HoC course instances and their related users and prepaids', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync(url, { json: true })
|
||||
[res, body] = yield request.postAsync(url, { json: true })
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.body.courseInstances[0]._id).toBe(@courseInstance.id)
|
||||
expect(res.body.students[0]._id).toBe(@student.id)
|
||||
|
@ -393,23 +393,23 @@ describe 'GET /db/course_instance/-/recent', ->
|
|||
it 'returns course instances within a specified range', utils.wrap (done) ->
|
||||
startDay = moment().subtract(1, 'day').format('YYYY-MM-DD')
|
||||
endDay = moment().add(1, 'day').format('YYYY-MM-DD')
|
||||
[res, body] = yield request.getAsync(url, { json: { startDay, endDay } })
|
||||
[res, body] = yield request.postAsync(url, { json: { startDay, endDay } })
|
||||
expect(res.body.courseInstances.length).toBe(1)
|
||||
|
||||
startDay = moment().add(1, 'day').format('YYYY-MM-DD')
|
||||
endDay = moment().add(2, 'day').format('YYYY-MM-DD')
|
||||
[res, body] = yield request.getAsync(url, { json: { startDay, endDay } })
|
||||
[res, body] = yield request.postAsync(url, { json: { startDay, endDay } })
|
||||
expect(res.body.courseInstances.length).toBe(0)
|
||||
|
||||
startDay = moment().subtract(2, 'day').format('YYYY-MM-DD')
|
||||
endDay = moment().subtract(1, 'day').format('YYYY-MM-DD')
|
||||
[res, body] = yield request.getAsync(url, { json: { startDay, endDay } })
|
||||
[res, body] = yield request.postAsync(url, { json: { startDay, endDay } })
|
||||
expect(res.body.courseInstances.length).toBe(0)
|
||||
|
||||
done()
|
||||
|
||||
it 'returns 403 if not an admin', utils.wrap (done) ->
|
||||
yield utils.loginUser(@teacher)
|
||||
[res, body] = yield request.getAsync(url, { json: true })
|
||||
[res, body] = yield request.postAsync(url, { json: true })
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
|
|
@ -8,6 +8,7 @@ User = require '../../../server/models/User'
|
|||
request = require '../request'
|
||||
utils = require '../utils'
|
||||
moment = require 'moment'
|
||||
mongoose = require 'mongoose'
|
||||
|
||||
describe 'Level', ->
|
||||
|
||||
|
@ -42,13 +43,18 @@ describe 'Level', ->
|
|||
|
||||
describe 'GET /db/level/:handle/session', ->
|
||||
|
||||
describe 'when level is a course level', ->
|
||||
describe 'when level IS a course level', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Campaign, Course, CourseInstance, Level, User])
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
@level = yield utils.makeLevel({type: 'course'})
|
||||
|
||||
# To ensure test compares original, not id, make them different. TODO: Make factories do this normally?
|
||||
@level.set('original', new mongoose.Types.ObjectId())
|
||||
@level.save()
|
||||
|
||||
@campaign = yield utils.makeCampaign({}, {levels: [@level]})
|
||||
@course = yield utils.makeCourse({free: true}, {campaign: @campaign})
|
||||
@student = yield utils.initUser({role: 'student'})
|
||||
|
@ -67,6 +73,14 @@ describe 'GET /db/level/:handle/session', ->
|
|||
expect(body.codeLanguage).toBe('javascript')
|
||||
done()
|
||||
|
||||
it 'works if the classroom has no aceConfig', utils.wrap (done) ->
|
||||
@classroom.set('aceConfig', undefined)
|
||||
yield @classroom.save()
|
||||
[res, body] = yield request.getAsync { uri: @url, json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.codeLanguage).toBe('python')
|
||||
done()
|
||||
|
||||
it 'returns 402 if the user is not in a course with that level', utils.wrap (done) ->
|
||||
otherStudent = yield utils.initUser({role: 'student'})
|
||||
yield utils.loginUser(otherStudent)
|
||||
|
@ -136,3 +150,31 @@ describe 'GET /db/level/:handle/session', ->
|
|||
[res, body] = yield request.getAsync { uri: @url, json: true }
|
||||
expect(body._id).toBe(sessionID)
|
||||
done()
|
||||
|
||||
describe 'when the level is not free', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield @level.update({$set: {requiresSubscription: true}})
|
||||
done()
|
||||
|
||||
it 'returns 402 for normal users', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: @url, json: true }
|
||||
expect(res.statusCode).toBe(402)
|
||||
done()
|
||||
|
||||
it 'returns 200 for admins', utils.wrap (done) ->
|
||||
yield @player.update({$set: {permissions: ['admin']}})
|
||||
[res, body] = yield request.getAsync { uri: @url, json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
done()
|
||||
|
||||
it 'returns 200 for adventurer levels', utils.wrap (done) ->
|
||||
yield @level.update({$set: {adventurer: true}})
|
||||
[res, body] = yield request.getAsync { uri: @url, json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
done()
|
||||
|
||||
it 'returns 200 for subscribed users', utils.wrap (done) ->
|
||||
yield @player.update({$set: {stripe: {free: true}}})
|
||||
[res, body] = yield request.getAsync { uri: @url, json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
done()
|
||||
|
|
|
@ -11,6 +11,7 @@ describe 'EnrollmentsView', ->
|
|||
beforeEach (done) ->
|
||||
me.set('anonymous', false)
|
||||
me.set('role', 'teacher')
|
||||
me.set('enrollmentRequestSent', false)
|
||||
@view = new EnrollmentsView()
|
||||
|
||||
# Make three classrooms, sharing users from a pool of 10, 5 of which are enrolled
|
||||
|
@ -92,7 +93,8 @@ describe 'EnrollmentsView', ->
|
|||
|
||||
describe 'when there are no prepaids to show', ->
|
||||
beforeEach (done) ->
|
||||
@view.prepaids.reset()
|
||||
@view.prepaids.reset([])
|
||||
@view.updatePrepaidGroups()
|
||||
_.defer(done)
|
||||
|
||||
it 'fills the void with the rest of the page content', ->
|
||||
|
|
|
@ -45,9 +45,6 @@ describe 'TeachersContactModal', ->
|
|||
it 'shows a success message', ->
|
||||
expect(@modal.$('.alert-success').length).toBe(1)
|
||||
|
||||
describe 'submit button', ->
|
||||
it 'is disabled until one of the inputs changes', ->
|
||||
it 'disables the submit button', ->
|
||||
expect(@modal.$('#submit-btn').is(':disabled')).toBe(true)
|
||||
@modal.$('input[name="email"]').trigger('change')
|
||||
expect(@modal.$('#submit-btn').is(':disabled')).toBe(false)
|
||||
|
||||
|
|
|
@ -12,7 +12,8 @@ describe 'EditStudentModal', ->
|
|||
describe 'for a verified user', ->
|
||||
beforeEach (done) ->
|
||||
user = factories.makeUser({ email, emailVerified: true })
|
||||
modal = new EditStudentModal({ user })
|
||||
classroom = factories.makeClassroom()
|
||||
modal = new EditStudentModal({ user, classroom })
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({ status: 200, responseText: JSON.stringify(user) })
|
||||
jasmine.demoModal(modal)
|
||||
|
@ -37,7 +38,8 @@ describe 'EditStudentModal', ->
|
|||
describe 'for an unverified user', ->
|
||||
beforeEach (done) ->
|
||||
user = factories.makeUser({ email , emailVerified: false })
|
||||
modal = new EditStudentModal({ user })
|
||||
classroom = factories.makeClassroom()
|
||||
modal = new EditStudentModal({ user, classroom })
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({ status: 200, responseText: JSON.stringify(user) })
|
||||
jasmine.demoModal(modal)
|
||||
|
|
Loading…
Reference in a new issue