diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index d3fa598617..9fe53b0115 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -11,7 +11,8 @@ module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-bunyan' grunt.loadNpmTasks 'grunt-sed' grunt.loadNpmTasks 'grunt-git-rev-parse' - + grunt.loadNpmTasks 'grunt-file-append' + config = execute: app: @@ -129,6 +130,14 @@ module.exports = (grunt) -> options: prop: 'commit' + + file_append: + default_options: files: [ { + append: '\n//ide.js is complete - used for automated testing' + input: 'public/minjs/ide.js' + output: 'public/minjs/ide.js' + }] + sed: version: path: "app/views/sentry.jade" @@ -272,7 +281,7 @@ module.exports = (grunt) -> grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server'] grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes'] grunt.registerTask 'compile:css', 'Compile the less files to css', ['less'] - grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs'] + grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append"] grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests'] grunt.registerTask 'compile:smoke_tests', 'Compile the smoke tests', ['coffee:smoke_tests'] grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests'] diff --git a/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee b/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee index 43349b9b1e..0127e5681e 100644 --- a/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee +++ b/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee @@ -32,46 +32,35 @@ module.exports = ReferalAllocator = assignBonus: (user_id, callback = (error) ->) -> - SubscriptionLocator.getUsersSubscription user_id, (error, subscription) -> - return callback(error) if error? - logger.log - subscription: subscription, - user_id: user_id, - "checking user doesn't have a subsciption before assigning bonus" - if !subscription? or !subscription.planCode? - query = _id: user_id - User.findOne query, (error, user) -> - return callback(error) if error - return callback(new Error("user not found")) if !user? - logger.log - user_id: user_id, - refered_user_count: user.refered_user_count, - "assigning bonus" - if user.refered_user_count? and user.refered_user_count > 0 - newFeatures = ReferalAllocator._calculateBonuses(user) - User.update query, { $set: features: newFeatures }, callback - - else - callback() + query = _id: user_id + User.findOne query, (error, user) -> + return callback(error) if error + return callback(new Error("user not found")) if !user? + logger.log user_id: user_id, refered_user_count: user.refered_user_count, "assigning bonus" + if user.refered_user_count? and user.refered_user_count > 0 + newFeatures = ReferalAllocator._calculateFeatures(user) + if _.isEqual newFeatures, user.features + return callback() + User.update query, { $set: features: newFeatures }, callback else callback() - - _calculateBonuses : (user)-> - bonusLevel = ReferalAllocator._getBonusLevel(user) - newFeatures = {} + _calculateFeatures : (user)-> + bonusLevel = ReferalAllocator._getBonusLevel(user) + currentFeatures = _.clone(user.features) #need to clone because we exend with underscore later + betterBonusFeatures = {} _.each Settings.bonus_features["#{bonusLevel}"], (bonusLevel, key)-> currentLevel = user?.features?[key] if _.isBoolean(currentLevel) and currentLevel == false - newFeatures[key] = bonusLevel + betterBonusFeatures[key] = bonusLevel - if _.isNumber(currentLevel) + if _.isNumber(currentLevel) if currentLevel == -1 return bonusIsGreaterThanCurrent = currentLevel < bonusLevel if bonusIsGreaterThanCurrent or bonusLevel == -1 - newFeatures[key] = bonusLevel - + betterBonusFeatures[key] = bonusLevel + newFeatures = _.extend(currentFeatures, betterBonusFeatures) return newFeatures diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 690325374a..589c33f241 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -6,6 +6,7 @@ html(itemscope, itemtype='http://schema.org/Product') script(type="text/javascript"). // Stop superfish from loading window.similarproducts = true + style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; } -if (typeof(gaExperiments) != "undefined") |!{gaExperiments} diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index 492c3795e8..2bf5fb9793 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -13,7 +13,9 @@ block content h3 #{translate("loading")}... .progress .progress-bar(style="width: 20%", ng-style="{'width': state.load_progress + '%'}") - p.text-center.text-danger(ng-if="state.error").ng-cloak {{ state.error }} + p.text-center.text-danger(ng-if="state.error").ng-cloak + span(ng-bind-html="state.error") + .global-alerts(ng-cloak) .alert.alert-danger.small(ng-if="connection.forced_disconnect") @@ -28,8 +30,11 @@ block content .alert.alert-warning.small(ng-if="connection.reconnecting") strong #{translate("reconnecting")}... + .alert.alert-warning.small(ng-if="connection.inactive_disconnect") + strong #{translate("editor_disconected_click_to_reconnect")} + .div(ng-controller="SavingNotificationController") - .alert.alert-warning.small( ng-repeat="(doc_id, state) in docSavingStatus" ng-if="state.unsavedSeconds > 8") #{translate("saving_notification_with_seconds", {docname:"{{ state.doc.name }}", seconds:"{{ state.unsavedSeconds }}"})} + .alert.alert-warning.small(ng-repeat="(doc_id, state) in docSavingStatus" ng-if="state.unsavedSeconds > 8") #{translate("saving_notification_with_seconds", {docname:"{{ state.doc.name }}", seconds:"{{ state.unsavedSeconds }}"})} include ./editor/left-menu diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 5c0ac374f1..5923c9b888 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -100,7 +100,7 @@ div.full-size.pdf(ng-controller="PdfController") p a.btn.btn-info( href - ng-click="hello('compile-timeout')" + ng-click="startFreeTrial('compile-timeout')" ) #{translate("start_free_trial")} .pdf-errors(ng-show="pdf.projectTooLarge") diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 2b367871f0..e6b7d30da2 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -284,7 +284,7 @@ module.exports = title: "ShareLaTeX Community Edition" left_footer: [{ - text: "Powered by ShareLaTeX © 2014" + text: "Powered by ShareLaTeX © 2015" }] right_footer: [{ diff --git a/services/web/package.json b/services/web/package.json index d1c139500f..5e2544cf7b 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -48,29 +48,30 @@ "session.socket.io": "0.1.4", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "socket.io": "0.9.16", - "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#v0.2.0", + "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master", "underscore": "1.6.0", "underscore.string": "^3.0.2", "v8-profiler": "^5.2.3", "xml2js": "0.2.0" }, "devDependencies": { + "bunyan": "0.22.1", "chai": "", "chai-spies": "", - "sandboxed-module": "0.2.0", - "timekeeper": "", - "sinon": "", + "grunt-available-tasks": "0.4.1", + "grunt-bunyan": "0.5.0", "grunt-concurrent": "0.4.3", "grunt-contrib-clean": "0.5.0", "grunt-contrib-coffee": "0.10.0", "grunt-contrib-less": "0.9.0", - "grunt-mocha-test": "0.9.0", - "grunt-available-tasks": "0.4.1", "grunt-contrib-requirejs": "0.4.1", "grunt-execute": "0.1.5", + "grunt-file-append": "0.0.6", "grunt-git-rev-parse": "^0.1.4", + "grunt-mocha-test": "0.9.0", "grunt-sed": "^0.1.1", - "bunyan": "0.22.1", - "grunt-bunyan": "0.5.0" + "sandboxed-module": "0.2.0", + "sinon": "", + "timekeeper": "" } } diff --git a/services/web/public/coffee/ide/connection/ConnectionManager.coffee b/services/web/public/coffee/ide/connection/ConnectionManager.coffee index e6c9597f68..b33566d208 100644 --- a/services/web/public/coffee/ide/connection/ConnectionManager.coffee +++ b/services/web/public/coffee/ide/connection/ConnectionManager.coffee @@ -1,5 +1,12 @@ define [], () -> + ONEHOUR = 1000 * 60 * 60 class ConnectionManager + + + disconnectAfterMs: ONEHOUR * 24 + + lastUserAction : new Date() + constructor: (@ide, @$scope) -> if !io? console.error "Socket.io javascript not loaded. Please check that the real-time service is running and accessible." @@ -8,18 +15,29 @@ define [], () -> $scope.$apply () => @$scope.state.error = "Could not connect to websocket server :(" return - - @connected = false + setInterval(() => + @disconnectIfInactive() + , ONEHOUR) + + @userIsLeavingPage = false + window.addEventListener 'beforeunload', => + @userIsLeavingPage = true + + @connected = false + @userIsInactive = false + @$scope.connection = reconnecting: false # If we need to force everyone to reload the editor forced_disconnect: false + inactive_disconnect: false @$scope.tryReconnectNow = () => @tryReconnect() @$scope.$on 'cursor:editor:update', () => + @lastUserAction = new Date() if !@connected @tryReconnect() @@ -29,6 +47,7 @@ define [], () -> @ide.socket = io.connect null, reconnect: false + 'connect timeout': 30 * 1000 "force new connection": true @ide.socket.on "connect", () => @@ -37,6 +56,7 @@ define [], () -> @$scope.$apply () => @$scope.connection.reconnecting = false + @$scope.connection.inactive_disconnect = false if @$scope.state.loading @$scope.state.load_progress = 70 @@ -44,6 +64,13 @@ define [], () -> @joinProject() , 100) + @ide.socket.on "connect_failed", () => + @connected = false + $scope.$apply () => + @$scope.state.error = "Unable to connect, please view the connection problems guide to fix the issue." + + + @ide.socket.on 'disconnect', () => @connected = false @ide.pushEvent("disconnected") @@ -55,7 +82,7 @@ define [], () -> ga('send', 'event', 'editor-interaction', 'disconnect') , 2000) - if !$scope.connection.forced_disconnect + if !$scope.connection.forced_disconnect and !@userIsInactive @startAutoReconnectCountdown() @ide.socket.on 'forceDisconnect', (message) => @@ -102,6 +129,9 @@ define [], () -> else countdown = 3 + Math.floor(Math.random() * 7) + if @userIsLeavingPage #user will have pressed refresh or back etc + return + @$scope.$apply () => @$scope.connection.reconnecting = false @$scope.connection.reconnection_countdown = countdown @@ -133,3 +163,10 @@ define [], () -> @ide.socket.socket.reconnect() setTimeout (=> @startAutoReconnectCountdown() if !@connected), 2000 + disconnectIfInactive: ()-> + @userIsInactive = (new Date() - @lastUserAction) > @disconnectAfterMs + if @userIsInactive and @connected + @disconnect() + @$scope.$apply () => + @$scope.connection.inactive_disconnect = true + diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 1bd92dc111..4a98b949fc 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -196,6 +196,11 @@ define [ else $scope.switchToSideBySideLayout() + $scope.startFreeTrial = (source) -> + ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source) + window.open("/user/subscription/new?planCode=student_free_trial_7_days") + $scope.startedFreeTrial = true + App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) -> synctex = syncToPdf: (cursorPosition) -> diff --git a/services/web/public/stylesheets/app/base.less b/services/web/public/stylesheets/app/base.less index 3ed79956cf..a78e1a7cfa 100644 --- a/services/web/public/stylesheets/app/base.less +++ b/services/web/public/stylesheets/app/base.less @@ -1,7 +1,3 @@ -[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { - display: none !important; -} - .system-message { padding: (@line-height-computed / 4) (@line-height-computed / 2); background-color: @state-warning-bg; diff --git a/services/web/test/UnitTests/coffee/Referal/ReferalAllocatorTests.coffee b/services/web/test/UnitTests/coffee/Referal/ReferalAllocatorTests.coffee index 28592f70bf..3fe925f91c 100644 --- a/services/web/test/UnitTests/coffee/Referal/ReferalAllocatorTests.coffee +++ b/services/web/test/UnitTests/coffee/Referal/ReferalAllocatorTests.coffee @@ -4,7 +4,7 @@ require('chai').should() sinon = require('sinon') modulePath = require('path').join __dirname, '../../../../app/js/Features/Referal/ReferalAllocator.js' -describe 'Referal allocator', -> +describe 'Referalallocator', -> beforeEach -> @ReferalAllocator = SandboxedModule.require modulePath, requires: @@ -75,7 +75,6 @@ describe 'Referal allocator', -> @callback.called.should.equal true describe "assignBonus", -> - describe "when user does not have a subscription", -> beforeEach -> @refered_user_count = 3 @Settings.bonus_features = @@ -88,16 +87,10 @@ describe 'Referal allocator', -> features:{collaborators:1, dropbox:false, versioning:false} } - @User.findOne = sinon.stub().callsArgWith 1, null,stubbedUser + @User.findOne = sinon.stub().callsArgWith 1, null, stubbedUser @User.update = sinon.stub().callsArgWith 2, null - @SubscriptionLocator.getUsersSubscription = sinon.stub().callsArgWith 1, null, null @ReferalAllocator.assignBonus @user_id, @callback - it "should get the users subscription", -> - @SubscriptionLocator.getUsersSubscription - .calledWith(@user_id) - .should.equal true - it "should get the users number of refered user", -> @User.findOne .calledWith(_id: @user_id) @@ -116,52 +109,26 @@ describe 'Referal allocator', -> it "should call the callback", -> @callback.called.should.equal true - - describe "when user does not have a recurlySubscription_id", -> + + describe "when there is nothing to assign", -> + beforeEach -> - @refered_user_count = 4 + @ReferalAllocator._calculateBonuses = sinon.stub().returns({}) + @stubbedUser = + refered_user_count:4 + features:{collaborators:3, versioning:true, dropbox:false} @Settings.bonus_features = - "2": - collaborators: 2 - dropbox: false - versioning: false - "5": - collaborators: 5 - dropbox: true - versioning: false - "3": - collaborators: 3 - dropbox: false - versioning: false - stubbedUser = { refered_user_count: @refered_user_count, features:{collaborators:1, dropbox:false, versioning:false} } - @User.findOne = sinon.stub().callsArgWith 1, null, stubbedUser + "4": + collaborators:3 + versioning:true + dropbox:false + @User.findOne = sinon.stub().callsArgWith 1, null, @stubbedUser @User.update = sinon.stub().callsArgWith 2, null - @SubscriptionLocator.getUsersSubscription = sinon.stub().callsArgWith 1, null, {} - @ReferalAllocator.assignBonus @user_id, @callback - it "should get the users subscription", -> - @SubscriptionLocator.getUsersSubscription - .calledWith(@user_id) - .should.equal true - - it "should get the users number of refered user", -> - @User.findOne - .calledWith(_id: @user_id) - .should.equal true - - it "should update the user to bonus features with the closest level", -> - @User.update - .calledWith({ - _id: @user_id - }, { - $set: - features: - @Settings.bonus_features["3"] - }) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true + it "should not call update if there are no bonuses to apply", (done)-> + @ReferalAllocator.assignBonus @user_id, (err)=> + @User.update.called.should.equal false + done() describe "when the user has better features already", -> @@ -181,7 +148,6 @@ describe 'Referal allocator', -> @User.findOne = sinon.stub().callsArgWith 1, null, @stubbedUser @User.update = sinon.stub().callsArgWith 2, null - @SubscriptionLocator.getUsersSubscription = sinon.stub().callsArgWith 1, null,null it "should not set in in mongo when the feature is better", (done)-> @ReferalAllocator.assignBonus @user_id, => @@ -191,7 +157,7 @@ describe 'Referal allocator', -> it "should not overright if the user has -1 users", (done)-> @stubbedUser.features.collaborators = -1 @ReferalAllocator.assignBonus @user_id, => - @User.update.calledWith({_id: @user_id }, {$set: features:{dropbox:true, versioning:false} }).should.equal true + @User.update.calledWith({_id: @user_id }, {$set: features:{dropbox:true, versioning:false, collaborators:-1} }).should.equal true done() describe "when the user is not at a bonus level", -> @@ -204,14 +170,8 @@ describe 'Referal allocator', -> versioning: false @User.findOne = sinon.stub().callsArgWith 1, null, { refered_user_count: @refered_user_count } @User.update = sinon.stub().callsArgWith 2, null - @SubscriptionLocator.getUsersSubscription = sinon.stub().callsArgWith 1, null, {} @ReferalAllocator.assignBonus @user_id, @callback - it "should get the users subscription", -> - @SubscriptionLocator.getUsersSubscription - .calledWith(@user_id) - .should.equal true - it "should get the users number of refered user", -> @User.findOne .calledWith(_id: @user_id) @@ -223,29 +183,4 @@ describe 'Referal allocator', -> it "should call the callback", -> @callback.called.should.equal true - describe "when user has a subscription", -> - beforeEach -> - @refered_user_count = 3 - @Settings.bonus_features = - "3": - collaborators: 3 - dropbox: false - versioning: false - @User.findOne = sinon.stub().callsArgWith 1, null, { refered_user_count: @refered_user_count } - @User.update = sinon.stub().callsArgWith 2, null - @SubscriptionLocator.getUsersSubscription = sinon.stub().callsArgWith 1, null, { planCode: "collaborator" } - @ReferalAllocator.assignBonus @user_id, @callback - it "should get the users subscription", -> - @SubscriptionLocator.getUsersSubscription - .calledWith(@user_id) - .should.equal true - - it "should not get the users number of refered user", -> - @User.findOne.called.should.equal false - - it "should not update the user to bonus features", -> - @User.update.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true