From 33b614911c1e4f2a8823e9090511bfb65e843a7e Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Mon, 6 Jun 2016 13:41:49 -0700 Subject: [PATCH 1/5] Support alternate NL spelling in lead import automation --- scripts/updateCloseIoLeads.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/updateCloseIoLeads.js b/scripts/updateCloseIoLeads.js index 6d9cfc029..8d1edce15 100644 --- a/scripts/updateCloseIoLeads.js +++ b/scripts/updateCloseIoLeads.js @@ -107,6 +107,7 @@ function upsertLeads(done) { function getCountryCode(country, emails) { // console.log(`DEBUG: getCountryCode ${country} ${emails.length}`); if (country) { + if (country.indexOf('Nederland') >= 0) return 'NL'; let countryCode = countryList.getCode(country); if (countryCode) return countryCode; } @@ -728,7 +729,7 @@ function createUpdateLeadFn(lead, existingLeads) { if (data.total_results === 0) { if (existingLeads[lead.name.toLowerCase()]) { if (existingLeads[lead.name.toLowerCase()].length === 1) { - console.log(`DEBUG: Using lead from email lookup: ${lead.name}`); + // console.log(`DEBUG: Using lead from email lookup: ${lead.name}`); return updateExistingLead(lead, existingLeads[lead.name.toLowerCase()][0], done); } console.error(`ERROR: ${existingLeads[lead.name.toLowerCase()].length} email leads found for ${lead.name}`); From 891d0fe12f0f9e45acac9a2caa2c1fd13a4f068d Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Mon, 6 Jun 2016 13:55:31 -0700 Subject: [PATCH 2/5] Fix TeacherClassView sometimes not loading This was a race condition where the view would trigger a render before courses loaded, and the template required them when it had some of its other resources. --- app/templates/courses/teacher-class-view.jade | 2 ++ app/views/courses/TeacherClassView.coffee | 24 +++++++++++++++---- .../teachers/TeacherClassView.spec.coffee | 15 ++++++++---- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 3f135c235..4ef3d947a 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -368,6 +368,7 @@ mixin copyCodes mixin bulkAssignControls .bulk-assign-controls.form-inline + - console.log('state errors', state.get('errors')) .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' : '') @@ -378,6 +379,7 @@ mixin bulkAssignControls select.bulk-course-select.form-control each trimCourse in _.rest(view.classroom.get('courses')) - var course = view.courses.get(trimCourse._id) + - console.log('???', course) option(value=course.id selected=(course===state.get('selectedCourse'))) = course.get('name') button.btn.btn-primary-alt.assign-to-selected-students diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index ce4c99c76..6957cc96a 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -71,6 +71,10 @@ module.exports = class TeacherClassView extends RootView @singleStudentLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-level' @allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level' + @debouncedRender = _.debounce -> + console.log 'we debounced', @ + @render() + @state = new State(@getInitialState()) @updateHash @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab) @@ -121,10 +125,13 @@ module.exports = class TeacherClassView extends RootView attachMediatorEvents: () -> @listenTo @state, 'sync change', -> + console.log '...' if _.isEmpty(_.omit(@state.changed, 'searchTerm')) - @renderSelectors('#enrollment-status-table') + console.log 'render selectors...' +# @renderSelectors('#enrollment-status-table') else - @render() + console.log 'render...' +# @render() # Model/Collection events @listenTo @classroom, 'sync change update', -> classCode = @classroom.get('codeCamel') or @classroom.get('code') @@ -137,7 +144,6 @@ module.exports = class TeacherClassView extends RootView @state.set selectedCourse: @courses.first() unless @state.get('selectedCourse') @listenTo @courseInstances, 'sync change update', -> @setCourseMembers() - @render() # TODO: use state @listenTo @courseInstances, 'add-members', -> noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000 @listenTo @students, 'sync change update add remove reset', -> @@ -149,7 +155,6 @@ module.exports = class TeacherClassView extends RootView @state.set students: @students @listenTo @students, 'sort', -> @state.set students: @students - @render() @listenTo @, 'course-select:change', ({ selectedCourse }) -> @state.set selectedCourse: selectedCourse @@ -162,6 +167,17 @@ module.exports = class TeacherClassView extends RootView onLoaded: -> @removeDeletedStudents() # TODO: Move this to mediator listeners? For both classroom and students? @calculateProgressAndLevels() + + # render callback setup + @listenTo @courseInstances, 'sync change update', @debouncedRender + console.log 'attaching' + @listenTo @state, 'sync change', -> + console.log 'we good' + if _.isEmpty(_.omit(@state.changed, 'searchTerm')) + @renderSelectors('#enrollment-status-table') + else + @debouncedRender() + @listenTo @students, 'sort', @debouncedRender super() afterRender: -> diff --git a/test/app/views/teachers/TeacherClassView.spec.coffee b/test/app/views/teachers/TeacherClassView.spec.coffee index 57a5af175..065c128ee 100644 --- a/test/app/views/teachers/TeacherClassView.spec.coffee +++ b/test/app/views/teachers/TeacherClassView.spec.coffee @@ -75,24 +75,29 @@ describe 'TeacherClassView', -> # it "shows the classroom's join code" describe 'the Students tab', -> - beforeEach -> + beforeEach (done) -> @view.state.set('activeTab', '#students-tab') + _.defer(done) # it 'shows all of the students' # it 'sorts correctly by Name' # it 'sorts correctly by Progress' describe 'bulk-assign controls', -> - it 'shows alert when assigning course 2 to unenrolled students', -> + it 'shows alert when assigning course 2 to unenrolled students', (done) -> expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(false) @view.$('.student-row .checkbox-flat').click() @view.$('.assign-to-selected-students').click() - expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(true) + _.defer => + expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(true) + done() - it 'shows alert when assigning but no students are selected', -> + it 'shows alert when assigning but no students are selected', (done) -> expect(@view.$('.no-students-selected').hasClass('visible')).toBe(false) @view.$('.assign-to-selected-students').click() - expect(@view.$('.no-students-selected').hasClass('visible')).toBe(true) + _.defer => + expect(@view.$('.no-students-selected').hasClass('visible')).toBe(true) + done() # describe 'the Course Progress tab', -> # it 'shows the correct Course Overview progress' From 53a7510c46c6c49f17b1c88a87792cdb2c56a5a4 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Mon, 6 Jun 2016 14:30:58 -0700 Subject: [PATCH 3/5] Remove logs --- app/templates/courses/teacher-class-view.jade | 2 -- app/views/courses/TeacherClassView.coffee | 14 +------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 4ef3d947a..3f135c235 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -368,7 +368,6 @@ mixin copyCodes mixin bulkAssignControls .bulk-assign-controls.form-inline - - console.log('state errors', state.get('errors')) .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' : '') @@ -379,7 +378,6 @@ mixin bulkAssignControls select.bulk-course-select.form-control each trimCourse in _.rest(view.classroom.get('courses')) - var course = view.courses.get(trimCourse._id) - - console.log('???', course) option(value=course.id selected=(course===state.get('selectedCourse'))) = course.get('name') button.btn.btn-primary-alt.assign-to-selected-students diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index 6957cc96a..906337f42 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -71,9 +71,7 @@ module.exports = class TeacherClassView extends RootView @singleStudentLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-level' @allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level' - @debouncedRender = _.debounce -> - console.log 'we debounced', @ - @render() + @debouncedRender = _.debounce @render @state = new State(@getInitialState()) @updateHash @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab) @@ -124,14 +122,6 @@ module.exports = class TeacherClassView extends RootView @attachMediatorEvents() attachMediatorEvents: () -> - @listenTo @state, 'sync change', -> - console.log '...' - if _.isEmpty(_.omit(@state.changed, 'searchTerm')) - console.log 'render selectors...' -# @renderSelectors('#enrollment-status-table') - else - console.log 'render...' -# @render() # Model/Collection events @listenTo @classroom, 'sync change update', -> classCode = @classroom.get('codeCamel') or @classroom.get('code') @@ -170,9 +160,7 @@ module.exports = class TeacherClassView extends RootView # render callback setup @listenTo @courseInstances, 'sync change update', @debouncedRender - console.log 'attaching' @listenTo @state, 'sync change', -> - console.log 'we good' if _.isEmpty(_.omit(@state.changed, 'searchTerm')) @renderSelectors('#enrollment-status-table') else From be78f4049c4a0b7e0a3ec29329e59d9d5fd940f2 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Mon, 6 Jun 2016 15:35:10 -0700 Subject: [PATCH 4/5] Fix adding inventory items to hero in level editor Was only happening when HeroPlaceholder had HasPet component. Presumably because the collection was loaded by HasPet, but the thang node for the inventory node needed to populate its autocomplete array. --- app/views/editor/component/ThangComponentConfigView.coffee | 2 +- app/views/editor/level/treema_nodes.coffee | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/editor/component/ThangComponentConfigView.coffee b/app/views/editor/component/ThangComponentConfigView.coffee index 878795081..ba95de63c 100644 --- a/app/views/editor/component/ThangComponentConfigView.coffee +++ b/app/views/editor/component/ThangComponentConfigView.coffee @@ -119,4 +119,4 @@ class SolutionsNode extends TreemaArrayNode succeeds: true }) - @set('/', solutions) \ No newline at end of file + @set('/', solutions) diff --git a/app/views/editor/level/treema_nodes.coffee b/app/views/editor/level/treema_nodes.coffee index 2cc00b7c3..ffa798b58 100644 --- a/app/views/editor/level/treema_nodes.coffee +++ b/app/views/editor/level/treema_nodes.coffee @@ -260,7 +260,10 @@ module.exports.ThangTypeNode = ThangTypeNode = class ThangTypeNode extends Treem thangType?.name or '...' getThangTypes: -> - return if ThangTypeNode.thangTypesCollection + if ThangTypeNode.thangTypesCollection + if not @constructor.thangTypes + @processThangTypes(ThangTypeNode.thangTypesCollection) + return ThangTypeNode.thangTypesCollection = new CocoCollection([], { url: '/db/thang.type' project:['name', 'components', 'original'] From 81d9e192213fe4d3747b63d0157e911ba0264eef Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 6 Jun 2016 13:22:53 -0700 Subject: [PATCH 5/5] Use SendWithUs versioning --- scripts/mail.coffee | 6 ++- server/commons/Handler.coffee | 3 +- .../handlers/course_instance_handler.coffee | 3 +- server/handlers/patch_handler.coffee | 3 +- server/middleware/auth.coffee | 5 ++- server/middleware/classrooms.coffee | 3 +- server/middleware/users.coffee | 3 +- server/middleware/versions.coffee | 5 ++- server/routes/contact.coffee | 5 ++- server/routes/mail.coffee | 6 ++- server/sendwithus.coffee | 44 ++++++++++--------- 11 files changed, 51 insertions(+), 35 deletions(-) diff --git a/scripts/mail.coffee b/scripts/mail.coffee index c73d8ee41..c9ff590c4 100644 --- a/scripts/mail.coffee +++ b/scripts/mail.coffee @@ -91,7 +91,8 @@ emailUserInitialRecruiting = (user, callback) -> team = user.session.levelInfo.team team = team.substr(0, team.length - 1) context = - email_id: sendwithus.templates.recruiting_email + email_id: sendwithus.templates.recruiting_email.id + version_name: sendwithus.templates.recruiting_email.version recipient: address: if DEBUGGING then 'nick@codecombat.com' else user.email name: name @@ -134,7 +135,8 @@ emailUserTournamentResults = (winner, callback) -> name = winner.name team = winner.team.substr(0, winner.team.length - 1) context = - email_id: sendwithus.templates.greed_tournament_rank + email_id: sendwithus.templates.greed_tournament_rank.id + version_name: sendwithus.templates.greed_tournament_rank.version recipient: address: if DEBUGGING then 'nick@codecombat.com' else winner.email name: name diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index aceff9437..c61941074 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -468,7 +468,8 @@ module.exports = class Handler notifyWatcherOfChange: (editor, watcher, changedDocument, editPath) -> context = - email_id: sendwithus.templates.change_made_notify_watcher + email_id: sendwithus.templates.change_made_notify_watcher.id + version_name: sendwithus.templates.change_made_notify_watcher.version recipient: address: watcher.get('email') name: watcher.get('name') diff --git a/server/handlers/course_instance_handler.coffee b/server/handlers/course_instance_handler.coffee index bf26df540..d7353f1ef 100644 --- a/server/handlers/course_instance_handler.coffee +++ b/server/handlers/course_instance_handler.coffee @@ -187,7 +187,8 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler 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 + email_id: sendwithus.templates.course_invite_email.id + version: sendwithus.templates.course_invite_email.version recipient: address: email subject: course.get('name') diff --git a/server/handlers/patch_handler.coffee b/server/handlers/patch_handler.coffee index b88f63a86..f47c88831 100644 --- a/server/handlers/patch_handler.coffee +++ b/server/handlers/patch_handler.coffee @@ -100,7 +100,8 @@ PatchHandler = class PatchHandler extends Handler sendPatchCreatedEmail: (patchCreator, watcher, patch, target, docLink) -> # return if watcher._id is patchCreator._id context = - email_id: sendwithus.templates.patch_created + email_id: sendwithus.templates.patch_created.id + version_name: sendwithus.templates.patch_created.version recipient: address: watcher.get('email') name: watcher.get('name') diff --git a/server/middleware/auth.coffee b/server/middleware/auth.coffee index 9f3c2f756..d60013ad1 100644 --- a/server/middleware/auth.coffee +++ b/server/middleware/auth.coffee @@ -52,7 +52,7 @@ module.exports = yield req.logInAsync(user) if req.query.callback - res.jsonp(req.user.toObject({req, publicOnly: true})) + res.jsonp(req.user.toObject({req, publicOnly: true})) else res.send(req.user.toObject({req, publicOnly: false})) res.end() @@ -132,7 +132,8 @@ module.exports = user.set('passwordReset', utils.getCodeCamel()) yield user.save() context = - email_id: sendwithus.templates.password_reset + email_id: sendwithus.templates.password_reset.id + version_name: sendwithus.templates.password_reset.version recipient: address: req.body.email email_data: diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 1c2ec0f78..783e375e4 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -235,7 +235,8 @@ module.exports = for email in req.body.emails joinCode = (classroom.get('codeCamel') or classroom.get('code')) context = - email_id: sendwithus.templates.course_invite_email + email_id: sendwithus.templates.course_invite_email.id + version_name: sendwithus.templates.course_invite_email.version recipient: address: email email_data: diff --git a/server/middleware/users.coffee b/server/middleware/users.coffee index b334ca8f2..aff914d5b 100644 --- a/server/middleware/users.coffee +++ b/server/middleware/users.coffee @@ -85,7 +85,8 @@ module.exports = if not user throw new errors.NotFound('User not found') context = - email_id: sendwithus.templates.verify_email + email_id: sendwithus.templates.verify_email.id + version_name: sendwithus.templates.verify_email.version recipient: address: user.get('email') name: user.broadName() diff --git a/server/middleware/versions.coffee b/server/middleware/versions.coffee index aafebcb08..21cd9af90 100644 --- a/server/middleware/versions.coffee +++ b/server/middleware/versions.coffee @@ -107,7 +107,8 @@ module.exports = User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) -> for watcher in watchers context = - email_id: sendwithus.templates.change_made_notify_watcher + email_id: sendwithus.templates.change_made_notify_watcher.id + version_name: sendwithus.templates.change_made_notify_watcher.version recipient: address: watcher.get('email') name: watcher.get('name') @@ -127,7 +128,7 @@ module.exports = original = req.params.handle version = req.params.version if not database.isID(original) - throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original) + throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original) query = { 'original': mongoose.Types.ObjectId(original) } if version? diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index 651639bcb..fea0b2ff7 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -17,7 +17,7 @@ module.exports.setup = (app) -> req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if req.body.recipientID is 'schools@codecombat.com' closeIO.sendMail fromAddress, subject, content, (err) -> log.error "Error sending contact form email via Close.io: #{err.message or err}" if err - else + else createSendWithUsContext req, fromAddress, subject, content, (context) -> sendwithus.api.send context, (err, result) -> log.error "Error sending contact form email via sendwithus: #{err.message or err}" if err @@ -61,7 +61,8 @@ createSendWithUsContext = (req, fromAddress, subject, content, done) -> else config.mail.supportPrimary context = - email_id: sendwithus.templates.plain_text_email + email_id: sendwithus.templates.plain_text_email.id + version_name: sendwithus.templates.plain_text_email.version recipient: address: toAddress sender: diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index b888f261e..d823d296c 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -592,7 +592,8 @@ sendLadderUpdateEmail = (session, now, daysAgo) -> sendEmail = (defeatContext, victoryContext, levelVersionsContext) -> # TODO: do something with the preferredLanguage? context = - email_id: sendwithus.templates.ladder_update_email + email_id: sendwithus.templates.ladder_update_email.id + version_name: sendwithus.templates.ladder_update_email.version recipient: address: if DEBUGGING then 'nick@codecombat.com' else user.get('email') name: name @@ -721,7 +722,8 @@ sendNextStepsEmail = (user, now, daysAgo) -> # Used to use these categories to customize the email; not doing it right now. TODO: customize it again in Sendwithus. # TODO: do something with the preferredLanguage? context = - email_id: sendwithus.templates.next_steps_email + email_id: sendwithus.templates.next_steps_email.id + version_name: sendwithus.templates.next_steps_email.version recipient: address: if DEBUGGING then 'nick@codecombat.com' else user.get('email') name: name diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index 23f834438..df6ba88c3 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -11,26 +11,30 @@ module.exports.api = send: (context, cb) -> log.debug('Tried to send email with context: ', JSON.stringify(context, null, ' ')) setTimeout(cb, 10) - + if swuAPIKey module.exports.api = new sendwithusAPI swuAPIKey, debug - + +# Version name can be supplied to tie a specific version to a deploy. +# That is most useful for testing templates with new data fields on staging. +# If it doesn't need to be synchronized to a deploy, you can just "publish" +# the new template version on SendWithUs (and leave this version blank) module.exports.templates = - parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud' - share_progress_email: 'tem_VHE3ihhGmVa3727qds9zY8' - welcome_email_user: 'tem_z7Xvj3mtWYk6ec6aW7RwFk' - welcome_email_student: 'tem_4WYPZNLzs5wawMF9qUJXUH' - verify_email: 'tem_zJee6uRsRmzqzktzneCkCn' - ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4' - patch_created: 'tem_xhxuNosLALsizTNojBjNcL' - change_made_notify_watcher: 'tem_7KVkfmv9SZETb25dtHbUtG' - recruiting_email: 'tem_mdFMgtcczHKYu94Jmq68j8' - greed_tournament_rank: 'tem_c4KYnk2TriEkkZx5NqqGLG' - generic_email: 'tem_JhRnQ4pvTS4KdQjYoZdbei' - plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y' - next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D' - course_invite_email: 'tem_ic2ZhPkpj8GBADFuyAp4bj' - teacher_free_trial: 'tem_R7d9Hpoba9SceQNiYSXBak' - teacher_free_trial_hoc: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc' - teacher_request_demo: 'tem_cwG3HZjEyb6QE493hZuUra' - password_reset: 'tem_wbQUMRtLY9xhec8BSCykLA' + parent_subscribe_email: { id: 'tem_2APERafogvwKhmcnouigud' } + share_progress_email: { id: 'tem_VHE3ihhGmVa3727qds9zY8' } + welcome_email_user: { id: 'tem_z7Xvj3mtWYk6ec6aW7RwFk' } + welcome_email_student: { id: 'tem_4WYPZNLzs5wawMF9qUJXUH' } + verify_email: { id: 'tem_zJee6uRsRmzqzktzneCkCn' } + ladder_update_email: { id: 'JzaZxf39A4cKMxpPZUfWy4' } + patch_created: { id: 'tem_xhxuNosLALsizTNojBjNcL' } + change_made_notify_watcher: { id: 'tem_7KVkfmv9SZETb25dtHbUtG' } + recruiting_email: { id: 'tem_mdFMgtcczHKYu94Jmq68j8' } + greed_tournament_rank: { id: 'tem_c4KYnk2TriEkkZx5NqqGLG' } + generic_email: { id: 'tem_JhRnQ4pvTS4KdQjYoZdbei' } + plain_text_email: { id: 'tem_85UvKDCCNPXsFckERTig6Y' } + next_steps_email: { id: 'tem_RDHhTG5inXQi8pthyqWr5D' } + course_invite_email: { id: 'tem_u6D2EFWYC5Ptk38bSykjsU', version: 'v3' } + teacher_free_trial: { id: 'tem_R7d9Hpoba9SceQNiYSXBak' } + teacher_free_trial_hoc: { id: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc' } + teacher_request_demo: { id: 'tem_cwG3HZjEyb6QE493hZuUra' } + password_reset: { id: 'tem_wbQUMRtLY9xhec8BSCykLA' }