diff --git a/app/locale/en.coffee b/app/locale/en.coffee index c27056dab..da56f80d3 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -875,6 +875,10 @@ play_time: "Play time:" completed: "Completed:" invite_students: "Invite students to join this class." + invite_link_header: "Link to join course" + invite_link_p_1: "Give this link to students you would like to have join the course." + invite_link_p_2: "Or have us email them directly:" + capacity_used: "Course slots used:" enter_emails: "Enter student emails to invite, one per line" send_invites: "Send Invites" title: "Title" diff --git a/app/styles/courses/course-details.sass b/app/styles/courses/course-details.sass index 4a61c3453..3cace8e09 100644 --- a/app/styles/courses/course-details.sass +++ b/app/styles/courses/course-details.sass @@ -1,6 +1,6 @@ #course-details-view - .invite-emails + #invite-emails-textarea width: 50% .progress-cell diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index bca464a35..157a61c2e 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -247,18 +247,24 @@ mixin progress-members-popup-started(i, level) mixin invite-tab p(data-i18n="courses.invite_students") - // TODO: finalize text here and then i18n it - h3 Link to join course - p Give this link to students you would like to have join the course. - a= document.location.origin + "/account/prepaid?_ppc=" + view.prepaid.get('code') - p Or have us email them directly: - textarea.invite-emails(rows=3, data-i18n="[placeholder]courses.enter_emails", placeholder="Enter student emails to invite, one per line") - div(style='margin-top:10px;') - button.btn.btn-success.btn-invite(data-i18n="courses.send_invites") + h3(data-i18n="courses.invite_link_header") + p(data-i18n="courses.invite_link_p_1") + .alert.alert-info + strong= document.location.origin + "/account/prepaid?_ppc=" + view.prepaid.get('code') + p(data-i18n="courses.invite_link_p_2") + .form + .form-group + textarea#invite-emails-textarea.form-control( + rows=3, data-i18n="[placeholder]courses.enter_emails", placeholder="Enter student emails to invite, one per line") + .form-group + button#invite-btn.btn.btn-success(data-i18n="courses.send_invites") + #invite-emails-sending-alert.alert.alert-info.hide(data-i18n="common.sending") + #invite-emails-success-alert.alert.alert-success.hide(data-i18n="play_level.done") + h3 Class Capacity if view.prepaid.loaded p - span.spr Course slots used: + span.spr(data-i18n="courses.capacity_used") span #{view.prepaid.get('redeemers').length} / #{view.prepaid.get('maxRedeemers')}. mixin levels-tab diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index 8b522f639..1cb66d71c 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -23,7 +23,7 @@ module.exports = class CourseDetailsView extends RootView 'click .progress-level-cell': 'onClickProgressLevelCell' 'mouseenter .progress-level-cell': 'onMouseEnterPoint' 'mouseleave .progress-level-cell': 'onMouseLeavePoint' - 'click .btn-invite': 'onClickButtonInvite' + 'click #invite-btn': 'onClickInviteButton' constructor: (options, @courseID, @courseInstanceID) -> super options @@ -123,11 +123,7 @@ module.exports = class CourseDetailsView extends RootView @members = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/members", model: User, comparator: 'nameLower' }) @listenToOnce @members, 'sync', @onMembersSync @supermodel.loadCollection @members, 'members', cache: false - if @adminMode - prepaidID = @course.get('prepaidID') - if not prepaidID - prepaidID = '560ef835444e5c9a847e0218' - # TODO: Just abort if no prepaidID + if @adminMode and prepaidID = @courseInstance.get('prepaidID') @prepaid = @supermodel.getModel(Prepaid, prepaidID) or new Prepaid _id: prepaidID @listenTo @prepaid, 'sync', @onPrepaidSync if @prepaid.loaded @@ -244,8 +240,25 @@ module.exports = class CourseDetailsView extends RootView viewArgs: [{}, levelSlug] } - onClickButtonInvite: (e) -> + onClickInviteButton: (e) -> + emails = @$('#invite-emails-textarea').val() + emails = emails.split('\n') + emails = _.filter((_.string.trim(email) for email in emails)) + if not emails.length + return + url = @courseInstance.url() + '/invite_students' + @$('#invite-btn, #invite-emails-textarea').addClass('hide') + @$('#invite-emails-sending-alert').removeClass('hide') + $.ajax({ + url: url + data: {emails: emails} + method: 'POST' + context: @ + success: -> + @$('#invite-emails-sending-alert').addClass('hide') + @$('#invite-emails-success-alert').removeClass('hide') + }) onMouseEnterPoint: (e) -> $('.progress-popup-container').hide() diff --git a/server/courses/course_instance_handler.coffee b/server/courses/course_instance_handler.coffee index 36ca90b35..f31ae10a4 100644 --- a/server/courses/course_instance_handler.coffee +++ b/server/courses/course_instance_handler.coffee @@ -9,6 +9,7 @@ PrepaidHandler = require '../prepaids/prepaid_handler' User = require '../users/User' UserHandler = require '../users/user_handler' utils = require '../../app/core/utils' +sendwithus = require '../sendwithus' CourseInstanceHandler = class CourseInstanceHandler extends Handler modelClass: CourseInstance @@ -31,6 +32,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler return @createAPI(req, res) if relationship is 'create' return @getLevelSessionsAPI(req, res, args[0]) if args[1] is 'level_sessions' return @getMembersAPI(req, res, args[0]) if args[1] is 'members' + return @inviteStudents(req, res, args[0]) if relationship is 'invite_students' super arguments... createAPI: (req, res) -> @@ -101,4 +103,28 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler cleandocs = (UserHandler.formatEntity(req, doc) for doc in users) @sendSuccess(res, cleandocs) + inviteStudents: (req, res, courseInstanceID) -> + if not req.body.emails + return @sendBadInputError(res, 'Emails not included') + CourseInstance.findById courseInstanceID, (err, courseInstance) => + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless courseInstance + return @sendForbiddenError(res) unless @hasAccessToDocument(req, courseInstance) + + Prepaid.findById courseInstance.get('prepaidID'), (err, prepaid) => + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless prepaid + return @sendForbiddenError(res) unless prepaid.get('maxRedeemers') > prepaid.get('redeemers').length + for email in req.body.emails + context = + email_id: sendwithus.templates.course_invite_email + recipient: + address: email + email_data: + class_name: courseInstance.get('name') + join_link: "https://codecombat.com/account/prepaid?_ppc=" + prepaid.get('code') + sendwithus.api.send context, _.noop + return @sendSuccess(res, {}) + + module.exports = new CourseInstanceHandler() diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index 328266b51..817f9542c 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -22,3 +22,5 @@ module.exports.templates = generic_email: 'tem_JhRnQ4pvTS4KdQjYoZdbei' plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y' next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D' + course_invite_email: 'tem_u6D2EFWYC5Ptk38bSykjsU' + diff --git a/test/server/functional/course_instance.spec.coffee b/test/server/functional/course_instance.spec.coffee index e856c83c5..4203a91ba 100644 --- a/test/server/functional/course_instance.spec.coffee +++ b/test/server/functional/course_instance.spec.coffee @@ -196,3 +196,25 @@ describe 'CourseInstance', -> expect(prepaid.get('maxRedeemers')).toEqual(50) expect(prepaid.get('properties')?.courseIDs?.length).toEqual(courses.length) done() + + describe 'Invite to course', -> + it 'takes a list of emails and sends invites', (done) -> + stripe.tokens.create { + card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } + }, (err, token) -> + loginNewUser (user1) -> + createCourse 0, (err, course) -> + expect(err).toBeNull() + return done(err) if err + createCourseInstances user1, course.get('_id'), 1, token.id, (err, courseInstances) -> + expect(err).toBeNull() + return done(err) if err + expect(courseInstances.length).toEqual(1) + inviteStudentsURL = getURL("/db/course_instance/#{courseInstances[0]._id}/invite_students") + requestBody = { + emails: ['test@test.com'] + } + request.post { uri: inviteStudentsURL, json: requestBody }, (err, res) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + done()