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/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:`); } 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