From 7cd49b438b59bb192bb6a21581a5733defdc2449 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 16 Aug 2016 05:27:56 -0700 Subject: [PATCH 1/2] Update ZP -> Close lead importing to only query for last 30 days --- scripts/addZenProspectLeadsToClose.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/addZenProspectLeadsToClose.js b/scripts/addZenProspectLeadsToClose.js index 1ea79e13b..ce44237d6 100644 --- a/scripts/addZenProspectLeadsToClose.js +++ b/scripts/addZenProspectLeadsToClose.js @@ -6,6 +6,10 @@ if (process.argv.length !== 4) { process.exit(); } +// NOTE: last_activity_date_range is the contacted at date, UTC + +// TODO: Only looking at contacts created in last 30 days. How do we catch replies for contacts older than 30 days? + const closeIoApiKey = process.argv[2]; const zpAuthToken = process.argv[3]; @@ -15,6 +19,9 @@ const async = require('async'); const request = require('request'); const zpPageSize = 100; +let zpMinActivityDate = new Date(); +zpMinActivityDate.setUTCDate(zpMinActivityDate.getUTCDate() - 30); +zpMinActivityDate = zpMinActivityDate.toISOString().substring(0, 10); getZPContacts((err, emailContactMap) => { if (err) { @@ -39,7 +46,6 @@ function createCloseLead(zpContact, done) { contacts: [ { name: zpContact.name, - title: zpContact.title, emails: [{email: zpContact.email}] } ], @@ -51,6 +57,9 @@ function createCloseLead(zpContact, done) { if (zpContact.phone) { postData.contacts[0].phones = [{phone: zpContact.phone}]; } + if (zpContact.title) { + postData.contacts[0].title = zpContact.title; + } if (zpContact.district) { postData.custom['demo_nces_district'] = zpContact.district; postData.custom['demo_nces_name'] = zpContact.organization; @@ -189,7 +198,7 @@ function getZPContactsPage(contacts, searchQuery, done) { function createGetZPAutoResponderContactsPage(contacts, page) { return (done) => { // console.log(`DEBUG: Fetching autoresponder page ${page} ${zpPageSize}...`); - let searchQuery = `codecombat_special_auth_token=${zpAuthToken}&page=${page}&per_page=${zpPageSize}&contact_email_autoresponder=true`; + let searchQuery = `codecombat_special_auth_token=${zpAuthToken}&page=${page}&per_page=${zpPageSize}&last_activity_date_range[min]=${zpMinActivityDate}&contact_email_autoresponder=true`; getZPContactsPage(contacts, searchQuery, done); }; } @@ -197,7 +206,7 @@ function createGetZPAutoResponderContactsPage(contacts, page) { function createGetZPRepliedContactsPage(contacts, page) { return (done) => { // console.log(`DEBUG: Fetching email reply page ${page} ${zpPageSize}...`); - let searchQuery = `codecombat_special_auth_token=${zpAuthToken}&page=${page}&per_page=${zpPageSize}&contact_email_replied=true`; + let searchQuery = `codecombat_special_auth_token=${zpAuthToken}&page=${page}&per_page=${zpPageSize}&last_activity_date_range[min]=${zpMinActivityDate}&contact_email_replied=true`; getZPContactsPage(contacts, searchQuery, done); }; } @@ -222,7 +231,7 @@ function getZPContacts(done) { if (err) return done(err); const emailContactMap = {}; for (const contact of contacts) { - if (!contact.organization || !contact.name || !contact.title || !contact.email) { + if (!contact.organization || !contact.name || !contact.email) { console.log(JSON.stringify(contact, null, 2)); return done(`DEBUG: missing data for zp contact:`); } From fd45c9d473b806cc71c00db5409c3facb1be110c Mon Sep 17 00:00:00 2001 From: Phoenix Eliot Date: Mon, 15 Aug 2016 12:43:28 -0700 Subject: [PATCH 2/2] Implement HeroSelectModal for demo flow Add Campaign factory First basic tests for HeroSelectModal in demo flow Implement HeroSelectModal for demo flow Improve tests Disable empty test Fix text inconsistency around 'me' Just listen once Add HeroSelectModal events test Don't reuse destroyed modal Fix inconsistent modal close behavior Fix tests --- app/views/TestView.coffee | 1 + app/views/core/CocoView.coffee | 1 + app/views/core/ModalView.coffee | 6 +- app/views/courses/TeacherCoursesView.coffee | 9 ++- test/app/factories.coffee | 19 ++++++ .../app/views/courses/CoursesView.spec.coffee | 1 + .../views/courses/HeroSelectModal.spec.coffee | 13 ++++ .../courses/TeacherCoursesView.spec.coffee | 63 +++++++++++++++++++ 8 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 test/app/views/courses/TeacherCoursesView.spec.coffee diff --git a/app/views/TestView.coffee b/app/views/TestView.coffee index 4fa08b26a..905c13579 100644 --- a/app/views/TestView.coffee +++ b/app/views/TestView.coffee @@ -97,6 +97,7 @@ module.exports = TestView = class TestView extends RootView jasmine.Ajax.install() beforeEach -> + me.clear() jasmine.Ajax.requests.reset() Backbone.Mediator.init() Backbone.Mediator.setValidationEnabled false diff --git a/app/views/core/CocoView.coffee b/app/views/core/CocoView.coffee index 3450ac721..2bfad5055 100644 --- a/app/views/core/CocoView.coffee +++ b/app/views/core/CocoView.coffee @@ -222,6 +222,7 @@ module.exports = class CocoView extends Backbone.View window.currentModal = modalView @getRootView().stopListeningToShortcuts(true) Backbone.Mediator.publish 'modal:opened', {} + modalView modalClosed: => visibleModal.willDisappear() if visibleModal diff --git a/app/views/core/ModalView.coffee b/app/views/core/ModalView.coffee index 21a5b1f2e..e880abe91 100644 --- a/app/views/core/ModalView.coffee +++ b/app/views/core/ModalView.coffee @@ -50,9 +50,13 @@ module.exports = class ModalView extends CocoView $el = @$el.find('.modal-body') unless $el super($el) + # TODO: Combine hide/onHidden such that backbone 'hide/hidden.bs.modal' events and our 'hide/hidden' events are more 1-to-1 + # For example: + # pressing 'esc' or using `currentModal.hide()` triggers 'hide', 'hide.bs.modal', 'hidden', 'hidden.bs.modal' + # clicking outside the modal triggers 'hide.bs.modal', 'hidden', 'hidden.bs.modal' (but not 'hide') hide: -> @trigger 'hide' - @$el.removeClass('fade').modal 'hide' + @$el.removeClass('fade').modal 'hide' unless @destroyed onHidden: -> @trigger 'hidden' diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index 004485754..5559e0d3e 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -9,6 +9,7 @@ User = require 'models/User' CourseInstance = require 'models/CourseInstance' RootView = require 'views/core/RootView' template = require 'templates/courses/teacher-courses-view' +HeroSelectModal = require 'views/courses/HeroSelectModal' module.exports = class TeacherCoursesView extends RootView id: 'teacher-courses-view' @@ -66,4 +67,10 @@ module.exports = class TeacherCoursesView extends RootView language = form.find('.language-select').val() window.tracker?.trackEvent 'Classes Guides Play Level', category: 'Teachers', courseID: courseID, language: language, levelSlug: levelSlug, ['Mixpanel'] url = "/play/level/#{levelSlug}?course=#{courseID}&codeLanguage=#{language}" - application.router.navigate(url, { trigger: true }) + firstLevelSlug = @campaigns.get(@courses.at(0).get('campaignID')).getLevels().at(0).get('slug') + if levelSlug is firstLevelSlug + @listenToOnce @openModalView(new HeroSelectModal()), + 'hidden': -> + application.router.navigate(url, { trigger: true }) + else + application.router.navigate(url, { trigger: true }) diff --git a/test/app/factories.coffee b/test/app/factories.coffee index 88c5a9dcb..09b66e479 100644 --- a/test/app/factories.coffee +++ b/test/app/factories.coffee @@ -1,6 +1,7 @@ Level = require 'models/Level' Course = require 'models/Course' Courses = require 'collections/Courses' +Campaign = require 'models/Campaign' User = require 'models/User' Classroom = require 'models/Classroom' LevelSession = require 'models/LevelSession' @@ -19,16 +20,34 @@ module.exports = { _id: _id name: _.string.humanize(_id) releasePhase: 'released' + concepts: [] }, attrs) attrs.campaignID ?= sources.campaign?.id or _.uniqueId('campaign_') return new Course(attrs) + makeCampaign: (attrs, sources={}) -> + _id = _.uniqueId('campaign_') + attrs = _.extend({}, { + _id + name: _.string.humanize(_id) + levels: [@makeLevel(), @makeLevel()] + }, attrs) + + if sources.levels + levelsMap = {} + sources.levels.each (level) -> + levelsMap[level.id] = level + attrs.levels = levelsMap + + return new Campaign(attrs) + makeLevel: (attrs) -> _id = _.uniqueId('level_') attrs = _.extend({}, { _id: _id name: _.string.humanize(_id) + slug: _.string.dasherize(_id) original: _id+'_original' version: major: 0 diff --git a/test/app/views/courses/CoursesView.spec.coffee b/test/app/views/courses/CoursesView.spec.coffee index 195111232..6489f9883 100644 --- a/test/app/views/courses/CoursesView.spec.coffee +++ b/test/app/views/courses/CoursesView.spec.coffee @@ -13,6 +13,7 @@ describe 'CoursesView', -> describe 'Change Hero button', -> beforeEach (done) -> + me.set(factories.makeUser({ role: 'student' }).attributes) view = new CoursesView() classrooms = new Classrooms([factories.makeClassroom()]) courseInstances = new CourseInstances([factories.makeCourseInstance()]) diff --git a/test/app/views/courses/HeroSelectModal.spec.coffee b/test/app/views/courses/HeroSelectModal.spec.coffee index 1c4bf52ed..d7908ed97 100644 --- a/test/app/views/courses/HeroSelectModal.spec.coffee +++ b/test/app/views/courses/HeroSelectModal.spec.coffee @@ -35,3 +35,16 @@ describe 'HeroSelectModal', -> expect(request.method).toBe("PUT") expect(JSON.parse(request.params).heroConfig?.thangType).toBe(hero2.get('original')) done() + + it 'triggers its events properly', (done) -> + spyOn(modal, 'trigger') + modal.render() + modal.$('.hero-option:nth-child(2)').click() + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ status: 200, responseText: me.attributes }) + expect(modal.trigger).toHaveBeenCalled() + expect(modal.trigger.calls.argsFor(0)[0]).toBe('hero-select:success') + expect(modal.trigger).not.toHaveBeenCalledWith('hide') + modal.$('.select-hero-btn').click() + expect(modal.trigger).toHaveBeenCalledWith('hide') + done() diff --git a/test/app/views/courses/TeacherCoursesView.spec.coffee b/test/app/views/courses/TeacherCoursesView.spec.coffee new file mode 100644 index 000000000..989aac1d7 --- /dev/null +++ b/test/app/views/courses/TeacherCoursesView.spec.coffee @@ -0,0 +1,63 @@ +TeacherCoursesView = require 'views/courses/TeacherCoursesView' +HeroSelectModal = require 'views/courses/HeroSelectModal' +Classrooms = require 'collections/Classrooms' +Courses = require 'collections/Courses' +Campaigns = require 'collections/Campaigns' +Levels = require 'collections/Levels' +auth = require 'core/auth' +factories = require 'test/app/factories' + +describe 'TeacherCoursesView', -> + + modal = null + view = null + + describe 'Play Level form', -> + beforeEach (done) -> + me.set(factories.makeUser({ role: 'teacher' }).attributes) + view = new TeacherCoursesView() + classrooms = new Classrooms([factories.makeClassroom()]) + levels1 = new Levels([ factories.makeLevel({ name: 'Dungeons of Kithgard' }), factories.makeLevel(), factories.makeLevel() ]) + levels2 = new Levels([ factories.makeLevel(), factories.makeLevel(), factories.makeLevel() ]) + campaigns = new Campaigns([factories.makeCampaign({}, { levels: levels1 }), factories.makeCampaign({}, { levels: levels2 })]) + courses = new Courses([factories.makeCourse({}, {campaign: campaigns.at(0)}), factories.makeCourse({}, {campaign: campaigns.at(1)})]) + view.ownedClassrooms.fakeRequests[0].respondWith({ status: 200, responseText: classrooms.stringify() }) + view.campaigns.fakeRequests[0].respondWith({ status: 200, responseText: campaigns.stringify() }) + view.courses.fakeRequests[0].respondWith({ status: 200, responseText: courses.stringify() }) + view.render() + done() + + it 'opens HeroSelectModal for the first level of the first course', (done) -> + spyOn(view, 'openModalView').and.callFake (modal) -> modal + spyOn(application.router, 'navigate') + view.$('.play-level-button').first().click() + expect(view.openModalView).toHaveBeenCalled() + expect(application.router.navigate).not.toHaveBeenCalled() + args = view.openModalView.calls.argsFor(0) + modalView = args[0] + expect(modalView instanceof HeroSelectModal).toBe(true) + modalView.trigger('hero-select:success') + expect(application.router.navigate).not.toHaveBeenCalled() + modalView.trigger('hide') + modalView.trigger('hidden') + _.defer -> + expect(application.router.navigate).toHaveBeenCalled() + done() + + it "doesn't open HeroSelectModal for other levels", -> + spyOn(view, 'openModalView') + spyOn(application.router, 'navigate') + secondLevelSlug = view.$('.level-select:first option:nth-child(2)').val() + view.$('.level-select').first().val(secondLevelSlug) + view.$('.play-level-button').first().click() + expect(view.openModalView).not.toHaveBeenCalled() + expect(application.router.navigate).toHaveBeenCalled() + + it "doesn't open HeroSelectModal for other courses", -> + spyOn(view, 'openModalView') + spyOn(application.router, 'navigate') + view.$('.play-level-button').get(1).click() + expect(view.openModalView).not.toHaveBeenCalled() + expect(application.router.navigate).toHaveBeenCalled() + + it "remembers the selected hero" # TODO