From 90986c1ade70bf061d833c3ecf08b82129a0ce35 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 27 Jun 2018 17:26:39 -0500 Subject: [PATCH 001/188] Express local for MathJax path --- services/web/app/coffee/infrastructure/ExpressLocals.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 6943a79cb2..d2e7415067 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -167,6 +167,11 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> path = Path.join("/img/", imgFile) return Url.resolve(staticFilesBase, path) + res.locals.mathJaxPath = res.locals.buildJsPath( + 'libs/mathjax/MathJax.js', + {cdn:false, qs:{config:'TeX-AMS_HTML'}} + ) + next() From a34dcef8b7ce0c0ec213b8dd5033a437b1273fc4 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 27 Jun 2018 17:27:27 -0500 Subject: [PATCH 002/188] Use express local for MathJax path --- services/web/app/views/project/editor.pug | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 602b9af86b..190ba57cf6 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -103,6 +103,8 @@ block content h3 {{ title }} .modal-body(ng-bind-html="message") + script(src=mathJaxPath) + block requirejs script(type="text/javascript" src='/socket.io/socket.io.js') @@ -133,7 +135,6 @@ block requirejs window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.requirejs = { "paths" : { - "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, qs:{config:'TeX-AMS_HTML'}})}", "moment": "libs/#{lib('moment')}", "pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf", "pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}", From 3e8168d32f723ec103d2dd2f2a9949ce04106af7 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 27 Jun 2018 17:27:47 -0500 Subject: [PATCH 003/188] Remove MathJax dependency injection --- services/web/public/coffee/ide/chat/directives/mathjax.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/public/coffee/ide/chat/directives/mathjax.coffee b/services/web/public/coffee/ide/chat/directives/mathjax.coffee index 08e167ee66..2fa449f55f 100644 --- a/services/web/public/coffee/ide/chat/directives/mathjax.coffee +++ b/services/web/public/coffee/ide/chat/directives/mathjax.coffee @@ -1,6 +1,5 @@ define [ "base" - "mathjax" ], (App) -> mathjaxConfig = messageStyle: "none" From a73934814619e3ede9bd88ac6d69498019c36147 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 27 Jun 2018 17:46:35 -0500 Subject: [PATCH 004/188] Move MathJax directive to a shared directory MathJax parsing now on learn pages, and soon to be on the blog, so move out of the `ide/chat` directive. --- .../web/public/coffee/{ide/chat => }/directives/mathjax.coffee | 0 services/web/public/coffee/ide/chat/index.coffee | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename services/web/public/coffee/{ide/chat => }/directives/mathjax.coffee (100%) diff --git a/services/web/public/coffee/ide/chat/directives/mathjax.coffee b/services/web/public/coffee/directives/mathjax.coffee similarity index 100% rename from services/web/public/coffee/ide/chat/directives/mathjax.coffee rename to services/web/public/coffee/directives/mathjax.coffee diff --git a/services/web/public/coffee/ide/chat/index.coffee b/services/web/public/coffee/ide/chat/index.coffee index de9c46d62d..f2f146c745 100644 --- a/services/web/public/coffee/ide/chat/index.coffee +++ b/services/web/public/coffee/ide/chat/index.coffee @@ -2,6 +2,6 @@ define [ "ide/chat/controllers/ChatButtonController" "ide/chat/controllers/ChatController" "ide/chat/controllers/ChatMessageController" - "ide/chat/directives/mathjax" + "directives/mathjax" "filters/wrapLongWords" ], () -> \ No newline at end of file From 49cf2a8dee64d98d52d5820d06703685d028a0fd Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 27 Jun 2018 17:57:07 -0500 Subject: [PATCH 005/188] Use the MathJax `Safe.js` extension to prevent XSS --- services/web/public/coffee/directives/mathjax.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/coffee/directives/mathjax.coffee b/services/web/public/coffee/directives/mathjax.coffee index 2fa449f55f..6d37353c07 100644 --- a/services/web/public/coffee/directives/mathjax.coffee +++ b/services/web/public/coffee/directives/mathjax.coffee @@ -2,6 +2,7 @@ define [ "base" ], (App) -> mathjaxConfig = + extensions: ["Safe.js"] messageStyle: "none" imageFont:null "HTML-CSS": { availableFonts: ["TeX"] }, From cb449f4b75fcaa66ddffa918c359c193a71e50aa Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jul 2018 10:31:25 +0100 Subject: [PATCH 006/188] Delete a subscription on expiry, rather than set personal plan code --- .../Subscription/LimitationsManager.coffee | 2 +- .../Subscription/SubscriptionUpdater.coffee | 18 +++++++++--------- .../SubscriptionUpdaterTests.coffee | 8 +++----- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index a23372903b..5f5de9f549 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -19,7 +19,7 @@ module.exports = LimitationsManager = if user.features? and user.features.collaborators? callback null, user.features.collaborators else - callback null, Settings.defaultPlanCode.collaborators + callback null, Settings.defaultFeatures.collaborators canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) -> @allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) => diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 7867db29b9..b5fce978a4 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -78,16 +78,16 @@ module.exports = SubscriptionUpdater = _updateSubscriptionFromRecurly: (recurlySubscription, subscription, callback)-> logger.log recurlySubscription:recurlySubscription, subscription:subscription, "updaing subscription" - plan = PlansLocator.findLocalPlanInSettings(recurlySubscription.plan.plan_code) if recurlySubscription.state == "expired" - subscription.recurlySubscription_id = undefined - subscription.planCode = Settings.defaultPlanCode - else - subscription.recurlySubscription_id = recurlySubscription.uuid - subscription.freeTrial.expiresAt = undefined - subscription.freeTrial.planCode = undefined - subscription.freeTrial.allowed = true - subscription.planCode = recurlySubscription.plan.plan_code + return SubscriptionUpdater.deleteSubscription subscription._id, callback + subscription.recurlySubscription_id = recurlySubscription.uuid + subscription.freeTrial.expiresAt = undefined + subscription.freeTrial.planCode = undefined + subscription.freeTrial.allowed = true + subscription.planCode = recurlySubscription.plan.plan_code + plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) + if !plan? + return callback(new Error("plan code not found: #{subscription.planCode}")) if plan.groupPlan subscription.groupPlan = true subscription.membersLimit = plan.membersLimit diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee index f97ba98ad4..376d526db0 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee @@ -103,6 +103,7 @@ describe "SubscriptionUpdater", -> describe "_updateSubscriptionFromRecurly", -> beforeEach -> @FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) + @SubscriptionUpdater.deleteSubscription = sinon.stub().yields() it "should update the subscription with token etc when not expired", (done)-> @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> @@ -116,13 +117,10 @@ describe "SubscriptionUpdater", -> @FeaturesUpdater.refreshFeatures.calledWith(@adminUser._id).should.equal true done() - it "should remove the recurlySubscription_id when expired", (done)-> + it "should remove the subscription when expired", (done)-> @recurlySubscription.state = "expired" - @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> - assert.equal(@subscription.recurlySubscription_id, undefined) - @subscription.save.called.should.equal true - @FeaturesUpdater.refreshFeatures.calledWith(@adminUser._id).should.equal true + @SubscriptionUpdater.deleteSubscription.calledWith(@subscription._id).should.equal true done() it "should update all the users features", (done)-> From 91079f3af2dba280ab655e5b8b74680e370992b6 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jul 2018 11:32:19 +0100 Subject: [PATCH 007/188] Fix unit tests --- .../coffee/Subscription/LimitationsManagerTests.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee index 4b47250d4a..87a8ac8afc 100644 --- a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee @@ -37,14 +37,14 @@ describe "LimitationsManager", -> describe "allowedNumberOfCollaboratorsInProject", -> describe "when the project is owned by a user without a subscription", -> beforeEach -> - @Settings.defaultPlanCode = collaborators: 23 + @Settings.defaultFeatures = collaborators: 23 @project.owner_ref = @user_id delete @user.features @callback = sinon.stub() @LimitationsManager.allowedNumberOfCollaboratorsInProject(@project_id, @callback) it "should return the default number", -> - @callback.calledWith(null, @Settings.defaultPlanCode.collaborators).should.equal true + @callback.calledWith(null, @Settings.defaultFeatures.collaborators).should.equal true describe "when the project is owned by a user with a subscription", -> beforeEach -> @@ -60,13 +60,13 @@ describe "LimitationsManager", -> describe "allowedNumberOfCollaboratorsForUser", -> describe "when the user has no features", -> beforeEach -> - @Settings.defaultPlanCode = collaborators: 23 + @Settings.defaultFeatures = collaborators: 23 delete @user.features @callback = sinon.stub() @LimitationsManager.allowedNumberOfCollaboratorsForUser(@user_id, @callback) it "should return the default number", -> - @callback.calledWith(null, @Settings.defaultPlanCode.collaborators).should.equal true + @callback.calledWith(null, @Settings.defaultFeatures.collaborators).should.equal true describe "when the user has features", -> beforeEach -> From 2dc509f43d4de23b2a34820e75154a888abb4d3b Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 10 Jul 2018 12:25:13 +0100 Subject: [PATCH 008/188] Fix memory leak setting up spell check Switching docs would set up event listeners on Ace without tearing down any old listeners, which could lead to a memory leak. This tears down old listeners when detaching from the "old" doc --- .../web/public/coffee/ide/editor/directives/aceEditor.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 617b41b845..ed35555196 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -468,6 +468,7 @@ define [ editor.focus() detachFromAce = (sharejs_doc) -> + tearDownSpellCheck() sharejs_doc.detachFromAce() sharejs_doc.off "remoteop.recordRemote" From e5ad2a8724e59c7ca633226fa4035343fa83fe67 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 3 Jul 2018 12:14:51 +0100 Subject: [PATCH 009/188] Abstract Ace-specific code to adapter --- .../ide/editor/directives/aceEditor.coffee | 22 +++++- .../CursorPositionAdapter.coffee | 35 ++++++++ .../CursorPositionManager.coffee | 79 ++++++++----------- 3 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 617b41b845..ef6db538c4 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -10,13 +10,14 @@ define [ "ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter" "ide/editor/directives/aceEditor/highlights/HighlightsManager" "ide/editor/directives/aceEditor/cursor-position/CursorPositionManager" + "ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter" "ide/editor/directives/aceEditor/track-changes/TrackChangesManager" "ide/editor/directives/aceEditor/metadata/MetadataManager" "ide/metadata/services/metadata" "ide/graphics/services/graphics" "ide/preamble/services/preamble" "ide/files/services/files" -], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, SpellCheckAdapter, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) -> +], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, SpellCheckAdapter, HighlightsManager, CursorPositionManager, CursorPositionAdapter, TrackChangesManager, MetadataManager) -> EditSession = ace.require('ace/edit_session').EditSession ModeList = ace.require('ace/ext/modelist') Vim = ace.require('ace/keyboard/vim').Vim @@ -108,7 +109,7 @@ define [ undoManager = new UndoManager(scope, editor, element) highlightsManager = new HighlightsManager(scope, editor, element) - cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage) + cursorPositionManager = new CursorPositionManager(scope, new CursorPositionAdapter(editor), localStorage) trackChangesManager = new TrackChangesManager(scope, editor, element) metadataManager = new MetadataManager(scope, editor, element, metadata) autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files) @@ -380,6 +381,21 @@ define [ editor.off 'changeSession', onSessionChangeForSpellCheck editor.off 'nativecontextmenu', spellCheckManager.onContextMenu + onSessionChangeForCursorPosition = (e) -> + cursorPositionManager.onSessionChange(e) + e.oldSession?.selection.off 'changeCursor', cursorPositionManager.onCursorChange + e.session.selection.on 'changeCursor', cursorPositionManager.onCursorChange + + initCursorPosition = () -> + cursorPositionManager.init() + editor.on 'changeSession', onSessionChangeForCursorPosition + onSessionChangeForCursorPosition({ session: editor.getSession() }) # Force initial setup + $(window).on "unload", () -> + cursorPositionManager.onUnload(editor.getSession()) + + tearDownCursorPosition = () -> + editor.off 'changeSession', onSessionChangeForCursorPosition + attachToAce = (sharejs_doc) -> lines = sharejs_doc.getSnapshot().split("\n") session = editor.getSession() @@ -426,6 +442,7 @@ define [ # now ready to edit document editor.setReadOnly(scope.readOnly) # respect the readOnly setting, normally false initSpellCheck() + initCursorPosition() resetScrollMargins() @@ -488,6 +505,7 @@ define [ scope.$on '$destroy', () -> if scope.sharejsDoc? tearDownSpellCheck() + tearDownCursorPosition() detachFromAce(scope.sharejsDoc) session = editor.getSession() session?.destroy() diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee new file mode 100644 index 0000000000..6c8c051f8a --- /dev/null +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee @@ -0,0 +1,35 @@ +define [ + "ide/editor/AceShareJsCodec" +], (AceShareJsCodec) -> + class CursorPositionAdapter + constructor: (@editor) -> + + getCursor: () -> + @editor.getCursorPosition() + + getCursorForSession: (session) -> + session.selection.getCursor() + + getScrollTopForSession: (session) -> + session.getScrollTop() + + setCursor: (pos) -> + pos = pos.cursorPosition or { row: 0, column: 0 } + @editor.moveCursorToPosition(pos) + + setScrollTop: (pos) -> + pos = pos.scrollTop or 0 + @editor.getSession().setScrollTop(pos) + + clearSelection: () -> + @editor.selection.clearSelection() + + gotoLine: (line, column) -> + @editor.gotoLine(line, column) + @editor.scrollToLine(line, true, true) # centre and animate + @editor.focus() + + gotoOffset: (offset) -> + lines = @editor.getSession().getDocument().getAllLines() + position = AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines) + @gotoLine(position.row + 1, position.column) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee index 119aa471e1..2b6ab3eeaf 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee @@ -1,75 +1,60 @@ -define [ - "ide/editor/AceShareJsCodec" -], (AceShareJsCodec) -> +define [], () -> class CursorPositionManager - constructor: (@$scope, @editor, @element, @localStorage) -> - - onChangeCursor = (e) => - @emitCursorUpdateEvent(e) - - @editor.on "changeSession", (e) => - if e.oldSession? - @storeCursorPosition(e.oldSession) - @storeScrollTopPosition(e.oldSession) - - @doc_id = @$scope.sharejsDoc?.doc_id - - e.oldSession?.selection.off 'changeCursor', onChangeCursor - e.session.selection.on 'changeCursor', onChangeCursor - - setTimeout () => - @gotoStoredPosition() - , 0 - - $(window).on "unload", () => - @storeCursorPosition(@editor.getSession()) - @storeScrollTopPosition(@editor.getSession()) - + constructor: (@$scope, @adapter, @localStorage) -> @$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) => if line? setTimeout () => - @gotoLine(line, column) + @adapter.gotoLine(line, column) , 10 # Hack: Must happen after @gotoStoredPosition @$scope.$on "#{@$scope.name}:gotoOffset", (e, offset) => if offset? setTimeout () => - @gotoOffset(offset) + @adapter.gotoOffset(offset) , 10 # Hack: Must happen after @gotoStoredPosition @$scope.$on "#{@$scope.name}:clearSelection", (e) => - @editor.selection.clearSelection() + @adapter.clearSelection() + + init: () -> + @emitCursorUpdateEvent() + + onSessionChange: (e) => + if e.oldSession? + @storeCursorPosition(e.oldSession) + @storeScrollTopPosition(e.oldSession) + + @doc_id = @$scope.sharejsDoc?.doc_id + + setTimeout () => + @gotoStoredPosition() + , 0 + + onUnload: (session) => + @storeCursorPosition(session) + @storeScrollTopPosition(session) + + onCursorChange: () => + @emitCursorUpdateEvent() storeScrollTopPosition: (session) -> if @doc_id? docPosition = @localStorage("doc.position.#{@doc_id}") || {} - docPosition.scrollTop = session.getScrollTop() + docPosition.scrollTop = @adapter.getScrollTopForSession(session) @localStorage("doc.position.#{@doc_id}", docPosition) storeCursorPosition: (session) -> if @doc_id? docPosition = @localStorage("doc.position.#{@doc_id}") || {} - docPosition.cursorPosition = session.selection.getCursor() + docPosition.cursorPosition = @adapter.getCursorForSession(session) @localStorage("doc.position.#{@doc_id}", docPosition) - + emitCursorUpdateEvent: () -> - cursor = @editor.getCursorPosition() + cursor = @adapter.getCursor() @$scope.$emit "cursor:#{@$scope.name}:update", cursor gotoStoredPosition: () -> return if !@doc_id? pos = @localStorage("doc.position.#{@doc_id}") || {} - @ignoreCursorPositionChanges = true - @editor.moveCursorToPosition(pos.cursorPosition or {row: 0, column: 0}) - @editor.getSession().setScrollTop(pos.scrollTop or 0) - delete @ignoreCursorPositionChanges - - gotoLine: (line, column) -> - @editor.gotoLine(line, column) - @editor.scrollToLine(line,true,true) # centre and animate - @editor.focus() - - gotoOffset: (offset) -> - lines = @editor.getSession().getDocument().getAllLines() - position = AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines) - @gotoLine(position.row + 1, position.column) \ No newline at end of file + @adapter.setCursor(pos) + @adapter.setScrollTop(pos) From 6c7e942470896e8266050231e37cb7f6ec78e161 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 5 Jul 2018 14:25:58 +0100 Subject: [PATCH 010/188] Unbind unload listener when destroying editor --- .../public/coffee/ide/editor/directives/aceEditor.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index ef6db538c4..b92e7bfb0c 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -386,15 +386,18 @@ define [ e.oldSession?.selection.off 'changeCursor', cursorPositionManager.onCursorChange e.session.selection.on 'changeCursor', cursorPositionManager.onCursorChange + onUnloadForCursorPosition = () -> + cursorPositionManager.onUnload(editor.getSession()) + initCursorPosition = () -> cursorPositionManager.init() editor.on 'changeSession', onSessionChangeForCursorPosition onSessionChangeForCursorPosition({ session: editor.getSession() }) # Force initial setup - $(window).on "unload", () -> - cursorPositionManager.onUnload(editor.getSession()) + $(window).on "unload", onUnloadForCursorPosition tearDownCursorPosition = () -> editor.off 'changeSession', onSessionChangeForCursorPosition + $(window).off "unload", onUnloadForCursorPosition attachToAce = (sharejs_doc) -> lines = sharejs_doc.getSnapshot().split("\n") From ba9fa9a0beddd5c18d18c9d3df8115b4cb059fc8 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 5 Jul 2018 17:24:45 +0100 Subject: [PATCH 011/188] Store first visible line instead of scrollTop scrollTop is affected by changing viewport size and switching between Ace and CM --- .../cursor-position/CursorPositionAdapter.coffee | 13 +++++-------- .../cursor-position/CursorPositionManager.coffee | 10 +++++----- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee index 6c8c051f8a..d58f012b57 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee @@ -7,19 +7,16 @@ define [ getCursor: () -> @editor.getCursorPosition() - getCursorForSession: (session) -> - session.selection.getCursor() - - getScrollTopForSession: (session) -> - session.getScrollTop() + getEditorScrollPosition: () -> + @editor.getFirstVisibleRow() setCursor: (pos) -> pos = pos.cursorPosition or { row: 0, column: 0 } @editor.moveCursorToPosition(pos) - setScrollTop: (pos) -> - pos = pos.scrollTop or 0 - @editor.getSession().setScrollTop(pos) + setEditorScrollPosition: (pos) -> + pos = pos.firstVisibleLine or 0 + @editor.scrollToLine(pos) clearSelection: () -> @editor.selection.clearSelection() diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee index 2b6ab3eeaf..b7ba730507 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee @@ -22,7 +22,7 @@ define [], () -> onSessionChange: (e) => if e.oldSession? @storeCursorPosition(e.oldSession) - @storeScrollTopPosition(e.oldSession) + @storeFirstVisibleLine() @doc_id = @$scope.sharejsDoc?.doc_id @@ -32,15 +32,15 @@ define [], () -> onUnload: (session) => @storeCursorPosition(session) - @storeScrollTopPosition(session) + @storeFirstVisibleLine() onCursorChange: () => @emitCursorUpdateEvent() - storeScrollTopPosition: (session) -> + storeFirstVisibleLine: () -> if @doc_id? docPosition = @localStorage("doc.position.#{@doc_id}") || {} - docPosition.scrollTop = @adapter.getScrollTopForSession(session) + docPosition.firstVisibleLine = @adapter.getEditorScrollPosition() @localStorage("doc.position.#{@doc_id}", docPosition) storeCursorPosition: (session) -> @@ -57,4 +57,4 @@ define [], () -> return if !@doc_id? pos = @localStorage("doc.position.#{@doc_id}") || {} @adapter.setCursor(pos) - @adapter.setScrollTop(pos) + @adapter.setEditorScrollPosition(pos) From da77c067744b01f6a87286717fda054b341afe58 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 5 Jul 2018 17:37:53 +0100 Subject: [PATCH 012/188] Refactor saving cursor position to not use Ace event This is will help with triggering CM correctly --- .../coffee/ide/editor/directives/aceEditor.coffee | 3 ++- .../cursor-position/CursorPositionManager.coffee | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index b92e7bfb0c..fadc5a4a03 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -308,6 +308,8 @@ define [ editor.renderer.updateFontSize() scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) -> + cursorPositionManager.onBeforeSessionChange(!!old_sharejs_doc) + if old_sharejs_doc? detachFromAce(old_sharejs_doc) @@ -382,7 +384,6 @@ define [ editor.off 'nativecontextmenu', spellCheckManager.onContextMenu onSessionChangeForCursorPosition = (e) -> - cursorPositionManager.onSessionChange(e) e.oldSession?.selection.off 'changeCursor', cursorPositionManager.onCursorChange e.session.selection.on 'changeCursor', cursorPositionManager.onCursorChange diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee index b7ba730507..57acb81546 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee @@ -19,9 +19,9 @@ define [], () -> init: () -> @emitCursorUpdateEvent() - onSessionChange: (e) => - if e.oldSession? - @storeCursorPosition(e.oldSession) + onBeforeSessionChange: (hasPrevSession = false) => + if hasPrevSession + @storeCursorPosition() @storeFirstVisibleLine() @doc_id = @$scope.sharejsDoc?.doc_id @@ -30,8 +30,8 @@ define [], () -> @gotoStoredPosition() , 0 - onUnload: (session) => - @storeCursorPosition(session) + onUnload: () => + @storeCursorPosition() @storeFirstVisibleLine() onCursorChange: () => @@ -43,10 +43,10 @@ define [], () -> docPosition.firstVisibleLine = @adapter.getEditorScrollPosition() @localStorage("doc.position.#{@doc_id}", docPosition) - storeCursorPosition: (session) -> + storeCursorPosition: () -> if @doc_id? docPosition = @localStorage("doc.position.#{@doc_id}") || {} - docPosition.cursorPosition = @adapter.getCursorForSession(session) + docPosition.cursorPosition = @adapter.getCursor() @localStorage("doc.position.#{@doc_id}", docPosition) emitCursorUpdateEvent: () -> From 5806101bd0223f77093e43928ef5e72591eb03ce Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 6 Jul 2018 15:37:51 +0100 Subject: [PATCH 013/188] Trigger events instead of calling cursor manager This improves readability and prevents race conditions in compat between Ace/CM --- .../ide/editor/directives/aceEditor.coffee | 19 +++++++++++++------ .../CursorPositionManager.coffee | 18 +++++++++--------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index fadc5a4a03..34da37b6e4 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -16,7 +16,7 @@ define [ "ide/metadata/services/metadata" "ide/graphics/services/graphics" "ide/preamble/services/preamble" - "ide/files/services/files" + "ide/files/services/files" ], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, SpellCheckAdapter, HighlightsManager, CursorPositionManager, CursorPositionAdapter, TrackChangesManager, MetadataManager) -> EditSession = ace.require('ace/edit_session').EditSession ModeList = ace.require('ace/ext/modelist') @@ -308,13 +308,13 @@ define [ editor.renderer.updateFontSize() scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) -> - cursorPositionManager.onBeforeSessionChange(!!old_sharejs_doc) - if old_sharejs_doc? + scope.$broadcast('beforeChangeDocument') detachFromAce(old_sharejs_doc) - if sharejs_doc? attachToAce(sharejs_doc) + if sharejs_doc? and old_sharejs_doc? + scope.$broadcast('afterChangeDocument') scope.$watch "text", (text) -> if text? @@ -391,7 +391,6 @@ define [ cursorPositionManager.onUnload(editor.getSession()) initCursorPosition = () -> - cursorPositionManager.init() editor.on 'changeSession', onSessionChangeForCursorPosition onSessionChangeForCursorPosition({ session: editor.getSession() }) # Force initial setup $(window).on "unload", onUnloadForCursorPosition @@ -400,6 +399,14 @@ define [ editor.off 'changeSession', onSessionChangeForCursorPosition $(window).off "unload", onUnloadForCursorPosition + initCursorPosition() + + # Trigger the event once *only* - this is called after Ace is connected + # to the ShareJs instance but this event should only be triggered the + # first time the editor is opened. Not every time the docs opened + triggerEditorInitEvent = _.once () -> + scope.$broadcast('editorInit') + attachToAce = (sharejs_doc) -> lines = sharejs_doc.getSnapshot().split("\n") session = editor.getSession() @@ -445,8 +452,8 @@ define [ editor.initing = false # now ready to edit document editor.setReadOnly(scope.readOnly) # respect the readOnly setting, normally false + triggerEditorInitEvent() initSpellCheck() - initCursorPosition() resetScrollMargins() diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee index 57acb81546..2cf025f70e 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee @@ -1,6 +1,14 @@ define [], () -> class CursorPositionManager constructor: (@$scope, @adapter, @localStorage) -> + @$scope.$on 'editorInit', @jumpToPositionInNewDoc + + @$scope.$on 'beforeChangeDocument', () => + @storeCursorPosition() + @storeFirstVisibleLine() + + @$scope.$on 'afterChangeDocument', @jumpToPositionInNewDoc + @$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) => if line? setTimeout () => @@ -16,16 +24,8 @@ define [], () -> @$scope.$on "#{@$scope.name}:clearSelection", (e) => @adapter.clearSelection() - init: () -> - @emitCursorUpdateEvent() - - onBeforeSessionChange: (hasPrevSession = false) => - if hasPrevSession - @storeCursorPosition() - @storeFirstVisibleLine() - + jumpToPositionInNewDoc: () => @doc_id = @$scope.sharejsDoc?.doc_id - setTimeout () => @gotoStoredPosition() , 0 From ba17c1da14b675d6e05764b3e64ff6946872b83a Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jul 2018 16:15:50 +0100 Subject: [PATCH 014/188] Cache pug templates and modules, by using production environment --- services/web/config/settings.defaults.coffee | 2 +- services/web/docker-compose.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index a9383714c9..1306ce91c2 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -425,7 +425,7 @@ module.exports = settings = redirects: "/templates/index": "/templates/" - reloadModuleViewsOnEachRequest: true + reloadModuleViewsOnEachRequest: process.env['NODE_ENV'] != 'production' domainLicences: [ diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml index afba1ccc60..ebf164f606 100644 --- a/services/web/docker-compose.yml +++ b/services/web/docker-compose.yml @@ -19,6 +19,7 @@ services: LINKED_URL_PROXY: 'http://localhost:6543' ENABLED_LINKED_FILE_TYPES: 'url,project_file,project_output_file,mendeley' SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee + NODE_ENV: production depends_on: - redis - mongo From dc724eae92c3e62cdbfed42e8b873fc2f0eed874 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jul 2018 16:42:52 +0100 Subject: [PATCH 015/188] Try with parallel tests --- services/web/Jenkinsfile | 58 +++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index 7b44d70315..721ede90a8 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -67,15 +67,38 @@ pipeline { } } - stage('Unit Test') { - agent { - docker { - image 'node:6.9.5' - reuseNode true + stage('Test and Minify') { + parallel { + stage('Unit Test') { + agent { + docker { + image 'node:6.9.5' + reuseNode true + } + } + steps { + sh 'make --no-print-directory test_unit MOCHA_ARGS="--reporter tap"' + } + } + + stage('Acceptance Test') { + steps { + // Spawns its own docker containers + sh 'make --no-print-directory test_acceptance MOCHA_ARGS="--reporter tap"' + } + } + + stage('Minify') { + agent { + docker { + image 'node:6.9.5' + reuseNode true + } + } + steps { + sh 'WEBPACK_ENV=production make minify' + } } - } - steps { - sh 'make --no-print-directory test_unit MOCHA_ARGS="--reporter tap"' } } @@ -86,25 +109,6 @@ pipeline { } } - stage('Acceptance Test') { - steps { - // Spawns its own docker containers - sh 'make --no-print-directory test_acceptance MOCHA_ARGS="--reporter tap"' - } - } - - stage('Minify') { - agent { - docker { - image 'node:6.9.5' - reuseNode true - } - } - steps { - sh 'WEBPACK_ENV=production make minify' - } - } - stage('Package') { steps { sh 'rm -rf ./node_modules/grunt*' From 791dc0df5b4ef87abfbbee234580e4e8e5430926 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Tue, 10 Jul 2018 11:42:17 +0200 Subject: [PATCH 016/188] check institution features on features update --- .../Institutions/InstitutionsFeatures.coffee | 23 +++++ .../Subscription/FeaturesUpdater.coffee | 9 +- .../coffee/FeatureUpdaterTests.coffee | 21 ++++ .../coffee/helpers/MockV1Api.coffee | 6 +- .../acceptance/coffee/helpers/User.coffee | 5 + .../acceptance/config/settings.test.coffee | 1 + .../InstitutionsFeaturesTests.coffee | 96 +++++++++++++++++++ .../Subscription/FeaturesUpdaterTests.coffee | 10 ++ 8 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee create mode 100644 services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee new file mode 100644 index 0000000000..5c91058146 --- /dev/null +++ b/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee @@ -0,0 +1,23 @@ +UserGetter = require '../User/UserGetter' +PlansLocator = require '../Subscription/PlansLocator' +Settings = require 'settings-sharelatex' +logger = require 'logger-sharelatex' + +module.exports = InstitutionsFeatures = + getInstitutionsFeatures: (userId, callback = (error, features) ->) -> + InstitutionsFeatures.hasLicence userId, (error, hasLicence) -> + return callback error if error? + return callback(null, {}) unless hasLicence + plan = PlansLocator.findLocalPlanInSettings Settings.institutionPlanCode + callback(null, plan?.features or {}) + + + hasLicence: (userId, callback = (error, hasLicence) ->) -> + UserGetter.getUserFullEmails userId, (error, emailsData) -> + return callback error if error? + + affiliation = emailsData.find (emailData) -> + licence = emailData.affiliation?.institution?.licence + emailData.confirmedAt? and licence? and licence != 'free' + + callback(null, !!affiliation) diff --git a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee b/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee index 5627072c93..ef5a6c4bb2 100644 --- a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee @@ -7,6 +7,7 @@ Settings = require("settings-sharelatex") logger = require("logger-sharelatex") ReferalFeatures = require("../Referal/ReferalFeatures") V1SubscriptionManager = require("./V1SubscriptionManager") +InstitutionsFeatures = require '../Institutions/InstitutionsFeatures' oneMonthInSeconds = 60 * 60 * 24 * 30 @@ -21,9 +22,11 @@ module.exports = FeaturesUpdater = if error? logger.err {err: error, user_id}, "error notifying v1 about updated features" + jobs = individualFeatures: (cb) -> FeaturesUpdater._getIndividualFeatures user_id, cb groupFeatureSets: (cb) -> FeaturesUpdater._getGroupFeatureSets user_id, cb + institutionFeatures:(cb) -> InstitutionsFeatures.getInstitutionsFeatures user_id, cb v1Features: (cb) -> FeaturesUpdater._getV1Features user_id, cb bonusFeatures: (cb) -> ReferalFeatures.getBonusFeatures user_id, cb async.series jobs, (err, results)-> @@ -32,9 +35,9 @@ module.exports = FeaturesUpdater = "error getting subscription or group for refreshFeatures" return callback(err) - {individualFeatures, groupFeatureSets, v1Features, bonusFeatures} = results - logger.log {user_id, individualFeatures, groupFeatureSets, v1Features, bonusFeatures}, 'merging user features' - featureSets = groupFeatureSets.concat [individualFeatures, v1Features, bonusFeatures] + {individualFeatures, groupFeatureSets, institutionFeatures, v1Features, bonusFeatures} = results + logger.log {user_id, individualFeatures, groupFeatureSets, institutionFeatures, v1Features, bonusFeatures}, 'merging user features' + featureSets = groupFeatureSets.concat [individualFeatures, institutionFeatures, v1Features, bonusFeatures] features = _.reduce(featureSets, FeaturesUpdater._mergeFeatures, Settings.defaultFeatures) logger.log {user_id, features}, 'updating user features' diff --git a/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee b/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee index 7c6c106fe2..29c0cce6f1 100644 --- a/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee +++ b/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee @@ -83,6 +83,27 @@ describe "FeatureUpdater.refreshFeatures", -> )) done() + describe "when the user has affiliations", -> + beforeEach -> + @institutionPlan = settings.plans.find (plan) -> + plan.planCode == settings.institutionPlanCode + @email = @user.emails[0].email + affiliationData = + email: @email + institution: { licence: 'pro_plus' } + MockV1Api.setAffiliations [affiliationData] + + it "should not set their features if email is not confirmed", (done) -> + syncUserAndGetFeatures @user, (error, features) => + expect(features).to.deep.equal(settings.defaultFeatures) + done() + + it "should set their features if email is confirmed", (done) -> + @user.confirmEmail @email, (error) => + syncUserAndGetFeatures @user, (error, features) => + expect(features).to.deep.equal(@institutionPlan.features) + done() + describe "when the user is due bonus features and has extra features that no longer apply", -> beforeEach -> User.update { diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee index f537614f17..7599230b09 100644 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee @@ -26,6 +26,10 @@ module.exports = MockV1Api = syncUserFeatures: sinon.stub() + affiliations: [] + + setAffiliations: (affiliations) -> @affiliations = affiliations + run: () -> app.get "/api/v1/sharelatex/users/:v1_user_id/plan_code", (req, res, next) => user = @users[req.params.v1_user_id] @@ -43,7 +47,7 @@ module.exports = MockV1Api = res.json exportId: @exportId app.get "/api/v2/users/:userId/affiliations", (req, res, next) => - res.json [] + res.json @affiliations app.post "/api/v2/users/:userId/affiliations", (req, res, next) => res.sendStatus 201 diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index 54b660c6b5..b8740f709c 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -100,6 +100,11 @@ class User @emails.push(email: email, createdAt: new Date()) UserUpdater.addEmailAddress @id, email, callback + confirmEmail: (email, callback = (error) ->) -> + for emailData, idx in @emails + @emails[idx].confirmedAt = new Date() if emailData.email == email + UserUpdater.confirmEmail @id, email, callback + ensure_admin: (callback = (error) ->) -> db.users.update {_id: ObjectId(@id)}, { $set: { isAdmin: true }}, callback diff --git a/services/web/test/acceptance/config/settings.test.coffee b/services/web/test/acceptance/config/settings.test.coffee index b64090da4b..4dd820f4a7 100644 --- a/services/web/test/acceptance/config/settings.test.coffee +++ b/services/web/test/acceptance/config/settings.test.coffee @@ -56,6 +56,7 @@ module.exports = defaultFeatures: features.personal defaultPlanCode: 'personal' + institutionPlanCode: 'professional' plans: plans = [{ planCode: "v1_free" diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee new file mode 100644 index 0000000000..2304f2e5b7 --- /dev/null +++ b/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee @@ -0,0 +1,96 @@ +SandboxedModule = require('sandboxed-module') +assert = require('assert') +require('chai').should() +expect = require('chai').expect +sinon = require('sinon') +modulePath = require('path').join __dirname, '../../../../app/js/Features/Institutions/InstitutionsFeatures.js' + +describe 'InstitutionsFeatures', -> + + beforeEach -> + @UserGetter = getUserFullEmails: sinon.stub() + @PlansLocator = findLocalPlanInSettings: sinon.stub() + @institutionPlanCode = 'institution_plan_code' + @InstitutionsFeatures = SandboxedModule.require modulePath, requires: + '../User/UserGetter': @UserGetter + '../Subscription/PlansLocator': @PlansLocator + 'settings-sharelatex': institutionPlanCode: @institutionPlanCode + 'logger-sharelatex': + log:-> + err:-> + + @userId = '12345abcde' + + describe "hasLicence", -> + it 'should handle error', (done)-> + @UserGetter.getUserFullEmails.yields(new Error('Nope')) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.exist + done() + + it 'should return false if user has no affiliations', (done) -> + @UserGetter.getUserFullEmails.yields(null, []) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.not.exist + expect(hasLicence).to.be.false + done() + + it 'should return false if user has no confirmed affiliations', (done) -> + affiliations = [ + { confirmedAt: null, affiliation: institution: { licence: 'pro_plus' } } + ] + @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.not.exist + expect(hasLicence).to.be.false + done() + + it 'should return false if user has no paid affiliations', (done) -> + affiliations = [ + { confirmedAt: new Date(), affiliation: institution: { licence: 'free' } } + ] + @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.not.exist + expect(hasLicence).to.be.false + done() + + it 'should return true if user has confirmed paid affiliation', (done)-> + affiliations = [ + { confirmedAt: new Date(), affiliation: institution: { licence: 'pro_plus' } } + { confirmedAt: new Date(), affiliation: institution: { licence: 'free' } } + { confirmedAt: null, affiliation: institution: { licence: 'pro' } } + { confirmedAt: null, affiliation: institution: { licence: null } } + { confirmedAt: new Date(), affiliation: institution: {} } + ] + @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.not.exist + expect(hasLicence).to.be.true + done() + + describe "getInstitutionsFeatures", -> + beforeEach -> + @InstitutionsFeatures.hasLicence = sinon.stub() + @testFeatures = features: { institution: 'all' } + @PlansLocator.findLocalPlanInSettings.withArgs(@institutionPlanCode).returns(@testFeatures) + + it 'should handle error', (done)-> + @InstitutionsFeatures.hasLicence.yields(new Error('Nope')) + @InstitutionsFeatures.getInstitutionsFeatures @userId, (error, features) -> + expect(error).to.exist + done() + + it 'should return no feaures if user has no plan code', (done) -> + @InstitutionsFeatures.hasLicence.yields(null, false) + @InstitutionsFeatures.getInstitutionsFeatures @userId, (error, features) -> + expect(error).to.not.exist + expect(features).to.deep.equal {} + done() + + it 'should return feaures if user has affiliations plan code', (done) -> + @InstitutionsFeatures.hasLicence.yields(null, true) + @InstitutionsFeatures.getInstitutionsFeatures @userId, (error, features) => + expect(error).to.not.exist + expect(features).to.deep.equal @testFeatures.features + done() diff --git a/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee index 489c5676ff..0b4378c5e2 100644 --- a/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee +++ b/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee @@ -19,6 +19,7 @@ describe "FeaturesUpdater", -> 'settings-sharelatex': @Settings = {} "../Referal/ReferalFeatures" : @ReferalFeatures = {} "./V1SubscriptionManager": @V1SubscriptionManager = {} + '../Institutions/InstitutionsFeatures': @InstitutionsFeatures = {} describe "refreshFeatures", -> beforeEach -> @@ -26,6 +27,7 @@ describe "FeaturesUpdater", -> @UserFeaturesUpdater.updateFeatures = sinon.stub().yields() @FeaturesUpdater._getIndividualFeatures = sinon.stub().yields(null, { 'individual': 'features' }) @FeaturesUpdater._getGroupFeatureSets = sinon.stub().yields(null, [{ 'group': 'features' }, { 'group': 'features2' }]) + @InstitutionsFeatures.getInstitutionsFeatures = sinon.stub().yields(null, { 'institutions': 'features' }) @FeaturesUpdater._getV1Features = sinon.stub().yields(null, { 'v1': 'features' }) @ReferalFeatures.getBonusFeatures = sinon.stub().yields(null, { 'bonus': 'features' }) @FeaturesUpdater._mergeFeatures = sinon.stub().returns({'merged': 'features'}) @@ -45,6 +47,11 @@ describe "FeaturesUpdater", -> .calledWith(@user_id) .should.equal true + it "should get the institution features", -> + @InstitutionsFeatures.getInstitutionsFeatures + .calledWith(@user_id) + .should.equal true + it "should get the v1 features", -> @FeaturesUpdater._getV1Features .calledWith(@user_id) @@ -65,6 +72,9 @@ describe "FeaturesUpdater", -> @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features' }).should.equal true @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features2' }).should.equal true + it "should merge the institutions features", -> + @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'institutions': 'features' }).should.equal true + it "should merge the v1 features", -> @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'v1': 'features' }).should.equal true From 4e8185d3692de54479ffe34e0bf9c0e0c71ccdae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Wed, 11 Jul 2018 09:31:57 +0100 Subject: [PATCH 017/188] Use the id in methods that modify a subscription This will make it easier to allow a user to manage multiple groups. --- .../Subscription/LimitationsManager.coffee | 8 +- .../SubscriptionGroupController.coffee | 73 +++++++++++-------- .../SubscriptionGroupHandler.coffee | 22 +++--- .../Subscription/SubscriptionLocator.coffee | 10 +++ .../Subscription/SubscriptionUpdater.coffee | 14 ++-- .../LimitationsManagerTests.coffee | 8 +- .../SubscriptionGroupControllerTests.coffee | 20 +++-- .../SubscriptionGroupHandlerTests.coffee | 8 +- .../SubscriptionUpdaterTests.coffee | 18 +++-- 9 files changed, 103 insertions(+), 78 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index 5f5de9f549..078676086b 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -89,13 +89,13 @@ module.exports = LimitationsManager = return currentTotal >= subscription.membersLimit - hasGroupMembersLimitReached: (user_id, callback = (err, limitReached, subscription)->)-> - SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> + hasGroupMembersLimitReached: (subscriptionId, callback = (err, limitReached, subscription)->)-> + SubscriptionLocator.getSubscription subscriptionId, (err, subscription)-> if err? - logger.err err:err, user_id:user_id, "error getting users subscription" + logger.err err:err, subscriptionId: subscriptionId, "error getting subscription" return callback(err) if !subscription? - logger.err user_id:user_id, "no subscription found for user" + logger.err subscriptionId: subscriptionId, "no subscription found" return callback("no subscription found") limitReached = LimitationsManager.teamHasReachedMemberLimit(subscription) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee index 8e84f1fb0b..cab61c5425 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -9,46 +9,56 @@ async = require("async") module.exports = - addUserToGroup: (req, res)-> + addUserToGroup: (req, res, next)-> adminUserId = AuthenticationController.getLoggedInUserId(req) newEmail = req.body?.email?.toLowerCase()?.trim() - logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription" - SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)-> - if err? - logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group" - return res.sendStatus 500 - result = - user:user - if err and err.limitReached - result.limitReached = true - res.json(result) - removeUserFromGroup: (req, res)-> + SubscriptionLocator.getManagedSubscription adminUserId, (error, subscription) -> + return next(error) if error? + + logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription" + + SubscriptionGroupHandler.addUserToGroup subscription._id, newEmail, (err, user)-> + if err? + logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group" + return res.sendStatus 500 + result = + user:user + if err and err.limitReached + result.limitReached = true + res.json(result) + + removeUserFromGroup: (req, res, next)-> adminUserId = AuthenticationController.getLoggedInUserId(req) userToRemove_id = req.params.user_id - logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription" - SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)-> - if err? - logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group" - return res.sendStatus 500 - res.send() + SubscriptionLocator.getManagedSubscription adminUserId, (error, subscription) -> + return next(error) if error? + logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription" + SubscriptionGroupHandler.removeUserFromGroup subscription._id, userToRemove_id, (err)-> + if err? + logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group" + return res.sendStatus 500 + res.send() - removeSelfFromGroup: (req, res)-> + removeSelfFromGroup: (req, res, next)-> adminUserId = req.query.admin_user_id userToRemove_id = AuthenticationController.getLoggedInUserId(req) - logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request" - SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)-> - if err? - logger.err err:err, userToRemove_id:userToRemove_id, adminUserId:adminUserId, "error removing self from group" - return res.sendStatus 500 - res.send() + SubscriptionLocator.getManagedSubscription adminUserId, (error, subscription) -> + return next(error) if error? + logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request" + SubscriptionGroupHandler.removeUserFromGroup subscription._id, userToRemove_id, (err)-> + if err? + logger.err err:err, userToRemove_id:userToRemove_id, adminUserId:adminUserId, "error removing self from group" + return res.sendStatus 500 + res.send() - renderSubscriptionGroupAdminPage: (req, res)-> + renderSubscriptionGroupAdminPage: (req, res, next)-> user_id = AuthenticationController.getLoggedInUserId(req) - SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> + SubscriptionLocator.getManagedSubscription user_id, (error, subscription)-> + return next(error) if error? if !subscription?.groupPlan return res.redirect("/user/subscription") - SubscriptionGroupHandler.getPopulatedListOfMembers user_id, (err, users)-> + SubscriptionGroupHandler.getPopulatedListOfMembers subscription._id, (err, users)-> res.render "subscriptions/group_admin", title: 'group_admin' users: users @@ -57,10 +67,9 @@ module.exports = exportGroupCsv: (req, res)-> user_id = AuthenticationController.getLoggedInUserId(req) logger.log user_id: user_id, "exporting group csv" - SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> - if !subscription.groupPlan - return res.redirect("/") - SubscriptionGroupHandler.getPopulatedListOfMembers user_id, (err, users)-> + SubscriptionLocator.getManagedSubscription user_id, (err, subscription)-> + return next(error) if error? + SubscriptionGroupHandler.getPopulatedListOfMembers subscription._id, (err, users)-> groupCsv = "" for user in users groupCsv += user.email + "\n" diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee index 2f3ac4a803..55bd67ccc2 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee @@ -14,19 +14,19 @@ NotificationsBuilder = require("../Notifications/NotificationsBuilder") module.exports = SubscriptionGroupHandler = - addUserToGroup: (adminUserId, newEmail, callback)-> - logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group" - LimitationsManager.hasGroupMembersLimitReached adminUserId, (err, limitReached, subscription)-> + addUserToGroup: (subscriptionId, newEmail, callback)-> + logger.log subscriptionId:subscriptionId, newEmail:newEmail, "adding user to group" + LimitationsManager.hasGroupMembersLimitReached subscriptionId, (err, limitReached, subscription)-> if err? - logger.err err:err, adminUserId:adminUserId, newEmail:newEmail, "error checking if limit reached for group plan" + logger.err err:err, subscriptionId:subscriptionId, newEmail:newEmail, "error checking if limit reached for group plan" return callback(err) if limitReached - logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group" + logger.err subscriptionId:subscriptionId, newEmail:newEmail, "group subscription limit reached not adding user to group" return callback(limitReached:limitReached) UserGetter.getUserByAnyEmail newEmail, (err, user)-> return callback(err) if err? if user? - SubscriptionUpdater.addUserToGroup adminUserId, user._id, (err)-> + SubscriptionUpdater.addUserToGroup subscriptionId, user._id, (err)-> if err? logger.err err:err, "error adding user to group" return callback(err) @@ -34,13 +34,13 @@ module.exports = SubscriptionGroupHandler = userViewModel = buildUserViewModel(user) callback(err, userViewModel) else - TeamInvitesHandler.createInvite adminUserId, newEmail, (err) -> + TeamInvitesHandler.createInvite subscriptionId, newEmail, (err) -> return callback(err) if err? userViewModel = buildEmailInviteViewModel(newEmail) callback(err, userViewModel) - removeUserFromGroup: (adminUser_id, userToRemove_id, callback)-> - SubscriptionUpdater.removeUserFromGroup adminUser_id, userToRemove_id, callback + removeUserFromGroup: (subscriptionId, userToRemove_id, callback)-> + SubscriptionUpdater.removeUserFromGroup subscriptionId, userToRemove_id, callback replaceUserReferencesInGroups: (oldId, newId, callback) -> Subscription.update {admin_id: oldId}, {admin_id: newId}, (error) -> @@ -56,8 +56,8 @@ module.exports = SubscriptionGroupHandler = return callback(error) if error? Subscription.update query, removeOldUserUpdate, { multi: true }, callback - getPopulatedListOfMembers: (adminUser_id, callback)-> - SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)-> + getPopulatedListOfMembers: (subscriptionId, callback)-> + SubscriptionLocator.getSubscription subscriptionId, (err, subscription)-> users = [] for email in subscription.invited_emails or [] diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee index ef6693f9b8..a228af7ffc 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee @@ -14,6 +14,16 @@ module.exports = logger.log user_id:user_id, "got users subscription" callback(err, subscription) + getManagedSubscription: (managerId, callback)-> + logger.log managerId: managerId, "getting managed subscription" + Subscription.findOne admin_id: managerId, (err, subscription)-> + if subscription? + logger.log managerId: managerId, "got managed subscription" + else + err ||= new Error("No subscription found managed by user #{managerId}") + + callback(err, subscription) + getMemberSubscriptions: (user_or_id, callback) -> if user_or_id? and user_or_id._id? user_id = user_or_id._id diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index b5fce978a4..aea2042b06 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -25,13 +25,13 @@ module.exports = SubscriptionUpdater = return callback(err) if err? SubscriptionUpdater._updateSubscriptionFromRecurly recurlySubscription, subscription, callback - addUserToGroup: (adminUserId, userId, callback)-> - @addUsersToGroup(adminUserId, [userId], callback) + addUserToGroup: (subscriptionId, userId, callback)-> + @addUsersToGroup(subscriptionId, [userId], callback) - addUsersToGroup: (adminUserId, memberIds, callback)-> - logger.log adminUserId: adminUserId, memberIds: memberIds, "adding members into mongo subscription" + addUsersToGroup: (subscriptionId, memberIds, callback)-> + logger.log subscriptionId: subscriptionId, memberIds: memberIds, "adding members into mongo subscription" searchOps = - admin_id: adminUserId + _id: subscriptionId insertOperation = { $push: { member_ids: { $each: memberIds } } } @@ -46,9 +46,9 @@ module.exports = SubscriptionUpdater = async.map userIds, FeaturesUpdater.refreshFeatures, callback - removeUserFromGroup: (adminUser_id, user_id, callback)-> + removeUserFromGroup: (subscriptionId, user_id, callback)-> searchOps = - admin_id: adminUser_id + _id: subscriptionId removeOperation = "$pull": {member_ids:user_id} Subscription.update searchOps, removeOperation, (err)-> diff --git a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee index 87a8ac8afc..10b8a897eb 100644 --- a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee @@ -23,6 +23,7 @@ describe "LimitationsManager", -> @SubscriptionLocator = getUsersSubscription: sinon.stub() + getSubscription: sinon.stub() @LimitationsManager = SandboxedModule.require modulePath, requires: '../Project/ProjectGetter': @ProjectGetter @@ -310,21 +311,21 @@ describe "LimitationsManager", -> ] it "should return true if the limit is hit (including members and invites)", (done)-> - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) + @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) @LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)-> limitReached.should.equal true done() it "should return false if the limit is not hit (including members and invites)", (done)-> @subscription.membersLimit = 4 - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) + @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) @LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)-> limitReached.should.equal false done() it "should return true if the limit has been exceded (including members and invites)", (done)-> @subscription.membersLimit = 2 - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) + @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) @LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)-> limitReached.should.equal true done() @@ -379,4 +380,3 @@ describe "LimitationsManager", -> @V1SubscriptionManager.getSubscriptionsFromV1.calledWith(@user_id).should.equal true result.should.equal false done() - diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee index d9ad55080f..5d8cb397c0 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee @@ -10,7 +10,7 @@ describe "SubscriptionGroupController", -> beforeEach -> @user = {_id:"!@312431",email:"user@email.com"} @adminUserId = "123jlkj" - @subscription_id = "123434325412" + @subscriptionId = "123434325412" @user_email = "bob@gmail.com" @req = session: @@ -18,15 +18,19 @@ describe "SubscriptionGroupController", -> _id: @adminUserId email:@user_email params: - subscription_id:@subscription_id + subscriptionId:@subscriptionId query:{} - @subscription = {} + @subscription = { + _id: @subscriptionId + } @GroupHandler = addUserToGroup: sinon.stub().callsArgWith(2, null, @user) removeUserFromGroup: sinon.stub().callsArgWith(2) isUserPartOfGroup: sinon.stub() getPopulatedListOfMembers: sinon.stub().callsArgWith(1, null, [@user]) - @SubscriptionLocator = getUsersSubscription: sinon.stub().callsArgWith(1, null, @subscription) + @SubscriptionLocator = + getManagedSubscription: sinon.stub().callsArgWith(1, null, @subscription) + @AuthenticationController = getLoggedInUserId: (req) -> req.session.user._id getSessionUser: (req) -> req.session.user @@ -55,25 +59,25 @@ describe "SubscriptionGroupController", -> describe "addUserToGroup", -> - it "should use the admin id for the logged in user and take the email address from the body", (done)-> + it "should use the subscription id for the logged in user and take the email address from the body", (done)-> newEmail = " boB@gmaiL.com " @req.body = email: newEmail res = json : (data)=> - @GroupHandler.addUserToGroup.calledWith(@adminUserId, "bob@gmail.com").should.equal true + @GroupHandler.addUserToGroup.calledWith(@subscriptionId, "bob@gmail.com").should.equal true data.user.should.deep.equal @user done() @Controller.addUserToGroup @req, res describe "removeUserFromGroup", -> - it "should use the admin id for the logged in user and take the user id from the params", (done)-> + it "should use the subscription id for the logged in user and take the user id from the params", (done)-> userIdToRemove = "31231" @req.params = user_id: userIdToRemove res = send : => - @GroupHandler.removeUserFromGroup.calledWith(@adminUserId, userIdToRemove).should.equal true + @GroupHandler.removeUserFromGroup.calledWith(@subscriptionId, userIdToRemove).should.equal true done() @Controller.removeUserFromGroup @req, res diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee index e5a5695cc1..db8aac0375 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee @@ -81,7 +81,7 @@ describe "SubscriptionGroupHandler", -> beforeEach -> @LimitationsManager.hasGroupMembersLimitReached.callsArgWith(1, null, false, @subscription) @UserGetter.getUserByAnyEmail.callsArgWith(1, null, @user) - + it "should find the user", (done)-> @Handler.addUserToGroup @adminUser_id, @newEmail, (err)=> @UserGetter.getUserByAnyEmail.calledWith(@newEmail).should.equal true @@ -156,13 +156,13 @@ describe "SubscriptionGroupHandler", -> describe "getPopulatedListOfMembers", -> beforeEach -> @subscription = {} - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) + @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) @UserGetter.getUser.callsArgWith(1, null, {_id:"31232"}) it "should locate the subscription", (done)-> @UserGetter.getUser.callsArgWith(1, null, {_id:"31232"}) - @Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=> - @SubscriptionLocator.getUsersSubscription.calledWith(@adminUser_id).should.equal true + @Handler.getPopulatedListOfMembers @subscriptionId, (err, users)=> + @SubscriptionLocator.getSubscription.calledWith(@subscriptionId).should.equal true done() it "should get the users by id", (done)-> diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee index 376d526db0..f88dac383f 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee @@ -18,6 +18,7 @@ describe "SubscriptionUpdater", -> @otherUserId = "5208dd34438842e2db000005" @allUserIds = ["13213", "dsadas", "djsaiud89"] @subscription = subscription = + _id: "111111111111111111111111" admin_id: @adminUser._id member_ids: @allUserIds save: sinon.stub().callsArgWith(0) @@ -26,6 +27,7 @@ describe "SubscriptionUpdater", -> @user_id = @adminuser_id @groupSubscription = + _id: "222222222222222222222222" admin_id: @adminUser._id member_ids: @allUserIds save: sinon.stub().callsArgWith(0) @@ -158,9 +160,9 @@ describe "SubscriptionUpdater", -> @SubscriptionUpdater.addUsersToGroup = sinon.stub().yields(null) it "delegates to addUsersToGroup", (done)-> - @SubscriptionUpdater.addUserToGroup @adminUser._id, @otherUserId, => + @SubscriptionUpdater.addUserToGroup @subscription._id, @otherUserId, => @SubscriptionUpdater.addUsersToGroup - .calledWith(@adminUser._id, [@otherUserId]).should.equal true + .calledWith(@subscription._id, [@otherUserId]).should.equal true done() describe "addUsersToGroup", -> @@ -168,16 +170,16 @@ describe "SubscriptionUpdater", -> @FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) it "should add the user ids to the group as a set", (done)-> - @SubscriptionUpdater.addUsersToGroup @adminUser._id, [@otherUserId], => + @SubscriptionUpdater.addUsersToGroup @subscription._id, [@otherUserId], => searchOps = - admin_id: @adminUser._id + _id: @subscription._id insertOperation = { $push: { member_ids: { $each: [@otherUserId] } } } @findAndModifyStub.calledWith(searchOps, insertOperation).should.equal true done() it "should update the users features", (done)-> - @SubscriptionUpdater.addUserToGroup @adminUser._id, @otherUserId, => + @SubscriptionUpdater.addUserToGroup @subscription._id, @otherUserId, => @FeaturesUpdater.refreshFeatures.calledWith(@otherUserId).should.equal true done() @@ -186,16 +188,16 @@ describe "SubscriptionUpdater", -> @FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) it "should pull the users id from the group", (done)-> - @SubscriptionUpdater.removeUserFromGroup @adminUser._id, @otherUserId, => + @SubscriptionUpdater.removeUserFromGroup @subscription._id, @otherUserId, => searchOps = - admin_id:@adminUser._id + _id: @subscription._id removeOperation = "$pull": {member_ids:@otherUserId} @updateStub.calledWith(searchOps, removeOperation).should.equal true done() it "should update the users features", (done)-> - @SubscriptionUpdater.removeUserFromGroup @adminUser._id, @otherUserId, => + @SubscriptionUpdater.removeUserFromGroup @subscription._id, @otherUserId, => @FeaturesUpdater.refreshFeatures.calledWith(@otherUserId).should.equal true done() From 3586fe840635814a7ce6d066f3480c3667ddde0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Wed, 11 Jul 2018 11:50:14 +0100 Subject: [PATCH 018/188] Update var name --- .../coffee/Subscription/LimitationsManagerTests.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee index 10b8a897eb..9d87870121 100644 --- a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee @@ -302,7 +302,7 @@ describe "LimitationsManager", -> describe "hasGroupMembersLimitReached", -> beforeEach -> - @user_id = "12312" + @subscriptionId = "12312" @subscription = membersLimit: 3 member_ids: ["", ""] @@ -312,21 +312,21 @@ describe "LimitationsManager", -> it "should return true if the limit is hit (including members and invites)", (done)-> @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) - @LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)-> + @LimitationsManager.hasGroupMembersLimitReached @subscriptionId, (err, limitReached)-> limitReached.should.equal true done() it "should return false if the limit is not hit (including members and invites)", (done)-> @subscription.membersLimit = 4 @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) - @LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)-> + @LimitationsManager.hasGroupMembersLimitReached @subscriptionId, (err, limitReached)-> limitReached.should.equal false done() it "should return true if the limit has been exceded (including members and invites)", (done)-> @subscription.membersLimit = 2 @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) - @LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)-> + @LimitationsManager.hasGroupMembersLimitReached @subscriptionId, (err, limitReached)-> limitReached.should.equal true done() From 40c1f675611751c36030f6a668f775831ded90d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Wed, 11 Jul 2018 12:10:38 +0100 Subject: [PATCH 019/188] Check that the subscription is a group before exporting it to csv I removed this check by mistake, just taking it back. --- .../Features/Subscription/SubscriptionGroupController.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee index cab61c5425..54c5827a20 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -69,6 +69,8 @@ module.exports = logger.log user_id: user_id, "exporting group csv" SubscriptionLocator.getManagedSubscription user_id, (err, subscription)-> return next(error) if error? + if !subscription.groupPlan + return res.redirect("/") SubscriptionGroupHandler.getPopulatedListOfMembers subscription._id, (err, users)-> groupCsv = "" for user in users From 35cb3924e2205fa1f4362d70aae5ab4e07e28f41 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Wed, 11 Jul 2018 14:57:35 +0200 Subject: [PATCH 020/188] refresh features after confirming email --- services/web/app/coffee/Features/User/UserUpdater.coffee | 3 ++- .../web/test/unit/coffee/User/UserUpdaterTests.coffee | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 948be16d41..5e9e3d906d 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -6,6 +6,7 @@ async = require("async") ObjectId = mongojs.ObjectId UserGetter = require("./UserGetter") { addAffiliation, removeAffiliation } = require("./UserAffiliationsManager") +FeaturesUpdater = require("../Subscription/FeaturesUpdater") EmailHelper = require "../Helpers/EmailHelper" Errors = require "../Errors/Errors" @@ -126,7 +127,7 @@ module.exports = UserUpdater = logger.log {res, userId, email}, "tried to confirm email" if res.n == 0 return callback(new Errors.NotFoundError('user id and email do no match')) - callback() + FeaturesUpdater.refreshFeatures userId, true, callback [ 'updateUser' diff --git a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee index 3820e2a448..cd8bb48cf4 100644 --- a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee +++ b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee @@ -21,12 +21,14 @@ describe "UserUpdater", -> @logger = err: sinon.stub(), log: -> @addAffiliation = sinon.stub().yields() @removeAffiliation = sinon.stub().callsArgWith(2, null) + @refreshFeatures = sinon.stub().yields() @UserUpdater = SandboxedModule.require modulePath, requires: "logger-sharelatex": @logger "./UserGetter": @UserGetter './UserAffiliationsManager': addAffiliation: @addAffiliation removeAffiliation: @removeAffiliation + '../Subscription/FeaturesUpdater': refreshFeatures: @refreshFeatures "../../infrastructure/mongojs":@mongojs "metrics-sharelatex": timeAsyncMethod: sinon.stub() @@ -245,3 +247,9 @@ describe "UserUpdater", -> should.exist(err) @UserUpdater.updateUser.called.should.equal false done() + + it 'refresh features', (done)-> + @UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=> + should.not.exist(err) + sinon.assert.calledWith(@refreshFeatures, @stubbedUser._id, true) + done() From 5616a91a9b0b3401e2315cbf206e4aebe50b8968 Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Wed, 11 Jul 2018 10:31:39 +0100 Subject: [PATCH 021/188] strip whitespace --- services/web/Jenkinsfile | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index 721ede90a8..f27eb79516 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -1,18 +1,18 @@ String cron_string = BRANCH_NAME == "master" ? "@daily" : "" pipeline { - + agent any - + environment { HOME = "/tmp" } - + triggers { pollSCM('* * * * *') cron(cron_string) } - + stages { stage('Install modules') { steps { @@ -21,7 +21,7 @@ pipeline { } } } - + stage('Install') { agent { docker { @@ -66,7 +66,7 @@ pipeline { sh 'make --no-print-directory lint' } } - + stage('Test and Minify') { parallel { stage('Unit Test') { @@ -80,7 +80,7 @@ pipeline { sh 'make --no-print-directory test_unit MOCHA_ARGS="--reporter tap"' } } - + stage('Acceptance Test') { steps { // Spawns its own docker containers @@ -108,7 +108,7 @@ pipeline { sh 'make --no-print-directory test_frontend' } } - + stage('Package') { steps { sh 'rm -rf ./node_modules/grunt*' @@ -117,7 +117,7 @@ pipeline { sh 'tar -czf build.tar.gz --exclude=build.tar.gz --exclude-vcs .' } } - + stage('Publish') { steps { withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { @@ -127,8 +127,8 @@ pipeline { } } } - - + + stage('Sync OSS') { when { branch 'master' @@ -140,29 +140,29 @@ pipeline { } } } - + post { always { sh 'make clean_ci' } failure { - mail(from: "${EMAIL_ALERT_FROM}", - to: "${EMAIL_ALERT_TO}", + mail(from: "${EMAIL_ALERT_FROM}", + to: "${EMAIL_ALERT_TO}", subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}", body: "Build: ${BUILD_URL}") } } - + // The options directive is for configuration that applies to the whole job. options { // Only build one at a time disableConcurrentBuilds() - + // we'd like to make sure remove old builds, so we don't fill up our storage! buildDiscarder(logRotator(numToKeepStr:'50')) - + // And we'd really like to be sure that this build doesn't hang forever, so let's time it out after: timeout(time: 30, unit: 'MINUTES') } From 9d81cf20f900af988fe84d8d458b9acb0742bffa Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Jul 2018 12:11:05 +0100 Subject: [PATCH 022/188] Add new unsupported class to control visibility while rich text is open --- .../web/app/views/project/editor/editor.pug | 1 + .../stylesheets/app/editor/review-panel.less | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index c24d967da7..28eafab60e 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -15,6 +15,7 @@ div.full-size( .ui-layout-center( ng-controller="ReviewPanelController", ng-class="{\ + 'rp-unsupported': editor.showRichText,\ 'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\ 'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\ 'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\ diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 1ed723c3b4..87fc3e73cf 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -139,6 +139,10 @@ padding: 0 5px; } + .rp-unsupported & { + display: none; + } + position: relative; border-bottom: 1px solid @rp-border-grey; background-color: @rp-bg-dim-blue; @@ -217,6 +221,10 @@ .rp-size-mini & { display: block; } + + .rp-unsupported & { + display: none; + } .rp-state-current-file & { position: absolute; @@ -714,6 +722,10 @@ display: flex; } + .rp-unsupported & { + display: none; + } + .rp-state-current-file & { position: absolute; bottom: 0; @@ -1003,7 +1015,8 @@ .rp-size-mini & { right: @review-off-width; } - .rp-size-expanded & { + .rp-size-expanded &, + .rp-unsupported & { display: none; } } @@ -1053,6 +1066,10 @@ display: block; } + .rp-unsupported & { + display: none; + } + .rp-size-expanded & { &::after { content: "\f105"; From 00c089448ee816f66fd19c1c3f9dc814134247e3 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Jul 2018 12:11:31 +0100 Subject: [PATCH 023/188] Add review panel unsupported message --- services/web/app/views/project/editor/review-panel.pug | 3 +++ .../web/public/stylesheets/app/editor/review-panel.less | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index 168947c16a..a7663c6e98 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -235,6 +235,9 @@ i.fa.fa-list span.rp-nav-label #{translate("overview")} + .rp-unsupported-msg + p UNSUPPORTED + script(type='text/ng-template', id='changeEntryTemplate') div diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 87fc3e73cf..6ba235f061 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -846,6 +846,15 @@ } } +.rp-unsupported-msg { + display: none; + .rp-unsupported & { + display: flex; + flex-direction: column; + width: @review-panel-width; + } +} + .ace-editor-wrapper { .track-changes-marker-callout { border-radius: 0; From 538aba323af87a596335e23c17891ab1fdaaaa27 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Jul 2018 14:08:11 +0100 Subject: [PATCH 024/188] Add first pass at unsupported message and style --- .../app/views/project/editor/review-panel.pug | 6 +++-- .../stylesheets/app/editor/review-panel.less | 22 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index a7663c6e98..97cbcabf69 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -235,8 +235,10 @@ i.fa.fa-list span.rp-nav-label #{translate("overview")} - .rp-unsupported-msg - p UNSUPPORTED + .rp-unsupported-msg-wrapper + .rp-unsupported-msg + i.fa.fa-5x.fa-exclamation-triangle + p Track Changes is unsupported in Rich Text mode script(type='text/ng-template', id='changeEntryTemplate') diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 6ba235f061..958d0298f0 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -846,12 +846,28 @@ } } -.rp-unsupported-msg { +.rp-unsupported-msg-wrapper { display: none; - .rp-unsupported & { + .rp-size-expanded.rp-unsupported & { + display: block; + } + + height: 100%; + + .rp-unsupported-msg { display: flex; + width: @review-panel-width - 40px; + height: 100%; flex-direction: column; - width: @review-panel-width; + justify-content: center; + align-items: center; + margin: 0 auto; + text-align: center; + + p { + font-size: 1.3em; + margin-top: 13px; + } } } From b4dd04737108f357f6089ba15f7a1b1a8ca6d5ab Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Jul 2018 14:08:43 +0100 Subject: [PATCH 025/188] Fix unnecessary right marging being applied to review panel when rich text is open --- .../web/public/coffee/ide/editor/directives/aceEditor.coffee | 1 + .../ide/review-panel/controllers/ReviewPanelController.coffee | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 33fac29f32..a78df6a10a 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -521,6 +521,7 @@ define [ detachFromAce(scope.sharejsDoc) session = editor.getSession() session?.destroy() + scope.eventsBridge.emit "aceScrollbarVisibilityChanged", false, 0 scope.$emit "#{scope.name}:inited", editor diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 430c5cd246..2eabee764c 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -164,7 +164,7 @@ define [ updateScrollbar() updateScrollbar = () -> - if scrollbar.isVisible and $scope.reviewPanel.subView == $scope.SubViews.CUR_FILE + if scrollbar.isVisible and $scope.reviewPanel.subView == $scope.SubViews.CUR_FILE and !$scope.editor.showRichText $reviewPanelEl.css "right", "#{ scrollbar.scrollbarWidth }px" else $reviewPanelEl.css "right", "0" From b1a47ddae4334399717712002024b65d373ef127 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Jul 2018 14:50:55 +0100 Subject: [PATCH 026/188] Improve copy --- services/web/app/views/project/editor/review-panel.pug | 4 +++- services/web/public/stylesheets/app/editor/review-panel.less | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index 97cbcabf69..c170701c79 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -238,7 +238,9 @@ .rp-unsupported-msg-wrapper .rp-unsupported-msg i.fa.fa-5x.fa-exclamation-triangle - p Track Changes is unsupported in Rich Text mode + p.rp-unsupported-msg-title Sorry, Track Changes is not supported in Rich Text mode (yet). + p We didn't want to delay your ability to use Rich Text mode so we've launched without support for it for now. + p We're working hard to include Track Changes as soon as possible. script(type='text/ng-template', id='changeEntryTemplate') diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 958d0298f0..800a25618f 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -864,7 +864,7 @@ margin: 0 auto; text-align: center; - p { + .rp-unsupported-msg-title { font-size: 1.3em; margin-top: 13px; } From 3806d2bda74e102ea2613f9d7b00fa688a78c78a Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Jul 2018 14:52:09 +0100 Subject: [PATCH 027/188] Add right margin to toolbar if review panel open This fixes bug where the toolbar would be measured incorrectly if the review panel was open --- .../web/public/stylesheets/app/editor/review-panel.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 800a25618f..05b45afc59 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -793,6 +793,12 @@ } } +.toolbar-editor { + .rp-size-expanded & { + margin-right: @review-panel-width; + } +} + .rp-toggle { display: inline-block; vertical-align: middle; From dc56ffb8a65f3a2f1a9bdc39199d219c2c29ea06 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Jul 2018 15:22:20 +0100 Subject: [PATCH 028/188] Improve copy --- services/web/app/views/project/editor/review-panel.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index c170701c79..5c10a8ed4d 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -239,7 +239,7 @@ .rp-unsupported-msg i.fa.fa-5x.fa-exclamation-triangle p.rp-unsupported-msg-title Sorry, Track Changes is not supported in Rich Text mode (yet). - p We didn't want to delay your ability to use Rich Text mode so we've launched without support for it for now. + p We didn't want to delay your ability to use Rich Text mode so we've launched without support for Track Changes. p We're working hard to include Track Changes as soon as possible. From 367b16a3ab27ee534b4e497c2699d6fc0dc7368f Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Wed, 11 Jul 2018 16:36:03 +0100 Subject: [PATCH 029/188] removes checking params for submit modal --- .../web/app/coffee/Features/Project/ProjectController.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index e2c71a956e..5a1bbb11f6 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -346,7 +346,6 @@ module.exports = ProjectController = useV2History: !!project.overleaf?.history?.display showRichText: req.query?.rt == 'true' showTestControls: req.query?.tc == 'true' || user.isAdmin - showPublishModal: req.query?.pm == 'true' timer.done() _buildProjectList: (allProjects, v1Projects = [])-> From 25b995d3b83730c99c7786719eb5274c89bf51d1 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 10 Jul 2018 15:37:52 +0100 Subject: [PATCH 030/188] Initialise editor mode from localStorage --- services/web/public/coffee/ide.coffee | 2 +- services/web/public/coffee/ide/editor/EditorManager.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 4b2c9ef440..7e25f9ca64 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -136,7 +136,7 @@ define [ ide.referencesSearchManager = new ReferencesManager(ide, $scope) ide.connectionManager = new ConnectionManager(ide, $scope) ide.fileTreeManager = new FileTreeManager(ide, $scope) - ide.editorManager = new EditorManager(ide, $scope) + ide.editorManager = new EditorManager(ide, $scope, localStorage) ide.onlineUsersManager = new OnlineUsersManager(ide, $scope) if window.data.useV2History ide.historyManager = new HistoryV2Manager(ide, $scope) diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index 7246e09b83..e1074fb14d 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -6,7 +6,7 @@ define [ "ide/editor/controllers/SavingNotificationController" ], (Document) -> class EditorManager - constructor: (@ide, @$scope) -> + constructor: (@ide, @$scope, localStorage) -> @$scope.editor = { sharejs_doc: null open_doc_id: null @@ -14,7 +14,7 @@ define [ opening: true trackChanges: false wantTrackChanges: false - showRichText: false + showRichText: localStorage("editor.mode.#{@$scope.project_id}") or false } @$scope.$on "entity:selected", (event, entity) => From d33c7f0149cf86740119a566f1779ece25bca994 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Jul 2018 10:44:58 +0100 Subject: [PATCH 031/188] Use string key to initialise editor mode instead of boolean --- services/web/public/coffee/ide/editor/EditorManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index e1074fb14d..26ddc7dd33 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -14,7 +14,7 @@ define [ opening: true trackChanges: false wantTrackChanges: false - showRichText: localStorage("editor.mode.#{@$scope.project_id}") or false + showRichText: localStorage("editor.mode.#{@$scope.project_id}") == 'rich-text' } @$scope.$on "entity:selected", (event, entity) => From ba6560ce048132838abc04a342f26afa167ec03f Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 12 Jul 2018 11:18:09 +0100 Subject: [PATCH 032/188] Fix bug where loading panel was showing over toolbar --- services/web/app/views/project/editor/editor.pug | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index c24d967da7..5f3215a06c 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -25,7 +25,10 @@ div.full-size( 'rp-loading-threads': reviewPanel.loadingThreads,\ }" ) - .loading-panel(ng-show="!editor.sharejs_doc || editor.opening") + .loading-panel( + ng-show="!editor.sharejs_doc || editor.opening", + style=showRichText ? "top: 32px" : "", + ) span(ng-show="editor.open_doc_id") i.fa.fa-spin.fa-refresh |   #{translate("loading")}... From 99ec132a1fc71e27fe3da7bb620ddc62b1c45ea7 Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Wed, 11 Jul 2018 10:32:24 +0100 Subject: [PATCH 033/188] update commit statuses in Jenkins pipeline --- services/web/Jenkinsfile | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index f27eb79516..cb8c4c23d0 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -6,6 +6,10 @@ pipeline { environment { HOME = "/tmp" + GIT_PROJECT = "web-sharelatex-internal" + JENKINS_WORKFLOW = "web-sharelatex-internal" + TARGET_URL = "${env.JENKINS_URL}blue/organizations/jenkins/${JENKINS_WORKFLOW}/detail/$BRANCH_NAME/$BUILD_NUMBER/pipeline" + GIT_API_URL = "https://api.github.com/repos/sharelatex/${GIT_PROJECT}/statuses/$GIT_COMMIT" } triggers { @@ -14,6 +18,20 @@ pipeline { } stages { + stage('Pre') { + steps { + withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { + sh "curl $GIT_API_URL \ + --data '{ \ + \"state\" : \"pending\", \ + \"target_url\": \"$TARGET_URL\", \ + \"description\": \"Your build is underway\", \ + \"context\": \"ci/jenkins\" }' \ + -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD" + } + } + } + stage('Install modules') { steps { sshagent (credentials: ['GIT_DEPLOY_KEY']) { @@ -146,11 +164,32 @@ pipeline { sh 'make clean_ci' } + success { + withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { + sh "curl $GIT_API_URL \ + --data '{ \ + \"state\" : \"success\", \ + \"target_url\": \"$TARGET_URL\", \ + \"description\": \"Your build succeeded!\", \ + \"context\": \"ci/jenkins\" }' \ + -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD" + } + } + failure { mail(from: "${EMAIL_ALERT_FROM}", to: "${EMAIL_ALERT_TO}", subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}", body: "Build: ${BUILD_URL}") + withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { + sh "curl $GIT_API_URL \ + --data '{ \ + \"state\" : \"failure\", \ + \"target_url\": \"$TARGET_URL\", \ + \"description\": \"Your build failed\", \ + \"context\": \"ci/jenkins\" }' \ + -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD" + } } } From abcfa0b0d2b4b8ee2ec56d363ff99d8137cf8a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Fri, 29 Jun 2018 11:22:58 +0100 Subject: [PATCH 034/188] Store manager ids in an array --- .../coffee/Features/Subscription/SubscriptionUpdater.coffee | 2 +- services/web/app/coffee/models/Subscription.coffee | 1 + .../unit/coffee/Subscription/SubscriptionUpdaterTests.coffee | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index aea2042b06..edcc58d57e 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -71,7 +71,7 @@ module.exports = SubscriptionUpdater = _createNewSubscription: (adminUser_id, callback)-> logger.log adminUser_id:adminUser_id, "creating new subscription" - subscription = new Subscription(admin_id:adminUser_id) + subscription = new Subscription(admin_id:adminUser_id, manager_ids: [adminUser_id]) subscription.freeTrial.allowed = false subscription.save (err)-> callback err, subscription diff --git a/services/web/app/coffee/models/Subscription.coffee b/services/web/app/coffee/models/Subscription.coffee index f739c14e2c..0e892a2ad6 100644 --- a/services/web/app/coffee/models/Subscription.coffee +++ b/services/web/app/coffee/models/Subscription.coffee @@ -7,6 +7,7 @@ ObjectId = Schema.ObjectId SubscriptionSchema = new Schema admin_id : {type:ObjectId, ref:'User', index: {unique: true, dropDups: true}} + manager_ids : [ type:ObjectId, ref:'User' ] member_ids : [ type:ObjectId, ref:'User' ] invited_emails: [ String ] teamInvites : [ TeamInviteSchema ] diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee index f88dac383f..a66d7f5dec 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee @@ -20,6 +20,7 @@ describe "SubscriptionUpdater", -> @subscription = subscription = _id: "111111111111111111111111" admin_id: @adminUser._id + manager_ids: [@adminUser._id] member_ids: @allUserIds save: sinon.stub().callsArgWith(0) freeTrial:{} @@ -29,6 +30,7 @@ describe "SubscriptionUpdater", -> @groupSubscription = _id: "222222222222222222222222" admin_id: @adminUser._id + manager_ids: [@adminUser._id] member_ids: @allUserIds save: sinon.stub().callsArgWith(0) freeTrial:{} @@ -40,6 +42,7 @@ describe "SubscriptionUpdater", -> @SubscriptionModel = class constructor: (opts)-> subscription.admin_id = opts.admin_id + subscription.manager_ids = [opts.admin_id] return subscription @remove: sinon.stub().yields() @SubscriptionModel.update = @updateStub @@ -151,6 +154,7 @@ describe "SubscriptionUpdater", -> it "should create a new subscription then update the subscription", (done)-> @SubscriptionUpdater._createNewSubscription @adminUser._id, => @subscription.admin_id.should.equal @adminUser._id + @subscription.manager_ids.should.deep.equal [@adminUser._id] @subscription.freeTrial.allowed.should.equal false @subscription.save.called.should.equal true done() From 3517db8348a49c117576a778483d1eb0fb7bf209 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 3 Jul 2018 15:28:00 +0100 Subject: [PATCH 035/188] SL styling adjustments. --- .../web/public/stylesheets/components/input-suggestions.less | 2 +- services/web/public/stylesheets/components/ui-select.less | 3 +++ services/web/public/stylesheets/core/_common-variables.less | 2 ++ services/web/public/stylesheets/core/ol-variables.less | 2 ++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/components/input-suggestions.less b/services/web/public/stylesheets/components/input-suggestions.less index bf2fd4a253..2424ad5cdc 100644 --- a/services/web/public/stylesheets/components/input-suggestions.less +++ b/services/web/public/stylesheets/components/input-suggestions.less @@ -10,7 +10,7 @@ .input-suggestions-shadow { background-color: @input-bg; - padding-top: 4px; + padding-top: @input-suggestion-v-offset; } .input-suggestions-shadow-existing { color: transparent; diff --git a/services/web/public/stylesheets/components/ui-select.less b/services/web/public/stylesheets/components/ui-select.less index 5513378629..f78cc2f6a3 100644 --- a/services/web/public/stylesheets/components/ui-select.less +++ b/services/web/public/stylesheets/components/ui-select.less @@ -35,12 +35,14 @@ > .btn { border-color: @input-border-focus; box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 8px fade(@input-border-focus, 60%); + padding-top: @input-suggestion-v-offset; } } > .btn { color: @input-color; background-color: @input-bg; border: 1px solid @input-border; + padding-top: @input-suggestion-v-offset; &[disabled] { cursor: not-allowed; background-color: @input-bg-disabled; @@ -53,6 +55,7 @@ .ui-select-container[tagging] { .ui-select-toggle { cursor: text; + padding-top: @input-suggestion-v-offset; > i.caret.pull-right { display: none; } diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 8dc3294a15..fefe12da73 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -995,3 +995,5 @@ @history-toolbar-bg-color : @toolbar-alt-bg-color; @history-toolbar-color : @text-color; +// Input suggestions +@input-suggestion-v-offset : 6px; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 9f4eb29c95..0e05ccdd68 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -289,6 +289,8 @@ @sys-msg-color : #FFF; @sys-msg-border : solid 1px lighten(@ol-blue, 10%); +@input-suggestion-v-offset : 4px; + //== Colors // //## Gray and brand colors for use across Bootstrap. From 1514e5e071947ed20c362504351c2937eec2177c Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 3 Jul 2018 16:47:02 +0100 Subject: [PATCH 036/188] Isolate affiliations form in a component. --- .../views/user/settings/user-affiliations.pug | 143 +++++++++--------- services/web/public/coffee/main.coffee | 1 + .../components/affiliationForm.coffee | 53 +++++++ .../UserAffiliationsController.coffee | 53 +------ 4 files changed, 134 insertions(+), 116 deletions(-) create mode 100644 services/web/public/coffee/main/affiliations/components/affiliationForm.coffee diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 6b08b09346..22498fb938 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -88,74 +88,11 @@ form.row( href ng-click="selectUniversityManually();" ) Let us know - .affiliations-form-group( - ng-if="ui.showManualUniversitySelectionUI" + affiliation-form( + affiliation-data="newAffiliation" + show-university-and-country="ui.showManualUniversitySelectionUI" + show-role-and-department="ui.isValidEmail && newAffiliation.university" ) - ui-select( - ng-model="newAffiliation.country" - ) - ui-select-match( - placeholder="Country" - ) {{ $select.selected.name }} - ui-select-choices( - repeat="country in countries | filter: $select.search" - ) - span( - ng-bind="country.name" - s) - .affiliations-form-group( - ng-if="ui.showManualUniversitySelectionUI" - ) - ui-select( - ng-model="newAffiliation.university" - ng-disabled="!newAffiliation.country" - tagging="addUniversityToSelection" - tagging-label="false" - ) - ui-select-match( - placeholder="Institution" - ) {{ $select.selected.name }} - ui-select-choices( - repeat="university in universities | filter: $select.search" - ) - span( - ng-bind="university.name" - ) - .affiliations-form-group( - ng-if="ui.isValidEmail && newAffiliation.university" - ) - ui-select( - ng-model="newAffiliation.role" - tagging - tagging-label="false" - ) - ui-select-match( - placeholder="Role" - ) {{ $select.selected }} - ui-select-choices( - repeat="role in roles | filter: $select.search" - ) - span( - ng-bind="role" - ) - - .affiliations-form-group( - ng-if="ui.isValidEmail && newAffiliation.university" - ) - ui-select( - ng-model="newAffiliation.department" - tagging - tagging-label="false" - ) - ui-select-match( - placeholder="Department" - ) {{ $select.selected }} - ui-select-choices( - repeat="department in departments | filter: $select.search" - ) - span( - ng-bind="department" - ) td button.btn.btn-primary( ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail" @@ -169,4 +106,74 @@ form.row( ) i.fa.fa-fw.fa-spin.fa-refresh |  Adding... - hr \ No newline at end of file + hr + +script(type="text/ng-template", id="affiliationFormTpl") + .affiliations-form-group( + ng-if="$ctrl.showUniversityAndCountry" + ) + ui-select( + ng-model="$ctrl.affiliationData.country" + ) + ui-select-match( + placeholder="Country" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="country in $ctrl.countries | filter: $select.search" + ) + span( + ng-bind="country.name" + ) + .affiliations-form-group( + ng-if="$ctrl.showUniversityAndCountry" + ) + ui-select( + ng-model="$ctrl.affiliationData.university" + ng-disabled="!$ctrl.affiliationData.country" + tagging="$ctrladdUniversityToSelection" + tagging-label="false" + ) + ui-select-match( + placeholder="Institution" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="university in $ctrl.universities | filter: $select.search" + ) + span( + ng-bind="university.name" + ) + .affiliations-form-group( + ng-if="$ctrl.showRoleAndDepartment" + ) + ui-select( + ng-model="$ctrl.affiliationData.role" + tagging + tagging-label="false" + ) + ui-select-match( + placeholder="Role" + ) {{ $select.selected }} + ui-select-choices( + repeat="role in $ctrl.roles | filter: $select.search" + ) + span( + ng-bind="role" + ) + + .affiliations-form-group( + ng-if="$ctrl.showRoleAndDepartment" + ) + ui-select( + ng-model="$ctrl.affiliationData.department" + tagging + tagging-label="false" + ) + ui-select-match( + placeholder="Department" + ) {{ $select.selected }} + ui-select-choices( + repeat="department in $ctrl.departments | filter: $select.search" + ) + span( + ng-bind="department" + ) diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index 80e637d705..abbc59f9e4 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -20,6 +20,7 @@ define [ "main/subscription/team-invite-controller" "main/contact-us" "main/learn" + "main/affiliations/components/affiliationForm" "main/affiliations/controllers/UserAffiliationsController" "main/affiliations/factories/UserAffiliationsDataService" "main/keys" diff --git a/services/web/public/coffee/main/affiliations/components/affiliationForm.coffee b/services/web/public/coffee/main/affiliations/components/affiliationForm.coffee new file mode 100644 index 0000000000..faec7b39c0 --- /dev/null +++ b/services/web/public/coffee/main/affiliations/components/affiliationForm.coffee @@ -0,0 +1,53 @@ +define [ + "base" +], (App) -> + affiliationFormController = ($scope, $element, $attrs, UserAffiliationsDataService) -> + ctrl = @ + ctrl.roles = [] + ctrl.departments = [] + ctrl.countries = [] + ctrl.universities = [] + _defaultDepartments = [] + + ctrl.addUniversityToSelection = (universityName) -> + { name: universityName, isUserSuggested: true } + # Populates the countries dropdown + UserAffiliationsDataService + .getCountries() + .then (countries) -> ctrl.countries = countries + # Populates the roles dropdown + UserAffiliationsDataService + .getDefaultRoleHints() + .then (roles) -> ctrl.roles = roles + # Fetches the default department hints + UserAffiliationsDataService + .getDefaultDepartmentHints() + .then (departments) -> + _defaultDepartments = departments + # Populates the universities dropdown (after selecting a country) + $scope.$watch "$ctrl.affiliationData.country", (newSelectedCountry, prevSelectedCountry) -> + if newSelectedCountry? and newSelectedCountry != prevSelectedCountry + ctrl.affiliationData.university = null + ctrl.affiliationData.role = null + ctrl.affiliationData.department = null + UserAffiliationsDataService + .getUniversitiesFromCountry(newSelectedCountry) + .then (universities) -> ctrl.universities = universities + # Populates the departments dropdown (after selecting a university) + $scope.$watch "$ctrl.affiliationData.university", (newSelectedUniversity, prevSelectedUniversity) -> + if newSelectedUniversity? and newSelectedUniversity != prevSelectedUniversity + if newSelectedUniversity.departments?.length > 0 + ctrl.departments = _.uniq newSelectedUniversity.departments + else + ctrl.departments = _defaultDepartments + + return + + App.component "affiliationForm", { + bindings: + affiliationData: "=" + showUniversityAndCountry: "<" + showRoleAndDepartment: "<" + controller: affiliationFormController + templateUrl: "affiliationFormTpl" + } \ No newline at end of file diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index a8cf710afc..3ebf25ab50 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -3,12 +3,6 @@ define [ ], (App) -> App.controller "UserAffiliationsController", ["$scope", "UserAffiliationsDataService", "$q", "_", ($scope, UserAffiliationsDataService, $q, _) -> $scope.userEmails = [] - $scope.countries = [] - $scope.universities = [] - $scope.roles = [] - $scope.departments = [] - - _defaultDepartments = [] LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/ EMAIL_REGEX = /^([A-Za-z0-9_\-\.]+)@([^\.]+)\.([A-Za-z0-9_\-\.]+)([^\.])$/ @@ -20,9 +14,6 @@ define [ else { local: null, domain: null } - $scope.addUniversityToSelection = (universityName) -> - { name: universityName, isUserSuggested: true } - $scope.getEmailSuggestion = (userInput) -> userInputLocalAndDomain = _matchLocalAndDomain(userInput) $scope.ui.isValidEmail = EMAIL_REGEX.test userInput @@ -97,11 +88,11 @@ define [ .removeUserEmail email .then () -> _getUserEmails() - $scope.getDepartments = () -> - if $scope.newAffiliation.university?.departments.length > 0 - _.uniq $scope.newAffiliation.university.departments - else - UserAffiliationsDataService.getDefaultDepartmentHints() + # $scope.getDepartments = () -> + # if $scope.newAffiliation.university?.departments.length > 0 + # _.uniq $scope.newAffiliation.university.departments + # else + # UserAffiliationsDataService.getDefaultDepartmentHints() _reset = () -> $scope.newAffiliation = @@ -129,38 +120,4 @@ define [ $scope.ui.isLoadingEmails = false _getUserEmails() - # Populates the countries dropdown - UserAffiliationsDataService - .getCountries() - .then (countries) -> $scope.countries = countries - - # Populates the roles dropdown - UserAffiliationsDataService - .getDefaultRoleHints() - .then (roles) -> $scope.roles = roles - - # Fetches the default department hints - UserAffiliationsDataService - .getDefaultDepartmentHints() - .then (departments) -> - _defaultDepartments = departments - - # Populates the universities dropdown (after selecting a country) - $scope.$watch "newAffiliation.country", (newSelectedCountry, prevSelectedCountry) -> - if newSelectedCountry? and newSelectedCountry != prevSelectedCountry - $scope.newAffiliation.university = null - $scope.newAffiliation.role = null - $scope.newAffiliation.department = null - UserAffiliationsDataService - .getUniversitiesFromCountry(newSelectedCountry) - .then (universities) -> $scope.universities = universities - - # Populates the departments dropdown (after selecting a university) - $scope.$watch "newAffiliation.university", (newSelectedUniversity, prevSelectedUniversity) -> - if newSelectedUniversity? and newSelectedUniversity != prevSelectedUniversity - if newSelectedUniversity.departments?.length > 0 - $scope.departments = _.uniq newSelectedUniversity.departments - else - $scope.departments = _defaultDepartments - ] \ No newline at end of file From a64910d4099e456cdfd435eac39109298f39307c Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 4 Jul 2018 10:56:43 +0100 Subject: [PATCH 037/188] Make sure to specify which email to set as default. --- .../affiliations/factories/UserAffiliationsDataService.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index ba558eb595..0b026de0e2 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -82,6 +82,7 @@ define [ setDefaultUserEmail = (email) -> $http.post "/user/emails/default", { + email, _csrf: window.csrfToken } From 19b57571bf1a42a196cd9adc7f15079824471c5e Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 4 Jul 2018 16:37:40 +0100 Subject: [PATCH 038/188] Basic affiliation change implementation. --- .../views/user/settings/user-affiliations.pug | 43 ++++++++++++++++--- .../components/affiliationForm.coffee | 9 ++-- .../UserAffiliationsController.coffee | 32 +++++++++++--- .../UserAffiliationsDataService.coffee | 10 +++++ 4 files changed, 78 insertions(+), 16 deletions(-) diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 22498fb938..3d12c5bfa2 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -16,12 +16,45 @@ form.row( ng-repeat="userEmail in userEmails" ) td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} + //- td {{ userEmail | json }} td - div(ng-if="userEmail.affiliation.institution") {{ userEmail.affiliation.institution.name }} - div(ng-if="userEmail.affiliation.role || userEmail.affiliation.department") - span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role }} - span(ng-if="userEmail.affiliation.role && userEmail.affiliation.department") , - span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }} + div(ng-if="userEmail.affiliation.institution") + div {{ userEmail.affiliation.institution.name }} + a( + href + ng-if="!isChangingAffiliation(userEmail.email) && !userEmail.affiliation.role && !userEmail.affiliation.department" + ng-click="changeAffiliation(userEmail);" + ) Add role and department + div( + ng-if="!isChangingAffiliation(userEmail.email) && (userEmail.affiliation.role || userEmail.affiliation.department)" + ) + span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role }} + span(ng-if="userEmail.affiliation.role && userEmail.affiliation.department") , + span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }} + | ( + a( + href + ng-click="changeAffiliation(userEmail);" + ) change + | ) + div( + ng-if="isChangingAffiliation(userEmail.email)" + ) + affiliation-form( + affiliation-data="affiliationToChange" + show-university-and-country="false" + show-role-and-department="true" + ) + .pull-right + a( + href + ng-click="saveAffiliationChange();" + ) Save + | or + a( + href + ng-click="cancelAffiliationChange();" + ) Cancel td a( href diff --git a/services/web/public/coffee/main/affiliations/components/affiliationForm.coffee b/services/web/public/coffee/main/affiliations/components/affiliationForm.coffee index faec7b39c0..1c0c7d6229 100644 --- a/services/web/public/coffee/main/affiliations/components/affiliationForm.coffee +++ b/services/web/public/coffee/main/affiliations/components/affiliationForm.coffee @@ -35,11 +35,10 @@ define [ .then (universities) -> ctrl.universities = universities # Populates the departments dropdown (after selecting a university) $scope.$watch "$ctrl.affiliationData.university", (newSelectedUniversity, prevSelectedUniversity) -> - if newSelectedUniversity? and newSelectedUniversity != prevSelectedUniversity - if newSelectedUniversity.departments?.length > 0 - ctrl.departments = _.uniq newSelectedUniversity.departments - else - ctrl.departments = _defaultDepartments + if newSelectedUniversity? and newSelectedUniversity != prevSelectedUniversity and newSelectedUniversity.departments?.length > 0 + ctrl.departments = _.uniq newSelectedUniversity.departments + else + ctrl.departments = _defaultDepartments return diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index 3ebf25ab50..f3ceca8e0d 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -46,6 +46,27 @@ define [ $scope.newAffiliation.department = null $scope.ui.showManualUniversitySelectionUI = true + $scope.changeAffiliation = (userEmail) -> + $scope.affiliationToChange.email = userEmail.email + $scope.affiliationToChange.role = userEmail.affiliation.role + $scope.affiliationToChange.department = userEmail.affiliation.department + + $scope.saveAffiliationChange = () -> + UserAffiliationsDataService + .addRoleAndDepartment( + $scope.affiliationToChange.email, + $scope.affiliationToChange.role, + $scope.affiliationToChange.department + ) + + $scope.cancelAffiliationChange = (email) -> + $scope.affiliationToChange.email = "" + $scope.affiliationToChange.role = null + $scope.affiliationToChange.department = null + + $scope.isChangingAffiliation = (email) -> + $scope.affiliationToChange.email == email + $scope.showAddEmailForm = () -> $scope.ui.showAddEmailUI = true @@ -88,12 +109,6 @@ define [ .removeUserEmail email .then () -> _getUserEmails() - # $scope.getDepartments = () -> - # if $scope.newAffiliation.university?.departments.length > 0 - # _.uniq $scope.newAffiliation.university.departments - # else - # UserAffiliationsDataService.getDefaultDepartmentHints() - _reset = () -> $scope.newAffiliation = email: "" @@ -102,12 +117,17 @@ define [ role: null department: null $scope.ui = + showChangeAffiliationUI: false showManualUniversitySelectionUI: false isLoadingEmails: false isAddingNewEmail: false showAddEmailUI: false isValidEmail: false isBlacklistedEmail: false + $scope.affiliationToChange = + email: "" + role: null + department: null _reset() # Populates the emails table diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index 0b026de0e2..6b689f5441 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -80,6 +80,15 @@ define [ _csrf: window.csrfToken } + addRoleAndDepartment = (email, role, department) -> + $http.post "/endorse", { + email, + role, + department, + _csrf: window.csrfToken + } + + setDefaultUserEmail = (email) -> $http.post "/user/emails/default", { email, @@ -105,6 +114,7 @@ define [ addUserEmail addUserAffiliationWithUnknownUniversity addUserAffiliation + addRoleAndDepartment setDefaultUserEmail removeUserEmail isDomainBlacklisted From c630c5d952f3a363e20352a58bab8a51e9c99953 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 4 Jul 2018 16:54:04 +0100 Subject: [PATCH 039/188] Preemptively remove front-end data when removing user emails. --- .../web/app/views/user/settings/user-affiliations.pug | 4 ++-- .../controllers/UserAffiliationsController.coffee | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 3d12c5bfa2..24424b495c 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -59,13 +59,13 @@ form.row( a( href ng-if="!userEmail.default" - ng-click="setDefaultUserEmail(userEmail.email)" + ng-click="setDefaultUserEmail(userEmail)" ) Make default br a( href ng-if="!userEmail.default" - ng-click="removeUserEmail(userEmail.email)" + ng-click="removeUserEmail(userEmail)" ) Remove tr.affiliations-table-highlighted-row( ng-if="ui.isLoadingEmails" diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index f3ceca8e0d..d88a15e161 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -97,16 +97,19 @@ define [ _reset() _getUserEmails() - $scope.setDefaultUserEmail = (email) -> + $scope.setDefaultUserEmail = (userEmail) -> $scope.ui.isLoadingEmails = true UserAffiliationsDataService - .setDefaultUserEmail email + .setDefaultUserEmail userEmail.email .then () -> _getUserEmails() - $scope.removeUserEmail = (email) -> + $scope.removeUserEmail = (userEmail) -> $scope.ui.isLoadingEmails = true + userEmailIdx = _.indexOf $scope.userEmails, userEmail + if userEmailIdx > -1 + $scope.userEmails.splice userEmailIdx, 1 UserAffiliationsDataService - .removeUserEmail email + .removeUserEmail userEmail.email .then () -> _getUserEmails() _reset = () -> From 4d1b6c6ba843cb70bcbc23cf7b546353d8d542ae Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 5 Jul 2018 11:36:02 +0100 Subject: [PATCH 040/188] Add error handling. --- .../views/user/settings/user-affiliations.pug | 23 +++++++++++++++++-- .../UserAffiliationsController.coffee | 22 +++++++++++++++--- .../UserAffiliationsDataService.coffee | 3 +-- .../stylesheets/app/account-settings.less | 12 ++++++++++ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 24424b495c..92f64dcdc3 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -11,7 +11,9 @@ form.row( th.affiliations-table-email Email th.affiliations-table-institution Institution and role th.affiliations-table-inline-actions - tbody + tbody( + ng-if="!ui.hasError" + ) tr( ng-repeat="userEmail in userEmails" ) @@ -73,7 +75,6 @@ form.row( td.text-center(colspan="3") i.fa.fa-fw.fa-spin.fa-refresh |  Loading... - tr.affiliations-table-highlighted-row( ng-if="!ui.showAddEmailUI && !ui.isLoadingEmails" ) @@ -139,6 +140,24 @@ form.row( ) i.fa.fa-fw.fa-spin.fa-refresh |  Adding... + tbody( + ng-if="ui.hasError" + ) + tr.affiliations-table-error-row( + ng-if="true" + ) + td.text-center(colspan="3") + div + i.fa.fa-fw.fa-exclamation-triangle + |  An error has occurred while performing your request. + div + | Click + a( + href + ng-click="acknowledgeError();" + ) here + | to reload your emails and affiliations. + hr script(type="text/ng-template", id="affiliationFormTpl") diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index d88a15e161..8b7a5c926f 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -52,12 +52,15 @@ define [ $scope.affiliationToChange.department = userEmail.affiliation.department $scope.saveAffiliationChange = () -> + $scope.ui.isLoadingEmails = true UserAffiliationsDataService .addRoleAndDepartment( $scope.affiliationToChange.email, $scope.affiliationToChange.role, $scope.affiliationToChange.department ) + .then () -> _getUserEmails() + .catch () -> $scope.ui.hasError = true $scope.cancelAffiliationChange = (email) -> $scope.affiliationToChange.email = "" @@ -93,15 +96,19 @@ define [ $scope.newAffiliation.role, $scope.newAffiliation.department ) - addEmailPromise.then () -> - _reset() - _getUserEmails() + addEmailPromise + .then () -> + _reset() + _getUserEmails() + .catch () -> + $scope.ui.hasError = true $scope.setDefaultUserEmail = (userEmail) -> $scope.ui.isLoadingEmails = true UserAffiliationsDataService .setDefaultUserEmail userEmail.email .then () -> _getUserEmails() + .catch () -> $scope.ui.hasError = true $scope.removeUserEmail = (userEmail) -> $scope.ui.isLoadingEmails = true @@ -111,6 +118,11 @@ define [ UserAffiliationsDataService .removeUserEmail userEmail.email .then () -> _getUserEmails() + .catch () -> $scope.ui.hasError = true + + $scope.acknowledgeError = () -> + _reset() + _getUserEmails() _reset = () -> $scope.newAffiliation = @@ -120,6 +132,7 @@ define [ role: null department: null $scope.ui = + hasError: false showChangeAffiliationUI: false showManualUniversitySelectionUI: false isLoadingEmails: false @@ -141,6 +154,9 @@ define [ .then (emails) -> $scope.userEmails = emails $scope.ui.isLoadingEmails = false + .catch () -> + $scope.ui.hasError = true + _getUserEmails() ] \ No newline at end of file diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index 6b689f5441..2886cf779d 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -27,7 +27,7 @@ define [ getDefaultDepartmentHints = () -> $q.resolve defaultDepartmentHints - getUserEmails = () -> + getUserEmails = () -> $http.get "/user/emails" .then (response) -> response.data @@ -88,7 +88,6 @@ define [ _csrf: window.csrfToken } - setDefaultUserEmail = (email) -> $http.post "/user/emails/default", { email, diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index f8663f7b5f..17fecb4110 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -29,6 +29,18 @@ .affiliations-table-highlighted-row { background-color: tint(@content-alt-bg-color, 6%); } + .affiliations-table-error-row { + background-color: @alert-danger-bg; + color: @alert-danger-text; + a { + color: #FFF; + font-weight: bold; + &:hover, + &:focus { + color: #FFF; + } + } + } .affiliations-form-group { margin-top: @table-cell-padding; From c817094a2d0be96e3b9552d721d4c451d5d2dc96 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 5 Jul 2018 15:31:34 +0100 Subject: [PATCH 041/188] Add i18n keys; minor style adjustments. --- .../views/user/settings/user-affiliations.pug | 52 +++++++++---------- .../stylesheets/app/account-settings.less | 16 +++--- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 92f64dcdc3..3f4378155a 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -3,13 +3,13 @@ form.row( name="affiliationsForm" ) .col-md-12 - h3 Emails and Affiliations - p.small Add additional email addresses to your account to access any upgrades your university or institution has, to make it easier for collaborators to find you, and to make sure you can recover your account. + h3 #{translate("emails_and_affiliations_title")} + p.small #{translate("emails_and_affiliations_explanation")} table.table.affiliations-table thead tr - th.affiliations-table-email Email - th.affiliations-table-institution Institution and role + th.affiliations-table-email #{translate("email")} + th.affiliations-table-institution #{translate("institution_and_role")} th.affiliations-table-inline-actions tbody( ng-if="!ui.hasError" @@ -18,7 +18,6 @@ form.row( ng-repeat="userEmail in userEmails" ) td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} - //- td {{ userEmail | json }} td div(ng-if="userEmail.affiliation.institution") div {{ userEmail.affiliation.institution.name }} @@ -26,7 +25,7 @@ form.row( href ng-if="!isChangingAffiliation(userEmail.email) && !userEmail.affiliation.role && !userEmail.affiliation.department" ng-click="changeAffiliation(userEmail);" - ) Add role and department + ) #{translate("add_role_and_department")} div( ng-if="!isChangingAffiliation(userEmail.email) && (userEmail.affiliation.role || userEmail.affiliation.department)" ) @@ -37,9 +36,9 @@ form.row( a( href ng-click="changeAffiliation(userEmail);" - ) change + ) #{translate("change")} | ) - div( + .affiliation-change-container( ng-if="isChangingAffiliation(userEmail.email)" ) affiliation-form( @@ -47,34 +46,34 @@ form.row( show-university-and-country="false" show-role-and-department="true" ) - .pull-right + .affiliation-change-actions a( href ng-click="saveAffiliationChange();" - ) Save - | or + ) #{translate("save_or_cancel-save")} + | #{translate("save_or_cancel-or" )} a( href ng-click="cancelAffiliationChange();" - ) Cancel + ) #{translate("save_or_cancel-cancel")} td a( href ng-if="!userEmail.default" ng-click="setDefaultUserEmail(userEmail)" - ) Make default + ) #{translate("make_default")} br a( href ng-if="!userEmail.default" ng-click="removeUserEmail(userEmail)" - ) Remove + ) #{translate("remove")} tr.affiliations-table-highlighted-row( ng-if="ui.isLoadingEmails" ) td.text-center(colspan="3") i.fa.fa-fw.fa-spin.fa-refresh - |  Loading... + |  #{translate("loading")}... tr.affiliations-table-highlighted-row( ng-if="!ui.showAddEmailUI && !ui.isLoadingEmails" ) @@ -82,7 +81,7 @@ form.row( a( href ng-click="showAddEmailForm()" - ) Add another email + ) #{translate("add_another_email")} tr.affiliations-table-highlighted-row( ng-if="ui.showAddEmailUI" @@ -108,20 +107,20 @@ form.row( a( href ng-click="selectUniversityManually();" - ) change + ) #{translate("change")} | ) .affiliations-table-label( ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI" - ) Start by adding your email address. + ) #{translate("start_by_adding_your_email")} .affiliations-table-label( ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI" ) - | Is your email affiliated with an institution? + | #{translate("is_email_affiliated")} br a( href ng-click="selectUniversityManually();" - ) Let us know + ) #{translate("let_us_know")} affiliation-form( affiliation-data="newAffiliation" show-university-and-country="ui.showManualUniversitySelectionUI" @@ -134,12 +133,12 @@ form.row( ) span( ng-if="!ui.isAddingNewEmail" - ) Add new email + ) #{translate("add_new_email")} span( ng-if="ui.isAddingNewEmail" ) i.fa.fa-fw.fa-spin.fa-refresh - |  Adding... + |  #{translate("adding")}... tbody( ng-if="ui.hasError" ) @@ -149,14 +148,11 @@ form.row( td.text-center(colspan="3") div i.fa.fa-fw.fa-exclamation-triangle - |  An error has occurred while performing your request. + |  #{translate("error_performing_request")} div - | Click - a( - href + button.btn.btn-xs( ng-click="acknowledgeError();" - ) here - | to reload your emails and affiliations. + ) #{translate("reload_emails_and_affiliations")} hr diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index 17fecb4110..439cf9c059 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -32,22 +32,22 @@ .affiliations-table-error-row { background-color: @alert-danger-bg; color: @alert-danger-text; - a { - color: #FFF; - font-weight: bold; - &:hover, - &:focus { - color: #FFF; - } + .btn { + margin-top: @table-cell-padding; + .button-variant(@btn-danger-color; darken(@btn-danger-bg, 8%); @btn-danger-border); } } - .affiliations-form-group { margin-top: @table-cell-padding; &:first-child { margin-top: 0; } } + .affiliation-change-container, + .affiliation-change-actions { + margin-top: @table-cell-padding; + } + .affiliations-table-label { padding-top: 4px; } From 85cf518457acee453f2b382ace43648b2b88ecfc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 5 Jul 2018 17:26:12 +0100 Subject: [PATCH 042/188] Avoid showing the email input when viewing the full affiliations UI; disable affiliations when using SAML and LDAP authentication strategies. --- .../app/coffee/infrastructure/Features.coffee | 2 + services/web/app/views/user/settings.pug | 41 ++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index afb2eefb9f..b17771f6f6 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -20,5 +20,7 @@ module.exports = Features = return Settings.overleaf? when 'templates' return !Settings.overleaf? + when 'affiliations' + return !(Settings.ldap? or Settings.saml?) else throw new Error("unknown feature: #{feature}") diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index a781cd708a..a43e81c12c 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -9,7 +9,7 @@ block content .page-header h1 #{translate("account_settings")} .account-settings(ng-controller="AccountSettingsController", ng-cloak) - if locals.showAffiliationsUI + if locals.showAffiliationsUI && hasFeature('affiliations') include settings/user-affiliations form-messages(for="settingsForm") @@ -22,25 +22,26 @@ block content h3 #{translate("update_account_info")} form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate) input(type="hidden", name="_csrf", value=csrfToken) - if !externalAuthenticationSystemUsed() - .form-group - label(for='email') #{translate("email")} - input.form-control( - type='email', - name='email', - placeholder="email@example.com" - required, - ng-model="email", - ng-init="email = "+JSON.stringify(user.email), - ng-model-options="{ updateOn: 'blur' }" - ) - span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty") - | #{translate("must_be_email_address")} - else - // show the email, non-editable - .form-group - label.control-label #{translate("email")} - div.form-control(readonly="true") #{user.email} + if !(locals.showAffiliationsUI && hasFeature('affiliations')) + if !externalAuthenticationSystemUsed() + .form-group + label(for='email') #{translate("email")} + input.form-control( + type='email', + name='email', + placeholder="email@example.com" + required, + ng-model="email", + ng-init="email = "+JSON.stringify(user.email), + ng-model-options="{ updateOn: 'blur' }" + ) + span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty") + | #{translate("must_be_email_address")} + else + // show the email, non-editable + .form-group + label.control-label #{translate("email")} + div.form-control(readonly="true") #{user.email} if shouldAllowEditingDetails .form-group From a759828ece8bf87e41b88ee94178645746301d2c Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 6 Jul 2018 12:03:26 +0100 Subject: [PATCH 043/188] Update feature flag for affiliations; use it in the router; minor style adjustments. --- services/web/app/coffee/infrastructure/Features.coffee | 2 +- services/web/app/coffee/router.coffee | 2 +- .../web/app/views/user/settings/user-affiliations.pug | 8 ++++---- .../factories/UserAffiliationsDataService.coffee | 2 +- services/web/public/stylesheets/app/account-settings.less | 3 +++ 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index b17771f6f6..bf7d37d773 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -21,6 +21,6 @@ module.exports = Features = when 'templates' return !Settings.overleaf? when 'affiliations' - return !(Settings.ldap? or Settings.saml?) + return settings?.apis?.v1?.url? else throw new Error("unknown feature: #{feature}") diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 2d9cce105d..4419dddac7 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -116,7 +116,7 @@ module.exports = class Router webRouter.post '/user/emails/confirm', UserEmailsController.confirm - unless Features.externalAuthenticationSystemUsed() + if Features.hasFeature 'affiliations' webRouter.post '/user/emails', AuthenticationController.requireLogin(), UserEmailsController.add diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 3f4378155a..ef4c055dc7 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -36,7 +36,7 @@ form.row( a( href ng-click="changeAffiliation(userEmail);" - ) #{translate("change")} + ) #{translate("change")} | ) .affiliation-change-container( ng-if="isChangingAffiliation(userEmail.email)" @@ -51,19 +51,19 @@ form.row( href ng-click="saveAffiliationChange();" ) #{translate("save_or_cancel-save")} - | #{translate("save_or_cancel-or" )} + |  #{translate("save_or_cancel-or" )}  a( href ng-click="cancelAffiliationChange();" ) #{translate("save_or_cancel-cancel")} td - a( + a.affiliations-table-inline-action( href ng-if="!userEmail.default" ng-click="setDefaultUserEmail(userEmail)" ) #{translate("make_default")} br - a( + a.affiliations-table-inline-action( href ng-if="!userEmail.default" ng-click="removeUserEmail(userEmail)" diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index 2886cf779d..ae043bbda9 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -81,7 +81,7 @@ define [ } addRoleAndDepartment = (email, role, department) -> - $http.post "/endorse", { + $http.post "/user/emails/endorse", { email, role, department, diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index 439cf9c059..a27f9d6b8f 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -26,6 +26,9 @@ .affiliations-table-inline-actions { width: 20%; } + .affiliations-table-inline-action { + text-transform: capitalize; + } .affiliations-table-highlighted-row { background-color: tint(@content-alt-bg-color, 6%); } From 59008b4a5161ada8f2f2fecca5cd9a830abda343 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 6 Jul 2018 13:45:34 +0100 Subject: [PATCH 044/188] Show university custom departments when changing affiliations; fix Features typo. --- services/web/app/coffee/infrastructure/Features.coffee | 2 +- .../controllers/UserAffiliationsController.coffee | 6 ++++++ .../factories/UserAffiliationsDataService.coffee | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index bf7d37d773..311d8943c9 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -21,6 +21,6 @@ module.exports = Features = when 'templates' return !Settings.overleaf? when 'affiliations' - return settings?.apis?.v1?.url? + return Settings?.apis?.v1?.url? else throw new Error("unknown feature: #{feature}") diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index 8b7a5c926f..dd23f72c0a 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -47,6 +47,10 @@ define [ $scope.ui.showManualUniversitySelectionUI = true $scope.changeAffiliation = (userEmail) -> + if userEmail.affiliation?.institution?.id? + UserAffiliationsDataService.getUniversityDetails userEmail.affiliation.institution.id + .then (universityDetails) -> $scope.affiliationToChange.university = universityDetails + $scope.affiliationToChange.email = userEmail.email $scope.affiliationToChange.role = userEmail.affiliation.role $scope.affiliationToChange.department = userEmail.affiliation.department @@ -64,6 +68,7 @@ define [ $scope.cancelAffiliationChange = (email) -> $scope.affiliationToChange.email = "" + $scope.affiliationToChange.university = null $scope.affiliationToChange.role = null $scope.affiliationToChange.department = null @@ -142,6 +147,7 @@ define [ isBlacklistedEmail: false $scope.affiliationToChange = email: "" + university: null role: null department: null _reset() diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index ae043bbda9..38e5c4e96c 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -53,6 +53,10 @@ define [ else $q.reject null + getUniversityDetails = (universityId) -> + $http.get "/institutions/list/#{ universityId }" + .then (response) -> response.data + addUserEmail = (email) -> $http.post "/user/emails", { email, @@ -110,6 +114,7 @@ define [ getUserEmails getUniversitiesFromCountry getUniversityDomainFromPartialDomainInput + getUniversityDetails addUserEmail addUserAffiliationWithUnknownUniversity addUserAffiliation From 67e2f6f9428aca78c9b46b8009b6408e6103b624 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 6 Jul 2018 14:04:38 +0100 Subject: [PATCH 045/188] Better handling of affiliation change submission. --- .../controllers/UserAffiliationsController.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index dd23f72c0a..03032c8734 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -63,8 +63,11 @@ define [ $scope.affiliationToChange.role, $scope.affiliationToChange.department ) - .then () -> _getUserEmails() - .catch () -> $scope.ui.hasError = true + .then () -> + _reset() + _getUserEmails() + .catch () -> + $scope.ui.hasError = true $scope.cancelAffiliationChange = (email) -> $scope.affiliationToChange.email = "" From 33b28db0610e6549b355c0e3987502d818ee5d3c Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jul 2018 16:39:04 +0100 Subject: [PATCH 046/188] Add backend endpoint for resending confirmation email --- .../Security/OneTimeTokenHandler.coffee | 10 ++ .../User/UserEmailsConfirmationHandler.coffee | 11 ++ .../Features/User/UserEmailsController.coffee | 13 ++ services/web/app/coffee/router.coffee | 3 + .../acceptance/coffee/UserEmailsTests.coffee | 126 ++++++++++++++++-- 5 files changed, 154 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee index 69c9f5b0e9..dfae55e7cf 100644 --- a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee +++ b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee @@ -27,6 +27,16 @@ module.exports = return callback(error) if error? callback null, token + findValidTokenFromData: (use, data, callback = (error, token) ->) -> + db.tokens.findOne { + use: use, + data: data, + expiresAt: { $gt: new Date() }, + usedAt: { $exists: false } + }, (error, token) -> + return callback(error) if error? + return callback null, token?.token + getValueFromTokenAndExpire: (use, token, callback = (error, data) ->)-> logger.log token_start: token.slice(0,8), "getting data from #{use} token" now = new Date() diff --git a/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee b/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee index dd87570450..53ae1fca2b 100644 --- a/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee @@ -23,6 +23,17 @@ module.exports = UserEmailsConfirmationHandler = confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}" EmailHandler.sendEmail emailTemplate, emailOptions, callback + resendConfirmationEmail: (user_id, email, callback = (error) ->) -> + OneTimeTokenHandler.findValidTokenFromData 'email_confirmation', { user_id, email }, (error, token) -> + return callback(error) if error? + if !token? + UserEmailsConfirmationHandler.sendConfirmationEmail user_id, email, callback + else + emailOptions = + to: email + confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}" + EmailHandler.sendEmail 'confirmEmail', emailOptions, callback + confirmEmailFromToken: (token, callback = (error) ->) -> logger.log {token_start: token.slice(0,8)}, 'confirming email from token' OneTimeTokenHandler.getValueFromTokenAndExpire 'email_confirmation', token, (error, data) -> diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index af1a355d6e..5c2fa5f587 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -61,6 +61,19 @@ module.exports = UserEmailsController = return next(error) if error? res.sendStatus 204 + resendConfirmation: (req, res, next) -> + userId = AuthenticationController.getLoggedInUserId(req) + email = EmailHelper.parseEmail(req.body.email) + return res.sendStatus 422 unless email? + UserGetter.getUserByAnyEmail email, {_id:1}, (error, user) -> + return next(error) if error? + if !user? or user?._id?.toString() != userId + logger.log {userId, email, foundUserId: user?._id}, "email doesn't match logged in user" + return res.sendStatus 422 + logger.log {userId, email}, 'resending email confirmation token' + UserEmailsConfirmationHandler.resendConfirmationEmail userId, email, (error) -> + return next(error) if error? + res.sendStatus 200 showConfirm: (req, res, next) -> res.render 'user/confirm_email', { diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 4419dddac7..72a0ddebc4 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -115,6 +115,9 @@ module.exports = class Router UserEmailsController.showConfirm webRouter.post '/user/emails/confirm', UserEmailsController.confirm + webRouter.post '/user/emails/resend_confirmation', + AuthenticationController.requireLogin(), + UserEmailsController.resendConfirmation if Features.hasFeature 'affiliations' webRouter.post '/user/emails', diff --git a/services/web/test/acceptance/coffee/UserEmailsTests.coffee b/services/web/test/acceptance/coffee/UserEmailsTests.coffee index 256c8a690f..40d0ca2244 100644 --- a/services/web/test/acceptance/coffee/UserEmailsTests.coffee +++ b/services/web/test/acceptance/coffee/UserEmailsTests.coffee @@ -17,7 +17,7 @@ describe "UserEmails", -> token = null async.series [ (cb) => - @user.request { + @user.request { method: 'POST', url: '/user/emails', json: @@ -45,7 +45,7 @@ describe "UserEmails", -> token = tokens[0].token cb() (cb) => - @user.request { + @user.request { method: 'POST', url: '/user/emails/confirm', json: @@ -80,7 +80,7 @@ describe "UserEmails", -> (cb) => @user2.login cb (cb) => # Create email for first user - @user.request { + @user.request { method: 'POST', url: '/user/emails', json: {@email} @@ -99,21 +99,21 @@ describe "UserEmails", -> cb() (cb) => # Delete the email from the first user - @user.request { + @user.request { method: 'POST', url: '/user/emails/delete', json: {@email} }, cb (cb) => # Create email for second user - @user2.request { + @user2.request { method: 'POST', url: '/user/emails', json: {@email} }, cb (cb) => # Original confirmation token should no longer work - @user.request { + @user.request { method: 'POST', url: '/user/emails/confirm', json: @@ -158,7 +158,7 @@ describe "UserEmails", -> token = null async.series [ (cb) => - @user.request { + @user.request { method: 'POST', url: '/user/emails', json: @@ -183,12 +183,12 @@ describe "UserEmails", -> db.tokens.update { token: token }, { - $set: { + $set: { expiresAt: new Date(Date.now() - 1000000) } }, cb (cb) => - @user.request { + @user.request { method: 'POST', url: '/user/emails/confirm', json: @@ -198,3 +198,111 @@ describe "UserEmails", -> expect(response.statusCode).to.equal 404 cb() ], done + + describe 'resending the confirmation', -> + it 'should resend the existing token', (done) -> + token = null + async.series [ + (cb) => + @user.request { + method: 'POST', + url: '/user/emails', + json: + email: 'reconfirmation-email@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 204 + cb() + (cb) => + db.tokens.find { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, (error, tokens) => + # There should only be one confirmation token at the moment + expect(tokens.length).to.equal 1 + expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com' + expect(tokens[0].data.user_id).to.equal @user._id + token = tokens[0].token + cb() + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/resend_confirmation', + json: + email: 'reconfirmation-email@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 200 + cb() + (cb) => + db.tokens.find { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, (error, tokens) => + # There should still only be one confirmation token + expect(tokens.length).to.equal 1 + expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com' + expect(tokens[0].data.user_id).to.equal @user._id + token = tokens[0].token + cb() + ], done + + it 'should create a new token if none exists', (done) -> + # This should only be for users that have sign up with their main + # emails before the confirmation system existed + token = null + async.series [ + (cb) => + db.tokens.remove { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/resend_confirmation', + json: + email: @user.email + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 200 + cb() + (cb) => + db.tokens.find { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, (error, tokens) => + # There should still only be one confirmation token + expect(tokens.length).to.equal 1 + expect(tokens[0].data.email).to.equal @user.email + expect(tokens[0].data.user_id).to.equal @user._id + token = tokens[0].token + cb() + ], done + + it "should not allow reconfirmation if the email doesn't match the user", (done) -> + token = null + async.series [ + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/resend_confirmation', + json: + email: 'non-matching-email@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 422 + cb() + (cb) => + db.tokens.find { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, (error, tokens) => + expect(tokens.length).to.equal 0 + cb() + ], done From d7e0b8c369fe8db986cd210495559ef781460ef9 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jul 2018 17:13:26 +0100 Subject: [PATCH 047/188] Show unconfirmed email status in UI and add link to resend confirmation --- .../views/user/settings/user-affiliations.pug | 76 ++++++++++++------- .../UserAffiliationsController.coffee | 7 ++ .../UserAffiliationsDataService.coffee | 7 ++ .../stylesheets/app/account-settings.less | 2 +- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index ef4c055dc7..2bbe3be8c4 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -17,27 +17,37 @@ form.row( tr( ng-repeat="userEmail in userEmails" ) - td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} + td + | {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} + div(ng-show="!userEmail.confirmedAt").small + strong #{translate('unconfirmed')}. + | + | #{translate('please_check_your_inbox')}. + br + a( + href, + ng-click="resendConfirmationEmail(userEmail)" + ) #{translate('resend_confirmation_email')} td div(ng-if="userEmail.affiliation.institution") div {{ userEmail.affiliation.institution.name }} - a( - href - ng-if="!isChangingAffiliation(userEmail.email) && !userEmail.affiliation.role && !userEmail.affiliation.department" - ng-click="changeAffiliation(userEmail);" - ) #{translate("add_role_and_department")} - div( + span.small + a( + href + ng-if="!isChangingAffiliation(userEmail.email) && !userEmail.affiliation.role && !userEmail.affiliation.department" + ng-click="changeAffiliation(userEmail);" + ) #{translate("add_role_and_department")} + div.small( ng-if="!isChangingAffiliation(userEmail.email) && (userEmail.affiliation.role || userEmail.affiliation.department)" ) span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role }} span(ng-if="userEmail.affiliation.role && userEmail.affiliation.department") , span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }} - | ( + br a( href ng-click="changeAffiliation(userEmail);" ) #{translate("change")} - | ) .affiliation-change-container( ng-if="isChangingAffiliation(userEmail.email)" ) @@ -46,7 +56,7 @@ form.row( show-university-and-country="false" show-role-and-department="true" ) - .affiliation-change-actions + .affiliation-change-actions.small a( href ng-click="saveAffiliationChange();" @@ -56,18 +66,28 @@ form.row( href ng-click="cancelAffiliationChange();" ) #{translate("save_or_cancel-cancel")} - td - a.affiliations-table-inline-action( - href - ng-if="!userEmail.default" + td.affiliations-table-inline-actions + // Disabled buttons don't work with tooltips, due to pointer-events: none, + // so create a wrapper for the tooltip + div( + style="display: inline-block" + tooltip=translate("please_confirm_your_email_before_making_it_default") + ng-if="!userEmail.default && !userEmail.confirmedAt" + ) + button.btn.btn-sm.btn-success.affiliations-table-inline-action( + disabled + ) #{translate("make_default")} + button.btn.btn-sm.btn-success.affiliations-table-inline-action( + ng-if="!userEmail.default && userEmail.confirmedAt" ng-click="setDefaultUserEmail(userEmail)" - ) #{translate("make_default")} - br - a.affiliations-table-inline-action( - href + ) #{translate("make_default")} + |   + button.btn.btn-sm.btn-danger.affiliations-table-inline-action( ng-if="!userEmail.default" ng-click="removeUserEmail(userEmail)" - ) #{translate("remove")} + tooltip=translate("remove") + ) + i.fa.fa-fw.fa-trash tr.affiliations-table-highlighted-row( ng-if="ui.isLoadingEmails" ) @@ -100,15 +120,17 @@ form.row( input-required="true" ) td - .affiliations-table-label( + p.affiliations-table-label( ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI" ) - | {{ newAffiliation.university.name }} ( - a( - href - ng-click="selectUniversityManually();" - ) #{translate("change")} - | ) + | {{ newAffiliation.university.name }} + span.small + | ( + a( + href + ng-click="selectUniversityManually();" + ) #{translate("change")} + | ) .affiliations-table-label( ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI" ) #{translate("start_by_adding_your_email")} @@ -127,7 +149,7 @@ form.row( show-role-and-department="ui.isValidEmail && newAffiliation.university" ) td - button.btn.btn-primary( + button.btn.btn-sm.btn-primary( ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail" ng-click="addNewEmail()" ) diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index 03032c8734..66162eda86 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -128,6 +128,13 @@ define [ .then () -> _getUserEmails() .catch () -> $scope.ui.hasError = true + $scope.resendConfirmationEmail = (userEmail) -> + $scope.ui.isLoadingEmails = true + UserAffiliationsDataService + .resendConfirmationEmail userEmail.email + .then () -> _getUserEmails() + .catch () -> $scope.ui.hasError = true + $scope.acknowledgeError = () -> _reset() _getUserEmails() diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index 38e5c4e96c..cbcadf7e67 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -104,6 +104,12 @@ define [ _csrf: window.csrfToken } + resendConfirmationEmail = (email) -> + $http.post "/user/emails/resend_confirmation", { + email, + _csrf: window.csrfToken + } + isDomainBlacklisted = (domain) -> domain.toLowerCase() of domainsBlackList @@ -121,6 +127,7 @@ define [ addRoleAndDepartment setDefaultUserEmail removeUserEmail + resendConfirmationEmail isDomainBlacklisted } ] \ No newline at end of file diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index a27f9d6b8f..d1b79b0c06 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -24,7 +24,7 @@ width: 40%; } .affiliations-table-inline-actions { - width: 20%; + text-align: right; } .affiliations-table-inline-action { text-transform: capitalize; From 5eb2410f5d8d6dc26c59573db4bacc9da283a829 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 13 Jul 2018 09:18:23 +0100 Subject: [PATCH 048/188] Add event to trigger syncToPdf so can be triggered by key bind --- .../aceEditor/cursor-position/CursorPositionManager.coffee | 3 +++ .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 2 ++ 2 files changed, 5 insertions(+) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee index 2cf025f70e..bb957f8cf6 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee @@ -37,6 +37,9 @@ define [], () -> onCursorChange: () => @emitCursorUpdateEvent() + onSyncToPdf: () => + @$scope.$emit "cursor:#{@$scope.name}:syncToPdf" + storeFirstVisibleLine: () -> if @doc_id? docPosition = @localStorage("doc.position.#{@doc_id}") || {} diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 77a94a564b..a9e95ac6ec 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -718,6 +718,8 @@ define [ .then (highlights) -> $scope.pdf.highlights = highlights + ide.$scope.$on "cursor:editor:syncToPdf", $scope.syncToPdf + $scope.syncToCode = () -> synctex .syncToCode($scope.pdf.position, includeVisualOffset: true, fromPdfPosition: true) From 46a1cdc5109c84d936f87834445d5a8b87e20d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Fri, 29 Jun 2018 17:01:34 +0100 Subject: [PATCH 049/188] Use the admin_ids array to query for subscriptions --- .../SubscriptionDomainHandler.coffee | 3 -- .../SubscriptionGroupHandler.coffee | 29 +++++++++++----- .../Subscription/SubscriptionLocator.coffee | 19 ++++++----- .../coffee/FeatureUpdaterTests.coffee | 2 ++ .../SubscriptionGroupHandlerTests.coffee | 34 +++++++++++-------- .../SubscriptionLocatorTests.coffee | 10 +++++- 6 files changed, 62 insertions(+), 35 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee index ac07350338..d32fd591d2 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee @@ -9,9 +9,6 @@ module.exports = SubscriptionDomainHandler = licence = SubscriptionDomainHandler._findDomainLicence(user.email) return licence - rejectInvitationToGroup: (user, subscription, callback)-> - removeUserFromGroup(subscription.admin_id, user._id, callback) - getDomainLicencePage: (user)-> licence = SubscriptionDomainHandler._findDomainLicence(user.email) if licence?.verifyEmail diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee index 55bd67ccc2..badcde4c36 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee @@ -46,15 +46,10 @@ module.exports = SubscriptionGroupHandler = Subscription.update {admin_id: oldId}, {admin_id: newId}, (error) -> callback(error) if error? - # Mongo won't let us pull and addToSet in the same query, so do it in - # two. Note we need to add first, since the query is based on the old user. - query = { member_ids: oldId } - addNewUserUpdate = $addToSet: { member_ids: newId } - removeOldUserUpdate = $pull: { member_ids: oldId } + replaceInArray Subscription, "manager_ids", oldId, newId, (error) -> + callback(error) if error? - Subscription.update query, addNewUserUpdate, { multi: true }, (error) -> - return callback(error) if error? - Subscription.update query, removeOldUserUpdate, { multi: true }, callback + replaceInArray Subscription, "member_ids", oldId, newId, callback getPopulatedListOfMembers: (subscriptionId, callback)-> SubscriptionLocator.getSubscription subscriptionId, (err, subscription)-> @@ -87,6 +82,24 @@ module.exports = SubscriptionGroupHandler = logger.log user_id:user_id, subscription_id:subscription_id, partOfGroup:partOfGroup, "checking if user is part of a group" callback(err, partOfGroup) +replaceInArray = (model, property, oldValue, newValue, callback) -> + logger.log "Replacing #{oldValue} with #{newValue} in #{property} of #{model}" + + # Mongo won't let us pull and addToSet in the same query, so do it in + # two. Note we need to add first, since the query is based on the old user. + query = {} + query[property] = oldValue + + setNewValue = {} + setNewValue[property] = newValue + + setOldValue = {} + setOldValue[property] = oldValue + + model.update query, { $addToSet: setNewValue }, { multi: true }, (error) -> + return callback(error) if error? + model.update query, { $pull: setOldValue }, { multi: true }, callback + buildUserViewModel = (user)-> u = email: user.email diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee index a228af7ffc..d348e2e829 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee @@ -2,7 +2,7 @@ Subscription = require('../../models/Subscription').Subscription logger = require("logger-sharelatex") ObjectId = require('mongoose').Types.ObjectId -module.exports = +module.exports = SubscriptionLocator = getUsersSubscription: (user_or_id, callback)-> if user_or_id? and user_or_id._id? @@ -15,14 +15,17 @@ module.exports = callback(err, subscription) getManagedSubscription: (managerId, callback)-> - logger.log managerId: managerId, "getting managed subscription" - Subscription.findOne admin_id: managerId, (err, subscription)-> - if subscription? - logger.log managerId: managerId, "got managed subscription" - else - err ||= new Error("No subscription found managed by user #{managerId}") + SubscriptionLocator.findManagedSubscription managerId, (err, subscription)-> + if subscription? + logger.log managerId: managerId, "got managed subscription" + else + err ||= new Error("No subscription found managed by user #{managerId}") - callback(err, subscription) + callback(err, subscription) + + findManagedSubscription: (managerId, callback)-> + logger.log managerId: managerId, "finding managed subscription" + Subscription.findOne manager_ids: managerId, callback getMemberSubscriptions: (user_or_id, callback) -> if user_or_id? and user_or_id._id? diff --git a/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee b/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee index 29c0cce6f1..d2918c1094 100644 --- a/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee +++ b/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee @@ -39,6 +39,7 @@ describe "FeatureUpdater.refreshFeatures", -> beforeEach -> Subscription.create { admin_id: @user._id + manager_ids: [@user._id] planCode: 'collaborator' customAccount: true } # returns a promise @@ -163,6 +164,7 @@ describe "FeatureUpdater.refreshFeatures", -> beforeEach (done) -> Subscription.create { admin_id: @user._id + manager_ids: [@user._id] planCode: 'professional' customAccount: true }, (error) => diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee index db8aac0375..41b543ea8e 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee @@ -16,7 +16,8 @@ describe "SubscriptionGroupHandler", -> @subscription_id = "31DSd1123D" @subscription = - admin_id:@adminUser_id + admin_id: @adminUser_id + manager_ids: [@adminUser_id] _id:@subscription_id @SubscriptionLocator = @@ -124,34 +125,37 @@ describe "SubscriptionGroupHandler", -> done() describe "replaceUserReferencesInGroups", -> - beforeEach -> + beforeEach (done)-> @oldId = "ba5eba11" @newId = "5ca1ab1e" + @Handler.replaceUserReferencesInGroups @oldId, @newId, -> + done() - it "replaces the admin_id", (done) -> - @Handler.replaceUserReferencesInGroups @oldId, @newId, (err) => - + it "replaces the admin_id", -> @Subscription.update.calledWith( { admin_id: @oldId }, { admin_id: @newId } ).should.equal true - done() - - it "replaces the member ids", (done) -> - @Handler.replaceUserReferencesInGroups @oldId, @newId, (err) => - + it "replaces the manager_ids", -> @Subscription.update.calledWith( - { member_ids: @oldId }, - { $addToSet: { member_ids: @newId } } + {manager_ids:"ba5eba11"},{$addToSet:{manager_ids:"5ca1ab1e"}},{multi:true} ).should.equal true @Subscription.update.calledWith( - { member_ids: @oldId }, - { $pull: { member_ids: @oldId } } + {manager_ids:"ba5eba11"},{$pull:{manager_ids:"ba5eba11"}},{multi:true} ).should.equal true - done() + it "replaces the member ids", -> + @Subscription.update.calledWith( + { member_ids: @oldId }, + { $addToSet: { member_ids: @newId } } + ).should.equal true + + @Subscription.update.calledWith( + { member_ids: @oldId }, + { $pull: { member_ids: @oldId } } + ).should.equal true describe "getPopulatedListOfMembers", -> beforeEach -> diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee index 4dbbba5a8c..6543ac0a8a 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee @@ -3,7 +3,7 @@ should = require('chai').should() sinon = require 'sinon' modulePath = "../../../../app/js/Features/Subscription/SubscriptionLocator" assert = require("chai").assert -ObjectId = require('mongoose').Types.ObjectId +ObjectId = require('mongoose').Types.ObjectId describe "Subscription Locator Tests", -> @@ -41,3 +41,11 @@ describe "Subscription Locator Tests", -> subscription.should.equal @subscription done() + describe "finding managed subscription", -> + + it "should query the database", (done)-> + @Subscription.findOne.callsArgWith(1, null, @subscription) + @SubscriptionLocator.getManagedSubscription @user._id, (err, subscription)=> + @Subscription.findOne.calledWith({"manager_ids":@user._id}).should.equal true + subscription.should.equal @subscription + done() From 476f4e55c39b0ee571ac2169bbfc200e8cc04b55 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 13 Jul 2018 10:42:31 +0100 Subject: [PATCH 050/188] Just generate a new token on resending confirmation email --- .../Features/Security/OneTimeTokenHandler.coffee | 10 ---------- .../User/UserEmailsConfirmationHandler.coffee | 11 ----------- .../Features/User/UserEmailsController.coffee | 2 +- .../test/acceptance/coffee/UserEmailsTests.coffee | 14 +++++--------- 4 files changed, 6 insertions(+), 31 deletions(-) diff --git a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee index dfae55e7cf..69c9f5b0e9 100644 --- a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee +++ b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee @@ -27,16 +27,6 @@ module.exports = return callback(error) if error? callback null, token - findValidTokenFromData: (use, data, callback = (error, token) ->) -> - db.tokens.findOne { - use: use, - data: data, - expiresAt: { $gt: new Date() }, - usedAt: { $exists: false } - }, (error, token) -> - return callback(error) if error? - return callback null, token?.token - getValueFromTokenAndExpire: (use, token, callback = (error, data) ->)-> logger.log token_start: token.slice(0,8), "getting data from #{use} token" now = new Date() diff --git a/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee b/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee index 53ae1fca2b..dd87570450 100644 --- a/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee @@ -23,17 +23,6 @@ module.exports = UserEmailsConfirmationHandler = confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}" EmailHandler.sendEmail emailTemplate, emailOptions, callback - resendConfirmationEmail: (user_id, email, callback = (error) ->) -> - OneTimeTokenHandler.findValidTokenFromData 'email_confirmation', { user_id, email }, (error, token) -> - return callback(error) if error? - if !token? - UserEmailsConfirmationHandler.sendConfirmationEmail user_id, email, callback - else - emailOptions = - to: email - confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}" - EmailHandler.sendEmail 'confirmEmail', emailOptions, callback - confirmEmailFromToken: (token, callback = (error) ->) -> logger.log {token_start: token.slice(0,8)}, 'confirming email from token' OneTimeTokenHandler.getValueFromTokenAndExpire 'email_confirmation', token, (error, data) -> diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index 5c2fa5f587..1afb60b99b 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -71,7 +71,7 @@ module.exports = UserEmailsController = logger.log {userId, email, foundUserId: user?._id}, "email doesn't match logged in user" return res.sendStatus 422 logger.log {userId, email}, 'resending email confirmation token' - UserEmailsConfirmationHandler.resendConfirmationEmail userId, email, (error) -> + UserEmailsConfirmationHandler.sendConfirmationEmail userId, email, (error) -> return next(error) if error? res.sendStatus 200 diff --git a/services/web/test/acceptance/coffee/UserEmailsTests.coffee b/services/web/test/acceptance/coffee/UserEmailsTests.coffee index 40d0ca2244..71837f4633 100644 --- a/services/web/test/acceptance/coffee/UserEmailsTests.coffee +++ b/services/web/test/acceptance/coffee/UserEmailsTests.coffee @@ -200,8 +200,7 @@ describe "UserEmails", -> ], done describe 'resending the confirmation', -> - it 'should resend the existing token', (done) -> - token = null + it 'should generate a new token', (done) -> async.series [ (cb) => @user.request { @@ -223,7 +222,6 @@ describe "UserEmails", -> expect(tokens.length).to.equal 1 expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com' expect(tokens[0].data.user_id).to.equal @user._id - token = tokens[0].token cb() (cb) => @user.request { @@ -241,18 +239,18 @@ describe "UserEmails", -> 'data.user_id': @user._id, usedAt: { $exists: false } }, (error, tokens) => - # There should still only be one confirmation token - expect(tokens.length).to.equal 1 + # There should be two tokens now + expect(tokens.length).to.equal 2 expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com' expect(tokens[0].data.user_id).to.equal @user._id - token = tokens[0].token + expect(tokens[1].data.email).to.equal 'reconfirmation-email@example.com' + expect(tokens[1].data.user_id).to.equal @user._id cb() ], done it 'should create a new token if none exists', (done) -> # This should only be for users that have sign up with their main # emails before the confirmation system existed - token = null async.series [ (cb) => db.tokens.remove { @@ -280,12 +278,10 @@ describe "UserEmails", -> expect(tokens.length).to.equal 1 expect(tokens[0].data.email).to.equal @user.email expect(tokens[0].data.user_id).to.equal @user._id - token = tokens[0].token cb() ], done it "should not allow reconfirmation if the email doesn't match the user", (done) -> - token = null async.series [ (cb) => @user.request { From 79ada88b139f0b1f2a4102455d41c83c374af098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Fri, 13 Jul 2018 11:11:04 +0100 Subject: [PATCH 051/188] Add team name to subscription model --- services/web/app/coffee/models/Subscription.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/models/Subscription.coffee b/services/web/app/coffee/models/Subscription.coffee index 0e892a2ad6..ea1c98e954 100644 --- a/services/web/app/coffee/models/Subscription.coffee +++ b/services/web/app/coffee/models/Subscription.coffee @@ -12,6 +12,7 @@ SubscriptionSchema = new Schema invited_emails: [ String ] teamInvites : [ TeamInviteSchema ] recurlySubscription_id : String + teamName : {type: String} planCode : {type: String} groupPlan : {type: Boolean, default: false} membersLimit: {type:Number, default:0} From 452d698a47c8ada3ed5dd770790239572885f376 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 13 Jul 2018 11:17:05 +0100 Subject: [PATCH 052/188] ng-show -> ng-if --- services/web/app/views/user/settings/user-affiliations.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 2bbe3be8c4..5f57cf1048 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -19,7 +19,7 @@ form.row( ) td | {{ userEmail.email + (userEmail.default ? ' (default)' : '') }} - div(ng-show="!userEmail.confirmedAt").small + div(ng-if="!userEmail.confirmedAt").small strong #{translate('unconfirmed')}. | | #{translate('please_check_your_inbox')}. From a238c74440aafa73d175f61bcb58ea77b791cc70 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 13 Jul 2018 11:19:49 +0100 Subject: [PATCH 053/188] Remove inline style --- services/web/app/views/user/settings/user-affiliations.pug | 3 +-- services/web/public/stylesheets/app/account-settings.less | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 5f57cf1048..7333a67a08 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -69,8 +69,7 @@ form.row( td.affiliations-table-inline-actions // Disabled buttons don't work with tooltips, due to pointer-events: none, // so create a wrapper for the tooltip - div( - style="display: inline-block" + div.affiliations-table-inline-action-disabled-wrapper( tooltip=translate("please_confirm_your_email_before_making_it_default") ng-if="!userEmail.default && !userEmail.confirmedAt" ) diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index d1b79b0c06..3b6d321925 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -29,6 +29,9 @@ .affiliations-table-inline-action { text-transform: capitalize; } + .affiliations-table-inline-action-disabled-wrapper { + display: inline-block; + } .affiliations-table-highlighted-row { background-color: tint(@content-alt-bg-color, 6%); } From 0bf807fa9f5723a9e2f49cd60a370f52b20842ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Fri, 13 Jul 2018 11:47:26 +0100 Subject: [PATCH 054/188] Remove SubscriptionLocator.getManagedSubscription It was used as a kind of access control check, but it's clearer if the check is in the only controller that actually needs it. --- .../SubscriptionGroupController.coffee | 20 ++++++++++++++----- .../Subscription/SubscriptionLocator.coffee | 9 --------- .../SubscriptionGroupControllerTests.coffee | 2 +- .../SubscriptionLocatorTests.coffee | 2 +- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee index 54c5827a20..0fb7b6e7c9 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -13,7 +13,7 @@ module.exports = adminUserId = AuthenticationController.getLoggedInUserId(req) newEmail = req.body?.email?.toLowerCase()?.trim() - SubscriptionLocator.getManagedSubscription adminUserId, (error, subscription) -> + getManagedSubscription adminUserId, (error, subscription) -> return next(error) if error? logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription" @@ -31,7 +31,7 @@ module.exports = removeUserFromGroup: (req, res, next)-> adminUserId = AuthenticationController.getLoggedInUserId(req) userToRemove_id = req.params.user_id - SubscriptionLocator.getManagedSubscription adminUserId, (error, subscription) -> + getManagedSubscription adminUserId, (error, subscription) -> return next(error) if error? logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription" SubscriptionGroupHandler.removeUserFromGroup subscription._id, userToRemove_id, (err)-> @@ -43,7 +43,7 @@ module.exports = removeSelfFromGroup: (req, res, next)-> adminUserId = req.query.admin_user_id userToRemove_id = AuthenticationController.getLoggedInUserId(req) - SubscriptionLocator.getManagedSubscription adminUserId, (error, subscription) -> + getManagedSubscription adminUserId, (error, subscription) -> return next(error) if error? logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request" SubscriptionGroupHandler.removeUserFromGroup subscription._id, userToRemove_id, (err)-> @@ -54,7 +54,7 @@ module.exports = renderSubscriptionGroupAdminPage: (req, res, next)-> user_id = AuthenticationController.getLoggedInUserId(req) - SubscriptionLocator.getManagedSubscription user_id, (error, subscription)-> + getManagedSubscription user_id, (error, subscription)-> return next(error) if error? if !subscription?.groupPlan return res.redirect("/user/subscription") @@ -67,7 +67,7 @@ module.exports = exportGroupCsv: (req, res)-> user_id = AuthenticationController.getLoggedInUserId(req) logger.log user_id: user_id, "exporting group csv" - SubscriptionLocator.getManagedSubscription user_id, (err, subscription)-> + getManagedSubscription user_id, (err, subscription)-> return next(error) if error? if !subscription.groupPlan return res.redirect("/") @@ -81,3 +81,13 @@ module.exports = ) res.contentType('text/csv') res.send(groupCsv) + + +getManagedSubscription = (managerId, callback) -> + SubscriptionLocator.findManagedSubscription managerId, (err, subscription)-> + if subscription? + logger.log managerId: managerId, "got managed subscription" + else + err ||= new Error("No subscription found managed by user #{managerId}") + + return callback(err, subscription) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee index d348e2e829..3a8d29673b 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee @@ -14,15 +14,6 @@ module.exports = SubscriptionLocator = logger.log user_id:user_id, "got users subscription" callback(err, subscription) - getManagedSubscription: (managerId, callback)-> - SubscriptionLocator.findManagedSubscription managerId, (err, subscription)-> - if subscription? - logger.log managerId: managerId, "got managed subscription" - else - err ||= new Error("No subscription found managed by user #{managerId}") - - callback(err, subscription) - findManagedSubscription: (managerId, callback)-> logger.log managerId: managerId, "finding managed subscription" Subscription.findOne manager_ids: managerId, callback diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee index 5d8cb397c0..a399496399 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee @@ -29,7 +29,7 @@ describe "SubscriptionGroupController", -> isUserPartOfGroup: sinon.stub() getPopulatedListOfMembers: sinon.stub().callsArgWith(1, null, [@user]) @SubscriptionLocator = - getManagedSubscription: sinon.stub().callsArgWith(1, null, @subscription) + findManagedSubscription: sinon.stub().callsArgWith(1, null, @subscription) @AuthenticationController = getLoggedInUserId: (req) -> req.session.user._id diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee index 6543ac0a8a..52dee42e84 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee @@ -45,7 +45,7 @@ describe "Subscription Locator Tests", -> it "should query the database", (done)-> @Subscription.findOne.callsArgWith(1, null, @subscription) - @SubscriptionLocator.getManagedSubscription @user._id, (err, subscription)=> + @SubscriptionLocator.findManagedSubscription @user._id, (err, subscription)=> @Subscription.findOne.calledWith({"manager_ids":@user._id}).should.equal true subscription.should.equal @subscription done() From b4f81082774966dea2fde8a859069a6fb081b091 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 13 Jul 2018 11:51:11 +0100 Subject: [PATCH 055/188] Move the pre-login async code into a helper function --- .../AuthenticationController.coffee | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index dfa19ed8a8..ab18b21a56 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -83,21 +83,24 @@ module.exports = AuthenticationController = return done(error) if error? if user? # async actions - UserHandler.setupLoginData(user, ()->) - LoginRateLimiter.recordSuccessfulLogin(email) - AuthenticationController._recordSuccessfulLogin(user._id) - Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip}) - Analytics.identifyUser(user._id, req.sessionID) - logger.log email: email, user_id: user._id.toString(), "successful log in" - req.session.justLoggedIn = true - # capture the request ip for use when creating the session - user._login_req_ip = req.ip + AuthenticationController._loginAsyncHandlers(req, email, user) return done(null, user) else AuthenticationController._recordFailedLogin() logger.log email: email, "failed log in" return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'}) + _loginAsyncHandlers: (req, email, user) -> + UserHandler.setupLoginData(user, ()->) + LoginRateLimiter.recordSuccessfulLogin(email) + AuthenticationController._recordSuccessfulLogin(user._id) + Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip}) + Analytics.identifyUser(user._id, req.sessionID) + logger.log email: email, user_id: user._id.toString(), "successful log in" + req.session.justLoggedIn = true + # capture the request ip for use when creating the session + user._login_req_ip = req.ip + setInSessionUser: (req, props) -> for key, value of props if req?.session?.passport?.user? From f95fb380566ffa0ecc2389b47780c0efffa19143 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 13 Jul 2018 15:13:22 +0100 Subject: [PATCH 056/188] Switch to serif font for rich text Makes rich text ressemble a manuscript more closely --- services/web/public/stylesheets/app/editor/rich-text.less | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/rich-text.less b/services/web/public/stylesheets/app/editor/rich-text.less index 825c348fbf..1e43691459 100644 --- a/services/web/public/stylesheets/app/editor/rich-text.less +++ b/services/web/public/stylesheets/app/editor/rich-text.less @@ -1,12 +1,11 @@ -@rt-font-family: 'Source Sans Pro', 'Helvetica', 'Arial', sans-serif; -// @rt-font-family-serif: 'Palatino Linotype', 'Book Antiqua', Palatino, serif; +@rt-font-family-serif: 'Palatino Linotype', 'Book Antiqua', Palatino, serif; .rich-text .CodeMirror { - font-family: @rt-font-family; + font-family: @rt-font-family-serif; font-size: 1.15em; pre { - font-family: @rt-font-family; + font-family: @rt-font-family-serif; } // TODO: Change prefix away from wl- ? From 4d611ca3995e945d76941cf9c5306fd2ffe78626 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 13 Jul 2018 15:45:45 +0100 Subject: [PATCH 057/188] Add horizontal padding to RT lines This is to emulate a manusript better --- services/web/public/stylesheets/app/editor/rich-text.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/web/public/stylesheets/app/editor/rich-text.less b/services/web/public/stylesheets/app/editor/rich-text.less index 1e43691459..12a66b4dce 100644 --- a/services/web/public/stylesheets/app/editor/rich-text.less +++ b/services/web/public/stylesheets/app/editor/rich-text.less @@ -8,6 +8,11 @@ font-family: @rt-font-family-serif; } + .CodeMirror-line { + // Add horizontal padding, to emulate a manuscript more closely + padding: 0 8%; + } + // TODO: Change prefix away from wl- ? /****************************************************************************/ From 304eac30244c17ea112c4eaf53041efc7b7ee713 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 13 Jul 2018 15:57:43 +0100 Subject: [PATCH 058/188] Deemphasise line numbers in rich text --- services/web/public/stylesheets/app/editor/rich-text.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/web/public/stylesheets/app/editor/rich-text.less b/services/web/public/stylesheets/app/editor/rich-text.less index 12a66b4dce..70d446bd97 100644 --- a/services/web/public/stylesheets/app/editor/rich-text.less +++ b/services/web/public/stylesheets/app/editor/rich-text.less @@ -13,6 +13,10 @@ padding: 0 8%; } + .CodeMirror-linenumber { + font-size: 0.8em; + } + // TODO: Change prefix away from wl- ? /****************************************************************************/ From 5013d135873e608b6f6311d95acedaa65898c0c3 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 13 Jul 2018 16:30:42 +0100 Subject: [PATCH 059/188] Reduce box-shadow slightly to look better with horizontal padding --- services/web/public/stylesheets/app/editor/rich-text.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor/rich-text.less b/services/web/public/stylesheets/app/editor/rich-text.less index 70d446bd97..d3316a079e 100644 --- a/services/web/public/stylesheets/app/editor/rich-text.less +++ b/services/web/public/stylesheets/app/editor/rich-text.less @@ -116,7 +116,7 @@ .wl-figure-wrap { padding: 10px 0; background-color: #f5f5f5; - box-shadow: 2px 2px 2px #DFDFDF; + box-shadow: 1.3px 2px 2px #DFDFDF; width: 96%; margin: 0 auto; text-align: center; From 120acf4918347731e777f3ea404cafb56244b55a Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Fri, 13 Jul 2018 15:02:52 -0500 Subject: [PATCH 060/188] Optional $ MathJax delimiter --- .../public/coffee/directives/mathjax.coffee | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/services/web/public/coffee/directives/mathjax.coffee b/services/web/public/coffee/directives/mathjax.coffee index 6d37353c07..269526287e 100644 --- a/services/web/public/coffee/directives/mathjax.coffee +++ b/services/web/public/coffee/directives/mathjax.coffee @@ -1,25 +1,27 @@ define [ "base" -], (App) -> - mathjaxConfig = - extensions: ["Safe.js"] - messageStyle: "none" - imageFont:null - "HTML-CSS": { availableFonts: ["TeX"] }, - TeX: - equationNumbers: { autoNumber: "AMS" }, - useLabelIDs: false - tex2jax: - inlineMath: [ ['$','$'], ["\\(","\\)"] ], - displayMath: [ ['$$','$$'], ["\\[","\\]"] ], - processEscapes: true - skipStartupTypeset: true - - MathJax?.Hub?.Config(mathjaxConfig); - +], (App) -> App.directive "mathjax", () -> return { link: (scope, element, attrs) -> + mathjaxConfig = + extensions: ["Safe.js"] + messageStyle: "none" + imageFont:null + "HTML-CSS": { availableFonts: ["TeX"] }, + TeX: + equationNumbers: { autoNumber: "AMS" }, + useLabelIDs: false + skipStartupTypeset: true + tex2jax: + processEscapes: true, + inlineMath: [ ["\\(","\\)"] ], + displayMath: [ ['$$','$$'], ["\\[","\\]"] ] + if attrs.delimiter != 'no-single-dollar' + mathjaxConfig.tex2jax.inlineMath.push(['$','$']); + + MathJax?.Hub?.Config(mathjaxConfig); + setTimeout () -> MathJax?.Hub?.Queue(["Typeset", MathJax?.Hub, element.get(0)]) , 0 From ea2a22905076a1426db463cc4b0204c6daadc90c Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 16 Jul 2018 11:28:04 +0100 Subject: [PATCH 061/188] Add line padding to indented padding Fixes bug where itemize and enumerate envs weren't correctly indented --- .../public/stylesheets/app/editor/rich-text.less | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/rich-text.less b/services/web/public/stylesheets/app/editor/rich-text.less index d3316a079e..96cdea9cad 100644 --- a/services/web/public/stylesheets/app/editor/rich-text.less +++ b/services/web/public/stylesheets/app/editor/rich-text.less @@ -1,4 +1,5 @@ @rt-font-family-serif: 'Palatino Linotype', 'Book Antiqua', Palatino, serif; +@rt-line-padding: 8%; .rich-text .CodeMirror { font-family: @rt-font-family-serif; @@ -10,7 +11,7 @@ .CodeMirror-line { // Add horizontal padding, to emulate a manuscript more closely - padding: 0 8%; + padding: 0 @rt-line-padding; } .CodeMirror-linenumber { @@ -41,23 +42,23 @@ /****************************************************************************/ .wl-indent-0 { - padding-left: 2.5em !important; + padding-left: calc(~"2.5em + @{rt-line-padding}") !important; } .wl-indent-1 { - padding-left: 3.5em !important; + padding-left: calc(~"3.5em + @{rt-line-padding}") !important; } .wl-indent-2 { - padding-left: 4.5em !important; + padding-left: calc(~"4.5em + @{rt-line-padding}") !important; } .wl-indent-3 { - padding-left: 5.5em !important; + padding-left: calc(~"5.5em + @{rt-line-padding}") !important; } .wl-indent-4 { - padding-left: 6.5em !important; + padding-left: calc(~"6.5em + @{rt-line-padding}") !important; } .wl-indent-env-0 { From d0f77abc70650c411e6430f9269c06ab6cfed6a5 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 16 Jul 2018 13:26:52 +0100 Subject: [PATCH 062/188] Update email in v1 when setting default in v2 --- .../app/coffee/Features/Errors/Errors.coffee | 8 ++ .../Features/User/UserEmailsController.coffee | 11 +- .../coffee/Features/User/UserUpdater.coffee | 41 ++++++ .../acceptance/coffee/UserEmailsTests.coffee | 128 ++++++++++++++++++ .../coffee/helpers/MockV1Api.coffee | 6 + .../User/UserEmailsControllerTests.coffee | 5 +- 6 files changed, 194 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index 25b3f3e06d..adf6fcaf12 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -68,6 +68,13 @@ V1ConnectionError = (message) -> return error V1ConnectionError.prototype.__proto___ = Error.prototype +UnconfirmedEmailError = (message) -> + error = new Error(message) + error.name = "UnconfirmedEmailError" + error.__proto__ = UnconfirmedEmailError.prototype + return error +UnconfirmedEmailError.prototype.__proto___ = Error.prototype + module.exports = Errors = NotFoundError: NotFoundError ServiceNotConfiguredError: ServiceNotConfiguredError @@ -79,3 +86,4 @@ module.exports = Errors = V1HistoryNotSyncedError: V1HistoryNotSyncedError ProjectHistoryDisabledError: ProjectHistoryDisabledError V1ConnectionError: V1ConnectionError + UnconfirmedEmailError: UnconfirmedEmailError diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index 1afb60b99b..32fabe4c6d 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -47,9 +47,14 @@ module.exports = UserEmailsController = email = EmailHelper.parseEmail(req.body.email) return res.sendStatus 422 unless email? - UserUpdater.setDefaultEmailAddress userId, email, (error)-> - return next(error) if error? - res.sendStatus 200 + UserUpdater.updateV1AndSetDefaultEmailAddress userId, email, (error)-> + if error? + if error instanceof Errors.UnconfirmedEmailError + return res.sendStatus 409 + else + return next(error) + else + return res.sendStatus 200 endorse: (req, res, next) -> diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 948be16d41..22ec235f91 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -8,6 +8,8 @@ UserGetter = require("./UserGetter") { addAffiliation, removeAffiliation } = require("./UserAffiliationsManager") EmailHelper = require "../Helpers/EmailHelper" Errors = require "../Errors/Errors" +Settings = require "settings-sharelatex" +request = require 'request' module.exports = UserUpdater = updateUser: (query, update, callback = (error) ->) -> @@ -106,6 +108,45 @@ module.exports = UserUpdater = return callback(new Error('Default email does not belong to user')) callback() + updateV1AndSetDefaultEmailAddress: (userId, email, callback) -> + @updateEmailAddressInV1 userId, email, (error) => + return callback(error) if error? + @setDefaultEmailAddress userId, email, callback + + updateEmailAddressInV1: (userId, newEmail, callback) -> + if !Settings.apis?.v1?.url? + return callback() + UserGetter.getUser userId, { 'overleaf.id': 1, emails: 1 }, (error, user) -> + return callback(error) if error? + return callback(new Errors.NotFoundError('no user found')) if !user? + if !user.overleaf?.id? + return callback() + newEmailIsConfirmed = false + for email in user.emails + if email.email == newEmail and email.confirmedAt? + newEmailIsConfirmed = true + break + if !newEmailIsConfirmed + return callback(new Errors.UnconfirmedEmailError("can't update v1 with unconfirmed email")) + request { + baseUrl: Settings.apis.v1.url + url: "/api/v1/sharelatex/users/#{user.overleaf.id}/email" + method: 'PUT' + auth: + user: Settings.apis.v1.user + pass: Settings.apis.v1.pass + sendImmediately: true + json: { email: newEmail }, + timeout: 5 * 1000 + }, (error, response, body) -> + if error? + error = new Errors.V1ConnectionError('No V1 connection') if error.code == 'ECONNREFUSED' + return callback(error) + if 200 <= response.statusCode < 300 + return callback() + else + return callback new Error("non-success code from v1: #{response.statusCode}") + confirmEmail: (userId, email, callback) -> email = EmailHelper.parseEmail(email) return callback(new Error('invalid email')) if !email? diff --git a/services/web/test/acceptance/coffee/UserEmailsTests.coffee b/services/web/test/acceptance/coffee/UserEmailsTests.coffee index 71837f4633..2ae078ff6c 100644 --- a/services/web/test/acceptance/coffee/UserEmailsTests.coffee +++ b/services/web/test/acceptance/coffee/UserEmailsTests.coffee @@ -302,3 +302,131 @@ describe "UserEmails", -> expect(tokens.length).to.equal 0 cb() ], done + + describe 'setting a default email', -> + it 'should update confirmed emails', (done) -> + token = null + async.series [ + (cb) => + @user.request { + method: 'POST', + url: '/user/emails', + json: + email: 'new-confirmed-default@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 204 + cb() + (cb) => + # Mark the email as confirmed + db.users.update { + 'emails.email': 'new-confirmed-default@example.com' + }, { + $set: { + 'emails.$.confirmedAt': new Date() + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/default', + json: + email: 'new-confirmed-default@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 200 + cb() + (cb) => + @user.request { url: '/user/emails', json: true }, (error, response, body) -> + expect(response.statusCode).to.equal 200 + expect(body[0].confirmedAt).to.not.exist + expect(body[0].default).to.equal false + expect(body[1].confirmedAt).to.exist + expect(body[1].default).to.equal true + cb() + ], done + + it 'should not allow changing unconfirmed emails in v1', (done) -> + token = null + async.series [ + (cb) => + db.users.update { + _id: ObjectId(@user._id) + }, { + $set: { + 'overleaf.id': 42 + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails', + json: + email: 'new-unconfirmed-default@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 204 + cb() + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/default', + json: + email: 'new-unconfirmed-default@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 409 + cb() + (cb) => + @user.request { url: '/user/emails', json: true }, (error, response, body) -> + expect(body[0].default).to.equal true + expect(body[1].default).to.equal false + cb() + ], done + + it 'should update the email in v2', (done) -> + token = null + async.series [ + (cb) => + db.users.update { + _id: ObjectId(@user._id) + }, { + $set: { + 'overleaf.id': 42 + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails', + json: + email: 'new-confirmed-default-in-v1@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 204 + cb() + (cb) => + # Mark the email as confirmed + db.users.update { + 'emails.email': 'new-confirmed-default-in-v1@example.com' + }, { + $set: { + 'emails.$.confirmedAt': new Date() + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/default', + json: + email: 'new-confirmed-default-in-v1@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 200 + cb() + ], (error) => + return done(error) if error? + expect( + MockV1Api.updateEmail.calledWith(42, 'new-confirmed-default-in-v1@example.com') + ).to.equal true + done() diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee index 7599230b09..a54d556935 100644 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee @@ -28,6 +28,8 @@ module.exports = MockV1Api = affiliations: [] + updateEmail: sinon.stub() + setAffiliations: (affiliations) -> @affiliations = affiliations run: () -> @@ -64,6 +66,10 @@ module.exports = MockV1Api = app.get '/university/domains', (req, res, next) -> res.json [] + app.put '/api/v1/sharelatex/users/:id/email', (req, res, next) => + @updateEmail parseInt(req.params.id), req.body.email + res.sendStatus 200 + app.listen 5000, (error) -> throw error if error? .on "error", (error) -> diff --git a/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee b/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee index 384623a2b5..2f67131c65 100644 --- a/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee +++ b/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee @@ -24,6 +24,7 @@ describe "UserEmailsController", -> addEmailAddress: sinon.stub() removeEmailAddress: sinon.stub() setDefaultEmailAddress: sinon.stub() + updateV1AndSetDefaultEmailAddress: sinon.stub() @EmailHelper = parseEmail: sinon.stub() @endorseAffiliation = sinon.stub().yields() @@ -126,13 +127,13 @@ describe "UserEmailsController", -> @EmailHelper.parseEmail.returns @email it 'sets default email', (done) -> - @UserUpdater.setDefaultEmailAddress.callsArgWith 2, null + @UserUpdater.updateV1AndSetDefaultEmailAddress.callsArgWith 2, null @UserEmailsController.setDefault @req, sendStatus: (code) => code.should.equal 200 assertCalledWith @EmailHelper.parseEmail, @email - assertCalledWith @UserUpdater.setDefaultEmailAddress, @user._id, @email + assertCalledWith @UserUpdater.updateV1AndSetDefaultEmailAddress, @user._id, @email done() it 'handles email parse error', (done) -> From ae440f2ee6c8cf9bbbd2f8d03e3ba76f457245fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Mon, 16 Jul 2018 14:40:53 +0100 Subject: [PATCH 063/188] Better variable name in test It should be a subscription id, instead of an admin id. The test passes anyway because the db calls are mocked, but the wrong variable name can cause confusion. --- .../coffee/Subscription/SubscriptionGroupHandlerTests.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee index db8aac0375..87ab39a6b5 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee @@ -88,8 +88,8 @@ describe "SubscriptionGroupHandler", -> done() it "should add the user to the group", (done)-> - @Handler.addUserToGroup @adminUser_id, @newEmail, (err)=> - @SubscriptionUpdater.addUserToGroup.calledWith(@adminUser_id, @user._id).should.equal true + @Handler.addUserToGroup @subscription_id, @newEmail, (err)=> + @SubscriptionUpdater.addUserToGroup.calledWith(@subscription_id, @user._id).should.equal true done() it "should not add the user to the group if the limit has been reached", (done)-> From 66b492ba5f25ab3f4e1c2a989bd23ffbb2564e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Mon, 16 Jul 2018 14:42:37 +0100 Subject: [PATCH 064/188] Use id to find subscription It's the new method signature. --- .../app/coffee/Features/Subscription/TeamInvitesHandler.coffee | 2 +- .../unit/coffee/Subscription/TeamInvitesHandlerTests.coffee | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee b/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee index c3b3b11089..7bbfd78780 100644 --- a/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee @@ -76,7 +76,7 @@ module.exports = TeamInvitesHandler = return callback(err) if err? return callback(new Errors.NotFoundError('invite not found')) unless invite? - SubscriptionUpdater.addUserToGroup subscription.admin_id, userId, (err) -> + SubscriptionUpdater.addUserToGroup subscription._id, userId, (err) -> return callback(err) if err? removeInviteFromTeam(subscription.id, invite.email, callback) diff --git a/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee index d23d1fc3a6..e6f404516e 100644 --- a/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee @@ -26,6 +26,7 @@ describe "TeamInvitesHandler", -> @subscription = { id: "55153a8014829a865bbf700d", + _id: new ObjectId("55153a8014829a865bbf700d"), admin_id: @manager.id, groupPlan: true, member_ids: [], @@ -210,7 +211,7 @@ describe "TeamInvitesHandler", -> it "adds the user to the team", (done) -> @TeamInvitesHandler.acceptInvite "dddddddd", @user.id, => - @SubscriptionUpdater.addUserToGroup.calledWith(@manager.id, @user.id).should.eq true + @SubscriptionUpdater.addUserToGroup.calledWith(@subscription._id, @user.id).should.eq true done() it "removes the invite from the subscription", (done) -> From 04bb83f3de552b5c7640df58fb33a007f61f99b3 Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Thu, 12 Jul 2018 09:28:24 +0100 Subject: [PATCH 065/188] add v2 history labels endpoints --- .../Features/History/HistoryController.coffee | 56 +++++++++++++++ services/web/app/coffee/router.coffee | 4 ++ .../test/acceptance/coffee/LabelsTests.coffee | 68 +++++++++++++++++++ .../helpers/MockProjectHistoryApi.coffee | 48 ++++++++++++- 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 services/web/test/acceptance/coffee/LabelsTests.coffee diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee index 7d9d347531..4d4cb93b34 100644 --- a/services/web/app/coffee/Features/History/HistoryController.coffee +++ b/services/web/app/coffee/Features/History/HistoryController.coffee @@ -21,6 +21,17 @@ module.exports = HistoryController = req.useProjectHistory = false next() + ensureProjectHistoryEnabled: (req, res, next) -> + {project_id} = req.params + ProjectDetailsHandler.getDetails project_id, (err, project) -> + return next(err) if err? + historyEnabled = project.overleaf?.history?.id? + if historyEnabled + next() + else + logger.log {project_id}, "project history not enabled" + res.sendStatus(404) + proxyToHistoryApi: (req, res, next = (error) ->) -> user_id = AuthenticationController.getLoggedInUserId req url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url @@ -98,3 +109,48 @@ module.exports = HistoryController = doc_id: doc._id } + getLabels: (req, res, next) -> + {project_id} = req.params + user_id = AuthenticationController.getLoggedInUserId(req) + request.get { + url: "#{settings.apis.project_history.url}/project/#{project_id}/labels" + json: true + }, (error, response, body) -> + return next(error) if error? + if 200 <= response.statusCode < 300 + res.json body + else + error = new Error("history api responded with non-success code: #{response.statusCode}") + logger.error err: error, user_id: user_id, "error getting labels from project-history" + next(error) + + createLabel: (req, res, next) -> + {project_id} = req.params + {comment, version} = req.body + user_id = AuthenticationController.getLoggedInUserId(req) + request.post { + url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels" + json: {comment, version} + }, (error, response, body) -> + return next(error) if error? + if 200 <= response.statusCode < 300 + res.json body + else + error = new Error("history api responded with non-success code: #{response.statusCode}") + logger.error err: error, user_id: user_id, "error creating label in project-history" + next(error) + + deleteLabel: (req, res, next) -> + {project_id, label_id} = req.params + user_id = AuthenticationController.getLoggedInUserId(req) + console.log "hof-debug DEL #{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}" + request.del { + url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}" + }, (error, response, body) -> + return next(error) if error? + if 200 <= response.statusCode < 300 + res.sendStatus 204 + else + error = new Error("history api responded with non-success code: #{response.statusCode}") + logger.error err: error, user_id: user_id, "error deleting label in project-history" + next(error) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 72a0ddebc4..d77af1fb96 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -239,6 +239,10 @@ module.exports = class Router webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory + webRouter.get "/project/:project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels + webRouter.post "/project/:project_id/labels", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel + webRouter.delete "/project/:project_id/labels/:label_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.ensureProjectHistoryEnabled, HistoryController.deleteLabel + webRouter.post '/project/:project_id/export/:brand_variation_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportProject webRouter.get '/project/:project_id/export/:export_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportStatus diff --git a/services/web/test/acceptance/coffee/LabelsTests.coffee b/services/web/test/acceptance/coffee/LabelsTests.coffee new file mode 100644 index 0000000000..8832f2a326 --- /dev/null +++ b/services/web/test/acceptance/coffee/LabelsTests.coffee @@ -0,0 +1,68 @@ +_ = require 'underscore' +{expect} = require 'chai' +{ObjectId} = require 'mongojs' +request = require './helpers/request' + +MockProjectHistoryApi = require './helpers/MockProjectHistoryApi' +User = require './helpers/User' + +describe 'Labels', -> + beforeEach (done) -> + @owner = new User() + @owner.login (error) => + throw error if error? + @owner.createProject 'example-project', {template: 'example'}, (error, @project_id) => + throw error if error? + done() + + afterEach -> + MockProjectHistoryApi.reset() + + it 'getting labels', (done) -> + label_id = new ObjectId().toString() + comment = 'a label comment' + version = 3 + MockProjectHistoryApi.addLabel @project_id, label_id, comment, version + + @owner.request { + method: 'GET' + url: "/project/#{@project_id}/labels" + json: true + }, (error, response, body) => + throw error if error? + expect(response.statusCode).to.equal 200 + expect(body).to.deep.equal [{ label_id, comment, version }] + done() + + it 'creating a label', (done) -> + comment = 'a label comment' + version = 3 + + @owner.request { + method: 'POST' + url: "/project/#{@project_id}/labels" + json: {comment, version} + }, (error, response, body) => + throw error if error? + expect(response.statusCode).to.equal 200 + {label_id} = body + expect( + MockProjectHistoryApi.getLabels(@project_id) + ).to.deep.equal [{label_id, comment, version} ] + done() + + it 'deleting a label', (done) -> + label_id = new ObjectId().toString() + comment = 'a label comment' + version = 3 + MockProjectHistoryApi.addLabel @project_id, label_id, comment, version + + @owner.request { + method: 'DELETE' + url: "/project/#{@project_id}/labels/#{label_id}" + json: true + }, (error, response, body) => + throw error if error? + expect(response.statusCode).to.equal 204 + expect(MockProjectHistoryApi.getLabels(@project_id)).to.deep.equal [] + done() diff --git a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee index b7df202d72..f73b6007a4 100644 --- a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee @@ -1,5 +1,8 @@ -express = require("express") +_ = require 'lodash' +express = require 'express' +bodyParser = require "body-parser" app = express() +{ObjectId} = require 'mongojs' module.exports = MockProjectHistoryApi = docs: {} @@ -8,12 +11,30 @@ module.exports = MockProjectHistoryApi = projectVersions: {} + labels: {} + addOldFile: (project_id, version, pathname, content) -> @oldFiles["#{project_id}:#{version}:#{pathname}"] = content setProjectVersion: (project_id, version) -> @projectVersions[project_id] = version + addLabel: (project_id, label_id, comment, version) -> + @labels[project_id] ?= {} + @labels[project_id][label_id] = {label_id,comment,version} + + deleteLabel: (project_id, label_id) -> + delete @labels[project_id][label_id] + + getLabels: (project_id) -> + return null unless @labels[project_id]? + _.values @labels[project_id] + + reset: () -> + @oldFiles = {} + @projectVersions = {} + @labels = {} + run: () -> app.post "/project", (req, res, next) => res.json project: id: 1 @@ -33,11 +54,34 @@ module.exports = MockProjectHistoryApi = else res.send 404 + app.get "/project/:project_id/labels", (req, res, next) => + {project_id} = req.params + labels = @getLabels project_id + if labels? + res.json labels + else + res.send 404 + + app.post "/project/:project_id/user/:user_id/labels", bodyParser.json(), (req, res, next) => + {project_id} = req.params + {comment, version} = req.body + label_id = new ObjectId().toString() + @addLabel project_id, label_id, comment, version + res.json {label_id, comment, version} + + app.delete "/project/:project_id/user/:user_id/labels/:label_id", (req, res, next) => + {project_id, label_id} = req.params + label = @labels[project_id]?[label_id] + if label? + @deleteLabel project_id, label_id + res.send 204 + else + res.send 404 + app.listen 3054, (error) -> throw error if error? .on "error", (error) -> console.error "error starting MockProjectHistoryApi:", error.message process.exit(1) - MockProjectHistoryApi.run() From f6def039bf6623ae75fed7e2736727fb32b16a33 Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Mon, 16 Jul 2018 16:01:39 +0100 Subject: [PATCH 066/188] code review feedback --- .../Features/History/HistoryController.coffee | 79 ++++++++----------- services/web/app/coffee/router.coffee | 6 +- 2 files changed, 37 insertions(+), 48 deletions(-) diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee index 4d4cb93b34..189e07b0f5 100644 --- a/services/web/app/coffee/Features/History/HistoryController.coffee +++ b/services/web/app/coffee/Features/History/HistoryController.coffee @@ -21,16 +21,12 @@ module.exports = HistoryController = req.useProjectHistory = false next() - ensureProjectHistoryEnabled: (req, res, next) -> - {project_id} = req.params - ProjectDetailsHandler.getDetails project_id, (err, project) -> - return next(err) if err? - historyEnabled = project.overleaf?.history?.id? - if historyEnabled - next() - else - logger.log {project_id}, "project history not enabled" - res.sendStatus(404) + ensureProjectHistoryEnabled: (req, res, next = (error) ->) -> + if req.useProjectHistory? + next() + else + logger.log {project_id}, "project history not enabled" + res.sendStatus(404) proxyToHistoryApi: (req, res, next = (error) ->) -> user_id = AuthenticationController.getLoggedInUserId req @@ -52,22 +48,17 @@ module.exports = HistoryController = user_id = AuthenticationController.getLoggedInUserId req url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url logger.log url: url, "proxying to history api" - request { + HistoryController._makeRequest { url: url method: req.method json: true headers: "X-User-Id": user_id - }, (error, response, body) -> + }, (error, body) -> return next(error) if error? - if 200 <= response.statusCode < 300 - HistoryManager.injectUserDetails body, (error, data) -> - return next(error) if error? - res.json data - else - error = new Error("history api responded with non-success code: #{response.statusCode}") - logger.error err: error, user_id: user_id, "error proxying request to history api" - next(error) + HistoryManager.injectUserDetails body, (error, data) -> + return next(error) if error? + res.json data buildHistoryServiceUrl: (useProjectHistory) -> # choose a history service, either document-level (trackchanges) @@ -110,47 +101,45 @@ module.exports = HistoryController = } getLabels: (req, res, next) -> - {project_id} = req.params + project_id = req.params.Project_id user_id = AuthenticationController.getLoggedInUserId(req) - request.get { + HistoryController._makeRequest { + method: "GET" url: "#{settings.apis.project_history.url}/project/#{project_id}/labels" json: true - }, (error, response, body) -> + }, (error, labels) -> return next(error) if error? - if 200 <= response.statusCode < 300 - res.json body - else - error = new Error("history api responded with non-success code: #{response.statusCode}") - logger.error err: error, user_id: user_id, "error getting labels from project-history" - next(error) + res.json labels createLabel: (req, res, next) -> - {project_id} = req.params + project_id = req.params.Project_id {comment, version} = req.body user_id = AuthenticationController.getLoggedInUserId(req) - request.post { + HistoryController._makeRequest { + method: "POST" url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels" json: {comment, version} - }, (error, response, body) -> + }, (error, label) -> return next(error) if error? - if 200 <= response.statusCode < 300 - res.json body - else - error = new Error("history api responded with non-success code: #{response.statusCode}") - logger.error err: error, user_id: user_id, "error creating label in project-history" - next(error) + res.json label deleteLabel: (req, res, next) -> - {project_id, label_id} = req.params + project_id = req.params.Project_id + label_id = req.params.label_id user_id = AuthenticationController.getLoggedInUserId(req) - console.log "hof-debug DEL #{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}" - request.del { + HistoryController._makeRequest { + method: "DELETE" url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}" - }, (error, response, body) -> + }, (error) -> return next(error) if error? + res.sendStatus 204 + + _makeRequest: (options, callback) -> + request options, (error, response, body) -> + return callback(error) if error? if 200 <= response.statusCode < 300 - res.sendStatus 204 + callback(null, body) else error = new Error("history api responded with non-success code: #{response.statusCode}") - logger.error err: error, user_id: user_id, "error deleting label in project-history" - next(error) + logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}" + callback(error) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index d77af1fb96..9ae764781c 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -239,9 +239,9 @@ module.exports = class Router webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory - webRouter.get "/project/:project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels - webRouter.post "/project/:project_id/labels", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel - webRouter.delete "/project/:project_id/labels/:label_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.ensureProjectHistoryEnabled, HistoryController.deleteLabel + webRouter.get "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels + webRouter.post "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel + webRouter.delete "/project/:Project_id/labels/:label_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.deleteLabel webRouter.post '/project/:project_id/export/:brand_variation_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportProject webRouter.get '/project/:project_id/export/:export_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportStatus From ca97698724f454792b6cdc85c597243c97fbb5bf Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 16 Jul 2018 17:25:52 +0100 Subject: [PATCH 067/188] Handle v1 response when email is already in use --- .../app/coffee/Features/Errors/Errors.coffee | 8 +++ .../Features/User/UserEmailsController.coffee | 5 ++ .../coffee/Features/User/UserUpdater.coffee | 4 +- .../acceptance/coffee/UserEmailsTests.coffee | 54 ++++++++++++++++++- .../coffee/helpers/MockV1Api.coffee | 10 +++- 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index adf6fcaf12..f33f3e523e 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -75,6 +75,13 @@ UnconfirmedEmailError = (message) -> return error UnconfirmedEmailError.prototype.__proto___ = Error.prototype +EmailExistsError = (message) -> + error = new Error(message) + error.name = "EmailExistsError" + error.__proto__ = EmailExistsError.prototype + return error +EmailExistsError.prototype.__proto___ = Error.prototype + module.exports = Errors = NotFoundError: NotFoundError ServiceNotConfiguredError: ServiceNotConfiguredError @@ -87,3 +94,4 @@ module.exports = Errors = ProjectHistoryDisabledError: ProjectHistoryDisabledError V1ConnectionError: V1ConnectionError UnconfirmedEmailError: UnconfirmedEmailError + EmailExistsError: EmailExistsError diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index 32fabe4c6d..b3bac95ca7 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -51,6 +51,11 @@ module.exports = UserEmailsController = if error? if error instanceof Errors.UnconfirmedEmailError return res.sendStatus 409 + else if error instanceof Errors.EmailExistsError + return res.status(409).json { + error: + message: "The email '#{email}' is already in use by another account" + } else return next(error) else diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 22ec235f91..2c27f5178d 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -142,7 +142,9 @@ module.exports = UserUpdater = if error? error = new Errors.V1ConnectionError('No V1 connection') if error.code == 'ECONNREFUSED' return callback(error) - if 200 <= response.statusCode < 300 + if response.statusCode == 409 # Conflict + return callback(new Errors.EmailExistsError('email exists in v1')) + else if 200 <= response.statusCode < 300 return callback() else return callback new Error("non-success code from v1: #{response.statusCode}") diff --git a/services/web/test/acceptance/coffee/UserEmailsTests.coffee b/services/web/test/acceptance/coffee/UserEmailsTests.coffee index 2ae078ff6c..2d4c437fc1 100644 --- a/services/web/test/acceptance/coffee/UserEmailsTests.coffee +++ b/services/web/test/acceptance/coffee/UserEmailsTests.coffee @@ -304,7 +304,7 @@ describe "UserEmails", -> ], done describe 'setting a default email', -> - it 'should update confirmed emails', (done) -> + it 'should update confirmed emails for users not in v1', (done) -> token = null async.series [ (cb) => @@ -384,7 +384,7 @@ describe "UserEmails", -> cb() ], done - it 'should update the email in v2', (done) -> + it 'should update the email in v1 if confirmed', (done) -> token = null async.series [ (cb) => @@ -430,3 +430,53 @@ describe "UserEmails", -> MockV1Api.updateEmail.calledWith(42, 'new-confirmed-default-in-v1@example.com') ).to.equal true done() + + it 'should return an error if the email exists in v1', (done) -> + MockV1Api.existingEmails.push 'exists-in-v1@example.com' + async.series [ + (cb) => + db.users.update { + _id: ObjectId(@user._id) + }, { + $set: { + 'overleaf.id': 42 + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails', + json: + email: 'exists-in-v1@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 204 + cb() + (cb) => + # Mark the email as confirmed + db.users.update { + 'emails.email': 'exists-in-v1@example.com' + }, { + $set: { + 'emails.$.confirmedAt': new Date() + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/default', + json: + email: 'exists-in-v1@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 409 + expect(body.error).to.deep.equal { + message: "The email 'exists-in-v1@example.com' is already in use by another account" + } + cb() + (cb) => + @user.request { url: '/user/emails', json: true }, (error, response, body) -> + expect(body[0].default).to.equal true + expect(body[1].default).to.equal false + cb() + ], done \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee index a54d556935..ef80ac8b23 100644 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee @@ -30,6 +30,8 @@ module.exports = MockV1Api = updateEmail: sinon.stub() + existingEmails: [] + setAffiliations: (affiliations) -> @affiliations = affiliations run: () -> @@ -67,8 +69,12 @@ module.exports = MockV1Api = res.json [] app.put '/api/v1/sharelatex/users/:id/email', (req, res, next) => - @updateEmail parseInt(req.params.id), req.body.email - res.sendStatus 200 + { email } = req.body + if email in @existingEmails + return res.sendStatus 409 + else + @updateEmail parseInt(req.params.id), email + return res.sendStatus 200 app.listen 5000, (error) -> throw error if error? From 2c25cbaf2538060382ce8dace8e5b19587007d89 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jul 2018 11:12:09 +0100 Subject: [PATCH 068/188] Update error handling on backend --- .../Features/User/UserController.coffee | 3 ++- .../Features/User/UserEmailsController.coffee | 25 +++++++++++-------- .../coffee/Features/User/UserGetter.coffee | 3 ++- .../coffee/Features/User/UserUpdater.coffee | 4 ++- .../acceptance/coffee/UserEmailsTests.coffee | 4 +-- .../coffee/helpers/MockV1Api.coffee | 2 +- .../coffee/User/UserControllerTests.coffee | 2 ++ .../unit/coffee/User/UserGetterTests.coffee | 3 +++ 8 files changed, 30 insertions(+), 16 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 9b102d13dd..823919f999 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -13,6 +13,7 @@ UserSessionsManager = require("./UserSessionsManager") UserUpdater = require("./UserUpdater") SudoModeHandler = require('../SudoMode/SudoModeHandler') settings = require "settings-sharelatex" +Errors = require "../Errors/Errors" module.exports = UserController = @@ -100,7 +101,7 @@ module.exports = UserController = UserUpdater.changeEmailAddress user_id, newEmail, (err)-> if err? logger.err err:err, user_id:user_id, newEmail:newEmail, "problem updaing users email address" - if err.message == "alread_exists" + if err instanceof Errors.EmailExistsError message = req.i18n.translate("email_already_registered") else message = req.i18n.translate("problem_changing_email_address") diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index b3bac95ca7..780960c3e2 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -26,7 +26,8 @@ module.exports = UserEmailsController = role: req.body.role department: req.body.department UserUpdater.addEmailAddress userId, email, affiliationOptions, (error)-> - return next(error) if error? + if error? + return UserEmailsController._handleEmailError error, req, res, next UserEmailsConfirmationHandler.sendConfirmationEmail userId, email, (err) -> return next(error) if error? res.sendStatus 204 @@ -49,15 +50,7 @@ module.exports = UserEmailsController = UserUpdater.updateV1AndSetDefaultEmailAddress userId, email, (error)-> if error? - if error instanceof Errors.UnconfirmedEmailError - return res.sendStatus 409 - else if error instanceof Errors.EmailExistsError - return res.status(409).json { - error: - message: "The email '#{email}' is already in use by another account" - } - else - return next(error) + return UserEmailsController._handleEmailError error, req, res, next else return res.sendStatus 200 @@ -105,3 +98,15 @@ module.exports = UserEmailsController = next(error) else res.sendStatus 200 + + _handleEmailError: (error, req, res, next) -> + if error instanceof Errors.UnconfirmedEmailError + return res.status(409).json { + message: 'email must be confirmed' + } + else if error instanceof Errors.EmailExistsError + return res.status(409).json { + message: req.i18n.translate("email_already_registered") + } + else + return next(error) \ No newline at end of file diff --git a/services/web/app/coffee/Features/User/UserGetter.coffee b/services/web/app/coffee/Features/User/UserGetter.coffee index bdcdd24482..13b81b399c 100644 --- a/services/web/app/coffee/Features/User/UserGetter.coffee +++ b/services/web/app/coffee/Features/User/UserGetter.coffee @@ -4,6 +4,7 @@ logger = require('logger-sharelatex') db = mongojs.db ObjectId = mongojs.ObjectId { getAffiliations } = require("./UserAffiliationsManager") +Errors = require("../Errors/Errors") module.exports = UserGetter = getUser: (query, projection, callback = (error, user) ->) -> @@ -77,7 +78,7 @@ module.exports = UserGetter = # check for duplicate email address. This is also enforced at the DB level ensureUniqueEmailAddress: (newEmail, callback) -> @getUserByAnyEmail newEmail, (error, user) -> - return callback(message: 'alread_exists') if user? + return callback(new Errors.EmailExistsError('alread_exists')) if user? callback(error) decorateFullEmails = (defaultEmail, emailsData, affiliationsData) -> diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 2c27f5178d..5e9058eb9b 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -136,7 +136,9 @@ module.exports = UserUpdater = user: Settings.apis.v1.user pass: Settings.apis.v1.pass sendImmediately: true - json: { email: newEmail }, + json: + user: + email: newEmail timeout: 5 * 1000 }, (error, response, body) -> if error? diff --git a/services/web/test/acceptance/coffee/UserEmailsTests.coffee b/services/web/test/acceptance/coffee/UserEmailsTests.coffee index 2d4c437fc1..3e8ffbd777 100644 --- a/services/web/test/acceptance/coffee/UserEmailsTests.coffee +++ b/services/web/test/acceptance/coffee/UserEmailsTests.coffee @@ -470,8 +470,8 @@ describe "UserEmails", -> }, (error, response, body) => return done(error) if error? expect(response.statusCode).to.equal 409 - expect(body.error).to.deep.equal { - message: "The email 'exists-in-v1@example.com' is already in use by another account" + expect(body).to.deep.equal { + message: "This email is already registered" } cb() (cb) => diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee index ef80ac8b23..eeafa6a44b 100644 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee @@ -69,7 +69,7 @@ module.exports = MockV1Api = res.json [] app.put '/api/v1/sharelatex/users/:id/email', (req, res, next) => - { email } = req.body + { email } = req.body?.user if email in @existingEmails return res.sendStatus 409 else diff --git a/services/web/test/unit/coffee/User/UserControllerTests.coffee b/services/web/test/unit/coffee/User/UserControllerTests.coffee index 26f345db39..d589852a37 100644 --- a/services/web/test/unit/coffee/User/UserControllerTests.coffee +++ b/services/web/test/unit/coffee/User/UserControllerTests.coffee @@ -9,6 +9,7 @@ MockResponse = require "../helpers/MockResponse" MockRequest = require "../helpers/MockRequest" ObjectId = require("mongojs").ObjectId assert = require("assert") +Errors = require "../../../../app/js/Features/Errors/Errors" describe "UserController", -> beforeEach -> @@ -81,6 +82,7 @@ describe "UserController", -> log:-> err:-> "metrics-sharelatex": inc:-> + "../Errors/Errors": Errors @res = send: sinon.stub() diff --git a/services/web/test/unit/coffee/User/UserGetterTests.coffee b/services/web/test/unit/coffee/User/UserGetterTests.coffee index 50ab2261f1..c754b9830b 100644 --- a/services/web/test/unit/coffee/User/UserGetterTests.coffee +++ b/services/web/test/unit/coffee/User/UserGetterTests.coffee @@ -5,6 +5,7 @@ path = require('path') sinon = require('sinon') modulePath = path.join __dirname, "../../../../app/js/Features/User/UserGetter" expect = require("chai").expect +Errors = require "../../../../app/js/Features/Errors/Errors" describe "UserGetter", -> @@ -30,6 +31,7 @@ describe "UserGetter", -> 'settings-sharelatex': settings './UserAffiliationsManager': getAffiliations: @getAffiliations + "../Errors/Errors": Errors describe "getUser", -> it "should get user", (done)-> @@ -150,6 +152,7 @@ describe "UserGetter", -> @UserGetter.getUserByAnyEmail.callsArgWith(1, null, @fakeUser) @UserGetter.ensureUniqueEmailAddress @newEmail, (err)=> should.exist(err) + expect(err).to.be.an.instanceof(Errors.EmailExistsError) err.message.should.equal 'alread_exists' done() From 4435eab423ba3a498bf0a69596adaa4f225dc231 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jul 2018 11:12:47 +0100 Subject: [PATCH 069/188] Update affiliations UI to show specific errors for conflicting emails --- .../views/user/settings/user-affiliations.pug | 57 ++++---- .../UserAffiliationsController.coffee | 138 +++++++++++------- 2 files changed, 107 insertions(+), 88 deletions(-) diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug index 7333a67a08..32b7d8fdf3 100644 --- a/services/web/app/views/user/settings/user-affiliations.pug +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -11,9 +11,7 @@ form.row( th.affiliations-table-email #{translate("email")} th.affiliations-table-institution #{translate("institution_and_role")} th.affiliations-table-inline-actions - tbody( - ng-if="!ui.hasError" - ) + tbody tr( ng-repeat="userEmail in userEmails" ) @@ -59,7 +57,7 @@ form.row( .affiliation-change-actions.small a( href - ng-click="saveAffiliationChange();" + ng-click="saveAffiliationChange(userEmail);" ) #{translate("save_or_cancel-save")} |  #{translate("save_or_cancel-or" )}  a( @@ -71,30 +69,26 @@ form.row( // so create a wrapper for the tooltip div.affiliations-table-inline-action-disabled-wrapper( tooltip=translate("please_confirm_your_email_before_making_it_default") - ng-if="!userEmail.default && !userEmail.confirmedAt" + tooltip-enable="!ui.isMakingRequest" + ng-if="!userEmail.default && (!userEmail.confirmedAt || ui.isMakingRequest)" ) button.btn.btn-sm.btn-success.affiliations-table-inline-action( disabled ) #{translate("make_default")} button.btn.btn-sm.btn-success.affiliations-table-inline-action( - ng-if="!userEmail.default && userEmail.confirmedAt" + ng-if="!userEmail.default && (userEmail.confirmedAt && !ui.isMakingRequest)" ng-click="setDefaultUserEmail(userEmail)" ) #{translate("make_default")} |   button.btn.btn-sm.btn-danger.affiliations-table-inline-action( ng-if="!userEmail.default" ng-click="removeUserEmail(userEmail)" + ng-disabled="ui.isMakingRequest" tooltip=translate("remove") ) i.fa.fa-fw.fa-trash tr.affiliations-table-highlighted-row( - ng-if="ui.isLoadingEmails" - ) - td.text-center(colspan="3") - i.fa.fa-fw.fa-spin.fa-refresh - |  #{translate("loading")}... - tr.affiliations-table-highlighted-row( - ng-if="!ui.showAddEmailUI && !ui.isLoadingEmails" + ng-if="!ui.showAddEmailUI && !ui.isMakingRequest" ) td(colspan="3") a( @@ -103,7 +97,7 @@ form.row( ) #{translate("add_another_email")} tr.affiliations-table-highlighted-row( - ng-if="ui.showAddEmailUI" + ng-if="ui.showAddEmailUI && !ui.isLoadingEmails" ) td .affiliations-form-group @@ -149,31 +143,30 @@ form.row( ) td button.btn.btn-sm.btn-primary( - ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail" + ng-disabled="affiliationsForm.$invalid || ui.isMakingRequest" ng-click="addNewEmail()" ) - span( - ng-if="!ui.isAddingNewEmail" - ) #{translate("add_new_email")} - span( - ng-if="ui.isAddingNewEmail" - ) - i.fa.fa-fw.fa-spin.fa-refresh - |  #{translate("adding")}... - tbody( - ng-if="ui.hasError" - ) + | #{translate("add_new_email")} + tr.affiliations-table-highlighted-row( + ng-if="ui.isMakingRequest" + ) + td.text-center(colspan="3", ng-if="ui.isLoadingEmails") + i.fa.fa-fw.fa-spin.fa-refresh + |  #{translate("loading")}... + td.text-center(colspan="3", ng-if="ui.isResendingConfirmation") + i.fa.fa-fw.fa-spin.fa-refresh + |  #{translate("sending")}... + td.text-center(colspan="3", ng-if="!ui.isLoadingEmails && !ui.isResendingConfirmation") + i.fa.fa-fw.fa-spin.fa-refresh + |  #{translate("saving")}... tr.affiliations-table-error-row( - ng-if="true" + ng-if="ui.hasError" ) td.text-center(colspan="3") div i.fa.fa-fw.fa-exclamation-triangle - |  #{translate("error_performing_request")} - div - button.btn.btn-xs( - ng-click="acknowledgeError();" - ) #{translate("reload_emails_and_affiliations")} + span(ng-if="!ui.errorMessage")  #{translate("error_performing_request")} + span(ng-if="ui.errorMessage")  {{ui.errorMessage}} hr diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index 66162eda86..14ea1a7d29 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -55,25 +55,23 @@ define [ $scope.affiliationToChange.role = userEmail.affiliation.role $scope.affiliationToChange.department = userEmail.affiliation.department - $scope.saveAffiliationChange = () -> - $scope.ui.isLoadingEmails = true - UserAffiliationsDataService - .addRoleAndDepartment( - $scope.affiliationToChange.email, - $scope.affiliationToChange.role, - $scope.affiliationToChange.department - ) + $scope.saveAffiliationChange = (userEmail) -> + userEmail.affiliation.role = $scope.affiliationToChange.role + userEmail.affiliation.department = $scope.affiliationToChange.department + _resetAffiliationToChange() + _monitorRequest( + UserAffiliationsDataService + .addRoleAndDepartment( + userEmail.email, + userEmail.affiliation.role, + userEmail.affiliation.department + ) + ) .then () -> - _reset() - _getUserEmails() - .catch () -> - $scope.ui.hasError = true + setTimeout () -> _getUserEmails() $scope.cancelAffiliationChange = (email) -> - $scope.affiliationToChange.email = "" - $scope.affiliationToChange.university = null - $scope.affiliationToChange.role = null - $scope.affiliationToChange.department = null + _resetAffiliationToChange() $scope.isChangingAffiliation = (email) -> $scope.affiliationToChange.email == email @@ -82,7 +80,6 @@ define [ $scope.ui.showAddEmailUI = true $scope.addNewEmail = () -> - $scope.ui.isAddingNewEmail = true if !$scope.newAffiliation.university? addEmailPromise = UserAffiliationsDataService .addUserEmail $scope.newAffiliation.email @@ -104,75 +101,104 @@ define [ $scope.newAffiliation.role, $scope.newAffiliation.department ) - addEmailPromise - .then () -> - _reset() - _getUserEmails() - .catch () -> - $scope.ui.hasError = true + + $scope.ui.isAddingNewEmail = true + $scope.ui.showAddEmailUI = false + _monitorRequest(addEmailPromise) + .then () -> + _resetNewAffiliation() + _resetAddingEmail() + setTimeout () -> _getUserEmails() + .finally () -> + $scope.ui.isAddingNewEmail = false $scope.setDefaultUserEmail = (userEmail) -> - $scope.ui.isLoadingEmails = true - UserAffiliationsDataService - .setDefaultUserEmail userEmail.email - .then () -> _getUserEmails() - .catch () -> $scope.ui.hasError = true + _monitorRequest( + UserAffiliationsDataService + .setDefaultUserEmail userEmail.email + ) + .then () -> + for email in $scope.userEmails or [] + email.default = false + userEmail.default = true $scope.removeUserEmail = (userEmail) -> - $scope.ui.isLoadingEmails = true - userEmailIdx = _.indexOf $scope.userEmails, userEmail - if userEmailIdx > -1 - $scope.userEmails.splice userEmailIdx, 1 - UserAffiliationsDataService - .removeUserEmail userEmail.email - .then () -> _getUserEmails() - .catch () -> $scope.ui.hasError = true + $scope.userEmails = $scope.userEmails.filter (ue) -> ue != userEmail + _monitorRequest( + UserAffiliationsDataService + .removeUserEmail userEmail.email + ) $scope.resendConfirmationEmail = (userEmail) -> - $scope.ui.isLoadingEmails = true - UserAffiliationsDataService - .resendConfirmationEmail userEmail.email - .then () -> _getUserEmails() - .catch () -> $scope.ui.hasError = true + $scope.ui.isResendingConfirmation = true + _monitorRequest( + UserAffiliationsDataService + .resendConfirmationEmail userEmail.email + ) + .finally () -> + $scope.ui.isResendingConfirmation = false $scope.acknowledgeError = () -> _reset() _getUserEmails() - _reset = () -> + _resetAffiliationToChange = () -> + $scope.affiliationToChange = + email: "" + university: null + role: null + department: null + + _resetNewAffiliation = () -> $scope.newAffiliation = email: "" country: null university: null role: null department: null + + _resetAddingEmail = () -> + $scope.ui.showAddEmailUI = false + $scope.ui.isValidEmail = false + $scope.ui.isBlacklistedEmail = false + $scope.ui.showManualUniversitySelectionUI = false + + _reset = () -> $scope.ui = hasError: false + errorMessage: "" showChangeAffiliationUI: false - showManualUniversitySelectionUI: false + isMakingRequest: false isLoadingEmails: false isAddingNewEmail: false - showAddEmailUI: false - isValidEmail: false - isBlacklistedEmail: false - $scope.affiliationToChange = - email: "" - university: null - role: null - department: null + isResendingConfirmation: false + _resetAffiliationToChange() + _resetNewAffiliation() + _resetAddingEmail() _reset() + _monitorRequest = (promise) -> + $scope.ui.hasError = false + $scope.ui.isMakingRequest = true + promise + .catch (response) -> + $scope.ui.hasError = true + $scope.ui.errorMessage = response?.data?.message + .finally () -> + $scope.ui.isMakingRequest = false + return promise + # Populates the emails table _getUserEmails = () -> $scope.ui.isLoadingEmails = true - UserAffiliationsDataService - .getUserEmails() + _monitorRequest( + UserAffiliationsDataService + .getUserEmails() + ) .then (emails) -> $scope.userEmails = emails + .finally () -> $scope.ui.isLoadingEmails = false - .catch () -> - $scope.ui.hasError = true - _getUserEmails() ] \ No newline at end of file From c2777918fa6998a69a7070f301cbe49f9abc0248 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jul 2018 11:26:07 +0100 Subject: [PATCH 070/188] Add some missing unit test mocks --- services/web/test/unit/coffee/User/UserUpdaterTests.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee index 3820e2a448..374fdd222d 100644 --- a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee +++ b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee @@ -29,6 +29,8 @@ describe "UserUpdater", -> removeAffiliation: @removeAffiliation "../../infrastructure/mongojs":@mongojs "metrics-sharelatex": timeAsyncMethod: sinon.stub() + "settings-sharelatex": @settings = {} + "request": @request = {} @stubbedUser = _id: "3131231" From 8f4c5d6122dcb65587bd01ed642cc6ff2057da8d Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 17 Jul 2018 11:53:42 +0100 Subject: [PATCH 071/188] Fix left padding on empty nested list environments --- .../public/stylesheets/app/editor/rich-text.less | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/rich-text.less b/services/web/public/stylesheets/app/editor/rich-text.less index 96cdea9cad..f96e707dcc 100644 --- a/services/web/public/stylesheets/app/editor/rich-text.less +++ b/services/web/public/stylesheets/app/editor/rich-text.less @@ -41,6 +41,8 @@ /****************************************************************************/ + // wl-indent-X is used to add extra left padding to nested itemize/enumerate + // environments, so that the inner list appears more indented than the outer .wl-indent-0 { padding-left: calc(~"2.5em + @{rt-line-padding}") !important; } @@ -61,24 +63,26 @@ padding-left: calc(~"6.5em + @{rt-line-padding}") !important; } + // wl-indent-env-X is used to add extra left padding to empty nested itemize/ + // enumerate environments .wl-indent-env-0 { - padding-left: 4px !important; + padding-left: calc(~"4px + @{rt-line-padding}") !important; } .wl-indent-env-1 { - padding-left: 1.5em !important; + padding-left: calc(~"1.5em + @{rt-line-padding}") !important; } .wl-indent-env-2 { - padding-left: 2.5em !important; + padding-left: calc(~"2.5em + @{rt-line-padding}") !important; } .wl-indent-env-3 { - padding-left: 3.5em !important; + padding-left: calc(~"3.5em + @{rt-line-padding}") !important; } .wl-indent-env-4 { - padding-left: 4.5em !important; + padding-left: calc(~"4.5em + @{rt-line-padding}") !important; } .wl-enumerate-item-open { From ddca090cd7bdb77ecb8d7b4515793407f62d6513 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Tue, 17 Jul 2018 09:09:07 -0500 Subject: [PATCH 072/188] Add Wiki controller with MathJax directive --- services/web/public/coffee/main/learn.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/main/learn.coffee b/services/web/public/coffee/main/learn.coffee index 375c2f0365..8ed54cf57e 100644 --- a/services/web/public/coffee/main/learn.coffee +++ b/services/web/public/coffee/main/learn.coffee @@ -1,5 +1,6 @@ define [ "base" + "directives/mathjax" "services/algolia-search" ], (App) -> @@ -55,4 +56,4 @@ define [ hits = _.map response.hits, buildHitViewModel updateHits hits - + App.controller "LearnController", () -> From 83b3904fcabf8df9b7b3504ecd48abeefe9be508 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Tue, 17 Jul 2018 09:10:03 -0500 Subject: [PATCH 073/188] Set max-width on Wiki images Fixes images expanding outside of parent container --- services/web/public/stylesheets/app/wiki.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/web/public/stylesheets/app/wiki.less b/services/web/public/stylesheets/app/wiki.less index 5f7462d02c..29ccf505e6 100644 --- a/services/web/public/stylesheets/app/wiki.less +++ b/services/web/public/stylesheets/app/wiki.less @@ -13,6 +13,10 @@ font-size: 18px; } } + + img { + max-width: 100%; + } .page-header { a { From fa2f703c448a858ade4a5c839cf05beb9bdb721a Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Thu, 12 Jul 2018 16:26:08 -0500 Subject: [PATCH 074/188] Define CMS module controller --- services/web/public/coffee/main.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index abbc59f9e4..fc62c6bc38 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -24,6 +24,7 @@ define [ "main/affiliations/controllers/UserAffiliationsController" "main/affiliations/factories/UserAffiliationsDataService" "main/keys" + "main/cms/index" "analytics/AbTestingManager" "directives/asyncForm" "directives/stopPropagation" From 299de369e538a748e9d74e5b3db5d3d28784ad23 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 17 Jul 2018 16:27:24 +0100 Subject: [PATCH 075/188] Refactor the way logins are finished off and sessions established --- .../AuthenticationController.coffee | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index ab18b21a56..832f01043f 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -62,16 +62,23 @@ module.exports = AuthenticationController = if err? return next(err) if user # `user` is either a user object or false - redir = AuthenticationController._getRedirectFromSession(req) || "/project" - AuthenticationController.afterLoginSessionSetup req, user, (err) -> - if err? - return next(err) - AuthenticationController._clearRedirectFromSession(req) - res.json {redir: redir} + AuthenticationController.finishLogin(user, req, res, next) else res.json message: info )(req, res, next) + finishLogin: (user, req, res, next) -> + redir = AuthenticationController._getRedirectFromSession(req) || "/project" + AuthenticationController._loginAsyncHandlers(req, user) + AuthenticationController.afterLoginSessionSetup req, user, (err) -> + if err? + return next(err) + AuthenticationController._clearRedirectFromSession(req) + if req.headers?['accept']?.match(/^application\/json.*$/) + res.json {redir: redir} + else + res.redirect(redir) + doPassportLogin: (req, username, password, done) -> email = username.toLowerCase() LoginRateLimiter.processLoginRequest email, (err, isAllowed)-> @@ -83,20 +90,19 @@ module.exports = AuthenticationController = return done(error) if error? if user? # async actions - AuthenticationController._loginAsyncHandlers(req, email, user) return done(null, user) else AuthenticationController._recordFailedLogin() logger.log email: email, "failed log in" return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'}) - _loginAsyncHandlers: (req, email, user) -> + _loginAsyncHandlers: (req, user) -> UserHandler.setupLoginData(user, ()->) - LoginRateLimiter.recordSuccessfulLogin(email) + LoginRateLimiter.recordSuccessfulLogin(user.email) AuthenticationController._recordSuccessfulLogin(user._id) Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip}) Analytics.identifyUser(user._id, req.sessionID) - logger.log email: email, user_id: user._id.toString(), "successful log in" + logger.log email: user.email, user_id: user._id.toString(), "successful log in" req.session.justLoggedIn = true # capture the request ip for use when creating the session user._login_req_ip = req.ip From fba0cd04f5ed90487084e963a13b876dbf799525 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 17 Jul 2018 17:16:29 +0100 Subject: [PATCH 076/188] Fix TC underlines being offset because review panel above editor toolbar --- services/web/app/views/project/editor/review-panel.pug | 2 +- services/web/public/stylesheets/app/editor.less | 2 ++ .../web/public/stylesheets/app/editor/review-panel.less | 6 ------ 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index 5c10a8ed4d..f7650a2328 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -1,4 +1,4 @@ -#review-panel +#review-panel(style=showRichText ? "top: 32px" : "") .rp-in-editor-widgets a.rp-track-changes-indicator( href diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 42c6c15daf..c0db61aef0 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -93,6 +93,8 @@ height: @editor-toolbar-height; background-color: @editor-toolbar-bg; overflow: hidden; + position: relative; + z-index: 10; // Prevent track changes showing over toolbar } .loading-screen { diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 05b45afc59..800a25618f 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -793,12 +793,6 @@ } } -.toolbar-editor { - .rp-size-expanded & { - margin-right: @review-panel-width; - } -} - .rp-toggle { display: inline-block; vertical-align: middle; From 4202b9c7176a2dc8bfd542fc7dcee6a13044d601 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 16 Jul 2018 13:55:07 +0100 Subject: [PATCH 077/188] Use hard-coded feature flag for rich text --- .../web/app/coffee/Features/Project/ProjectController.coffee | 3 ++- services/web/app/coffee/infrastructure/Features.coffee | 4 ++++ services/web/app/views/project/editor.pug | 1 + services/web/app/views/project/editor/editor.pug | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index e2c71a956e..026f55156b 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -28,6 +28,7 @@ Modules = require '../../infrastructure/Modules' ProjectEntityHandler = require './ProjectEntityHandler' crypto = require 'crypto' { V1ConnectionError } = require '../Errors/Errors' +Features = require('../../Infrastructure/Features') module.exports = ProjectController = @@ -344,7 +345,7 @@ module.exports = ProjectController = themes: THEME_LIST maxDocLength: Settings.max_doc_length useV2History: !!project.overleaf?.history?.display - showRichText: req.query?.rt == 'true' + richTextEnabled: Features.richTextEnabled() showTestControls: req.query?.tc == 'true' || user.isAdmin showPublishModal: req.query?.pm == 'true' timer.done() diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index 311d8943c9..52b27a73fd 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -4,6 +4,10 @@ module.exports = Features = externalAuthenticationSystemUsed: -> Settings.ldap? or Settings.saml? or Settings.overleaf?.oauth? + richTextEnabled: -> + isEnabled = true # Switch to false to disable + Settings.overleaf? and isEnabled + hasFeature: (feature) -> switch feature when 'homepage' diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 190ba57cf6..6d7d18687d 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -133,6 +133,7 @@ block requirejs window.maxDocLength = #{maxDocLength}; window.trackChangesState = data.trackChangesState; window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; + window.richTextEnabled = #{richTextEnabled} window.requirejs = { "paths" : { "moment": "libs/#{lib('moment')}", diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index 63f3e78c5b..69a874cc05 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -43,7 +43,7 @@ div.full-size( ace-editor="editor", ng-if="!editor.showRichText", ng-show="!!editor.sharejs_doc && !editor.opening", - style=showRichText ? "top: 32px" : "", + style=richTextEnabled ? "top: 32px" : "", theme="settings.theme", keybindings="settings.mode", font-size="settings.fontSize", From 85313f1b5a696b88ac9be6538c33201cc2c109a2 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 16 Jul 2018 13:55:44 +0100 Subject: [PATCH 078/188] Check if rich text is enabled before using stored mode --- .../web/public/coffee/ide/editor/EditorManager.coffee | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index 26ddc7dd33..5130e4796d 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -6,7 +6,7 @@ define [ "ide/editor/controllers/SavingNotificationController" ], (Document) -> class EditorManager - constructor: (@ide, @$scope, localStorage) -> + constructor: (@ide, @$scope, @localStorage) -> @$scope.editor = { sharejs_doc: null open_doc_id: null @@ -14,7 +14,7 @@ define [ opening: true trackChanges: false wantTrackChanges: false - showRichText: localStorage("editor.mode.#{@$scope.project_id}") == 'rich-text' + showRichText: @showRichText() } @$scope.$on "entity:selected", (event, entity) => @@ -41,6 +41,11 @@ define [ return if !value? @_syncTrackChangesState(@$scope.editor.sharejs_doc) + showRichText: () -> + if !window.richTextEnabled + return false + @localStorage("editor.mode.#{@$scope.project_id}") == 'rich-text' + autoOpenDoc: () -> open_doc_id = @ide.localStorage("doc.open_id.#{@$scope.project_id}") or From 9cd3f32365950f210a4ec8c2c7935e09a9538cb4 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 16 Jul 2018 14:20:53 +0100 Subject: [PATCH 079/188] Use hasFeature function instead of new standalone function --- .../app/coffee/Features/Project/ProjectController.coffee | 2 +- services/web/app/coffee/infrastructure/Features.coffee | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 026f55156b..301a1ed916 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -345,7 +345,7 @@ module.exports = ProjectController = themes: THEME_LIST maxDocLength: Settings.max_doc_length useV2History: !!project.overleaf?.history?.display - richTextEnabled: Features.richTextEnabled() + richTextEnabled: Features.hasFeature('rich-text') showTestControls: req.query?.tc == 'true' || user.isAdmin showPublishModal: req.query?.pm == 'true' timer.done() diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index 52b27a73fd..bdb68b9480 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -4,10 +4,6 @@ module.exports = Features = externalAuthenticationSystemUsed: -> Settings.ldap? or Settings.saml? or Settings.overleaf?.oauth? - richTextEnabled: -> - isEnabled = true # Switch to false to disable - Settings.overleaf? and isEnabled - hasFeature: (feature) -> switch feature when 'homepage' @@ -26,5 +22,8 @@ module.exports = Features = return !Settings.overleaf? when 'affiliations' return Settings?.apis?.v1?.url? + when 'rich-text' + isEnabled = false # Switch to false to disable + Settings.overleaf? and isEnabled else throw new Error("unknown feature: #{feature}") From 823e12e4a874da73616c803f9a58b9869e298702 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 16 Jul 2018 14:21:10 +0100 Subject: [PATCH 080/188] Fix tests to add new Features dependency --- .../web/test/unit/coffee/Project/ProjectControllerTests.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index bfd67e42d2..3f81c9d62c 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -72,6 +72,8 @@ describe "ProjectController", -> @Modules = hooks: fire: sinon.stub() + @Features = + hasFeature: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -102,6 +104,7 @@ describe "ProjectController", -> "../../infrastructure/Modules": @Modules "./ProjectEntityHandler": @ProjectEntityHandler "../Errors/Errors": Errors + "../../Infrastructure/Features": @Features @projectName = "£12321jkj9ujkljds" @req = From 62f69a4bcd5ff7c18d1528b03aabe420b8d09916 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 16 Jul 2018 15:53:28 +0100 Subject: [PATCH 081/188] Fix case sensitivity --- .../web/app/coffee/Features/Project/ProjectController.coffee | 2 +- .../web/test/unit/coffee/Project/ProjectControllerTests.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 301a1ed916..8efdf9f218 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -28,7 +28,7 @@ Modules = require '../../infrastructure/Modules' ProjectEntityHandler = require './ProjectEntityHandler' crypto = require 'crypto' { V1ConnectionError } = require '../Errors/Errors' -Features = require('../../Infrastructure/Features') +Features = require('../../infrastructure/Features') module.exports = ProjectController = diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index 3f81c9d62c..1acd1c8128 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -104,7 +104,7 @@ describe "ProjectController", -> "../../infrastructure/Modules": @Modules "./ProjectEntityHandler": @ProjectEntityHandler "../Errors/Errors": Errors - "../../Infrastructure/Features": @Features + "../../infrastructure/Features": @Features @projectName = "£12321jkj9ujkljds" @req = From ffe3bc297713713d560ede4852ff6359dd81e881 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 17 Jul 2018 17:19:42 +0100 Subject: [PATCH 082/188] Switch review panel to use richTextEnabled variable --- services/web/app/views/project/editor/review-panel.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index f7650a2328..8328d313e5 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -1,4 +1,4 @@ -#review-panel(style=showRichText ? "top: 32px" : "") +#review-panel(style=richTextEnabled ? "top: 32px" : "") .rp-in-editor-widgets a.rp-track-changes-indicator( href From d36d3f2a0eaaf05612c187cecfbb3c5696c570df Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 17 Jul 2018 17:33:41 +0100 Subject: [PATCH 083/188] Actually enable rich text --- services/web/app/coffee/infrastructure/Features.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index bdb68b9480..ffc60604e6 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -23,7 +23,7 @@ module.exports = Features = when 'affiliations' return Settings?.apis?.v1?.url? when 'rich-text' - isEnabled = false # Switch to false to disable + isEnabled = true # Switch to false to disable Settings.overleaf? and isEnabled else throw new Error("unknown feature: #{feature}") From 00bf50906d2b3a2a46faded3cb5ed176aefe58d1 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Wed, 18 Jul 2018 09:54:52 +0100 Subject: [PATCH 084/188] fixing button wrapping in the publish modal --- services/web/public/stylesheets/app/editor/publish-modal.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less index d196d3ccf0..35a3dd7c44 100644 --- a/services/web/public/stylesheets/app/editor/publish-modal.less +++ b/services/web/public/stylesheets/app/editor/publish-modal.less @@ -23,6 +23,9 @@ .wl-icon:before{ font-size: 14px; } + .btn-primary{ + whiteSpace: 'normal' + } .button-as-link{ color: green; text-transform: none; From dae9506f84c9e1c4e8e16aba1a3f5b4930a40ec7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 18 Jul 2018 09:57:05 +0100 Subject: [PATCH 085/188] Un-break unit tests --- .../AuthenticationControllerTests.coffee | 75 ++++++++----------- 1 file changed, 30 insertions(+), 45 deletions(-) diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index f631dfe9a9..c48a3bc65c 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -98,6 +98,7 @@ describe "AuthenticationController", -> @req.session.destroy = sinon.stub().callsArgWith(0, null) @req.session.save = sinon.stub().callsArgWith(0, null) @req.sessionStore = {generate: sinon.stub()} + @AuthenticationController.finishLogin = sinon.stub() @passport.authenticate.callsArgWith(1, null, @user, @info) @err = new Error('woops') @@ -123,27 +124,10 @@ describe "AuthenticationController", -> afterEach -> delete @req.session.postLoginRedirect - it 'should call req.login', () -> + it 'should call finishLogin', () -> @AuthenticationController.passportLogin @req, @res, @next - @req.login.callCount.should.equal 1 - @req.login.calledWith(@user).should.equal true - - it 'should send a json response with redirect', () -> - @AuthenticationController.passportLogin @req, @res, @next - @res.json.callCount.should.equal 1 - @res.json.calledWith({redir: 'some_redirect'}).should.equal true - - describe 'when session.save produces an error', () -> - beforeEach -> - @req.session.save = sinon.stub().callsArgWith(0, new Error('woops')) - - it 'should return next with an error', () -> - @AuthenticationController.passportLogin @req, @res, @next - @next.calledWith(@err).should.equal true - - it 'should not return json', () -> - @AuthenticationController.passportLogin @req, @res, @next - @res.json.callCount.should.equal 0 + @AuthenticationController.finishLogin.callCount.should.equal 1 + @AuthenticationController.finishLogin.calledWith(@user).should.equal true describe 'when authenticate does not produce a user', -> @@ -151,9 +135,9 @@ describe "AuthenticationController", -> @info = {text: 'a', type: 'b'} @passport.authenticate.callsArgWith(1, null, false, @info) - it 'should not call req.login', () -> + it 'should not call finishLogin', () -> @AuthenticationController.passportLogin @req, @res, @next - @req.login.callCount.should.equal 0 + @AuthenticationController.finishLogin.callCount.should.equal 0 it 'should not send a json response with redirect', () -> @AuthenticationController.passportLogin @req, @res, @next @@ -255,7 +239,6 @@ describe "AuthenticationController", -> @LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true) @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user) @req.sessionID = Math.random() - @AnalyticsManager.identifyUser = sinon.stub() @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb) it "should attempt to authorise the user", -> @@ -263,35 +246,37 @@ describe "AuthenticationController", -> .calledWith(email: @email.toLowerCase(), @password) .should.equal true - it "should call identifyUser", -> - @AnalyticsManager.identifyUser.calledWith(@user._id, @req.sessionID).should.equal true - - it "should setup the user data in the background", -> - @UserHandler.setupLoginData.calledWith(@user).should.equal true - it "should establish the user's session", -> @cb.calledWith(null, @user).should.equal true - it "should set res.session.justLoggedIn", -> - @req.session.justLoggedIn.should.equal true + # TODO: move these to a test for the async handlers, or for finishLogin - it "should record the successful login", -> - @AuthenticationController._recordSuccessfulLogin - .calledWith(@user._id) - .should.equal true + # it "should call identifyUser", -> + # @AnalyticsManager.identifyUser.calledWith(@user._id, @req.sessionID).should.equal true - it "should tell the rate limiter that there was a success for that email", -> - @LoginRateLimiter.recordSuccessfulLogin.calledWith(@email.toLowerCase()).should.equal true + # it "should setup the user data in the background", -> + # @UserHandler.setupLoginData.calledWith(@user).should.equal true - it "should log the successful login", -> - @logger.log - .calledWith(email: @email.toLowerCase(), user_id: @user._id.toString(), "successful log in") - .should.equal true + # it "should set res.session.justLoggedIn", -> + # @req.session.justLoggedIn.should.equal true - it "should track the login event", -> - @AnalyticsManager.recordEvent - .calledWith(@user._id, "user-logged-in") - .should.equal true + # it "should record the successful login", -> + # @AuthenticationController._recordSuccessfulLogin + # .calledWith(@user._id) + # .should.equal true + + # it "should tell the rate limiter that there was a success for that email", -> + # @LoginRateLimiter.recordSuccessfulLogin.calledWith(@email.toLowerCase()).should.equal true + + # it "should log the successful login", -> + # @logger.log + # .calledWith(email: @email.toLowerCase(), user_id: @user._id.toString(), "successful log in") + # .should.equal true + + # it "should track the login event", -> + # @AnalyticsManager.recordEvent + # .calledWith(@user._id, "user-logged-in") + # .should.equal true describe 'when the user is not authenticated', -> beforeEach -> From ef0532a54baba1c895a268d1fea426e4e017be98 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Wed, 18 Jul 2018 10:47:39 +0100 Subject: [PATCH 086/188] replace wrapping button with new class name --- services/web/public/stylesheets/app/editor/publish-modal.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less index 35a3dd7c44..3f049fc75b 100644 --- a/services/web/public/stylesheets/app/editor/publish-modal.less +++ b/services/web/public/stylesheets/app/editor/publish-modal.less @@ -23,8 +23,8 @@ .wl-icon:before{ font-size: 14px; } - .btn-primary{ - whiteSpace: 'normal' + .btn-wrapping{ + white-space: normal; } .button-as-link{ color: green; From 943bfe98aad95c4df8a9ca3c3f07bf493385fc5f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 18 Jul 2018 11:13:42 +0100 Subject: [PATCH 087/188] Unit test for `_loginAsyncHandlers` --- .../AuthenticationControllerTests.coffee | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index c48a3bc65c..2405e9fba7 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -249,34 +249,41 @@ describe "AuthenticationController", -> it "should establish the user's session", -> @cb.calledWith(null, @user).should.equal true - # TODO: move these to a test for the async handlers, or for finishLogin + describe '_loginAsyncHandlers', -> + beforeEach -> + @UserHandler.setupLoginData = sinon.stub() + @LoginRateLimiter.recordSuccessfulLogin = sinon.stub() + @AuthenticationController._recordSuccessfulLogin = sinon.stub() + @AnalyticsManager.recordEvent = sinon.stub() + @AnalyticsManager.identifyUser = sinon.stub() + @AuthenticationController._loginAsyncHandlers(@req, @user) - # it "should call identifyUser", -> - # @AnalyticsManager.identifyUser.calledWith(@user._id, @req.sessionID).should.equal true + it "should call identifyUser", -> + @AnalyticsManager.identifyUser.calledWith(@user._id, @req.sessionID).should.equal true - # it "should setup the user data in the background", -> - # @UserHandler.setupLoginData.calledWith(@user).should.equal true + it "should setup the user data in the background", -> + @UserHandler.setupLoginData.calledWith(@user).should.equal true - # it "should set res.session.justLoggedIn", -> - # @req.session.justLoggedIn.should.equal true + it "should set res.session.justLoggedIn", -> + @req.session.justLoggedIn.should.equal true - # it "should record the successful login", -> - # @AuthenticationController._recordSuccessfulLogin - # .calledWith(@user._id) - # .should.equal true + it "should record the successful login", -> + @AuthenticationController._recordSuccessfulLogin + .calledWith(@user._id) + .should.equal true - # it "should tell the rate limiter that there was a success for that email", -> - # @LoginRateLimiter.recordSuccessfulLogin.calledWith(@email.toLowerCase()).should.equal true + it "should tell the rate limiter that there was a success for that email", -> + @LoginRateLimiter.recordSuccessfulLogin.calledWith(@user.email).should.equal true - # it "should log the successful login", -> - # @logger.log - # .calledWith(email: @email.toLowerCase(), user_id: @user._id.toString(), "successful log in") - # .should.equal true + it "should log the successful login", -> + @logger.log + .calledWith(email: @user.email, user_id: @user._id.toString(), "successful log in") + .should.equal true - # it "should track the login event", -> - # @AnalyticsManager.recordEvent - # .calledWith(@user._id, "user-logged-in") - # .should.equal true + it "should track the login event", -> + @AnalyticsManager.recordEvent + .calledWith(@user._id, "user-logged-in") + .should.equal true describe 'when the user is not authenticated', -> beforeEach -> From c423672b552cc68594b8f5d29eadecaee41905e3 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 18 Jul 2018 12:08:34 +0100 Subject: [PATCH 088/188] Unit test for `finishLogin` --- .../AuthenticationControllerTests.coffee | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index 2405e9fba7..8482c28e04 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -579,3 +579,56 @@ describe "AuthenticationController", -> @AuthenticationController._clearRedirectFromSession(@req) expect(@req.session.postLoginRedirect).to.equal undefined + + describe 'finishLogin', -> + # - get redirect + # - async handlers + # - afterLoginSessionSetup + # - clear redirect + # - issue redir, two ways + beforeEach -> + @AuthenticationController._getRedirectFromSession = sinon.stub().returns '/some/page' + @AuthenticationController._loginAsyncHandlers = sinon.stub() + @AuthenticationController.afterLoginSessionSetup = sinon.stub().callsArgWith(2, null) + @AuthenticationController._clearRedirectFromSession = sinon.stub() + @req.headers = {accept: 'application/json, whatever'} + @res.json = sinon.stub() + @res.redirect = sinon.stub() + + it 'should extract the redirect from the session', () -> + @AuthenticationController.finishLogin(@user, @req, @res, @next) + expect(@AuthenticationController._getRedirectFromSession.callCount).to.equal 1 + expect(@AuthenticationController._getRedirectFromSession.calledWith(@req)).to.equal true + + it 'should call the async handlers', () -> + @AuthenticationController.finishLogin(@user, @req, @res, @next) + expect(@AuthenticationController._loginAsyncHandlers.callCount).to.equal 1 + expect(@AuthenticationController._loginAsyncHandlers.calledWith(@req, @user)).to.equal true + + it 'should call afterLoginSessionSetup', () -> + @AuthenticationController.finishLogin(@user, @req, @res, @next) + expect(@AuthenticationController.afterLoginSessionSetup.callCount).to.equal 1 + expect(@AuthenticationController.afterLoginSessionSetup.calledWith(@req, @user)).to.equal true + + it 'should clear redirect from session', () -> + @AuthenticationController.finishLogin(@user, @req, @res, @next) + expect(@AuthenticationController._clearRedirectFromSession.callCount).to.equal 1 + expect(@AuthenticationController._clearRedirectFromSession.calledWith(@req)).to.equal true + + it 'should issue a json response with a redirect', () -> + @AuthenticationController.finishLogin(@user, @req, @res, @next) + expect(@res.json.callCount).to.equal 1 + expect(@res.redirect.callCount).to.equal 0 + expect(@res.json.calledWith({ redir: '/some/page' })).to.equal true + + describe 'with a non-json request', -> + beforeEach -> + @req.headers = {} + @res.json = sinon.stub() + @res.redirect = sinon.stub() + + it 'should issue a plain redirect', () -> + @AuthenticationController.finishLogin(@user, @req, @res, @next) + expect(@res.json.callCount).to.equal 0 + expect(@res.redirect.callCount).to.equal 1 + expect(@res.redirect.calledWith('/some/page')).to.equal true From 413721ebf9c75be36d6fe34f7e5858bdc856a583 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 18 Jul 2018 13:49:53 +0100 Subject: [PATCH 089/188] look at more chars for if the server is in dark mode v2-dark.overleaf.com also needs to be marked as dark --- services/web/app/coffee/infrastructure/ExpressLocals.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index d2e7415067..5c71afbe5d 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -109,7 +109,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> logger.log user_id:user_id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets" req.session.cdnBlocked = true - isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark" + isDark = req.headers?.host?.slice(0,7)?.toLowerCase().indexOf("dark") != -1 isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke" isLive = !isDark and !isSmoke From f260f4246697dcee0392bd08015091d2c862027a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Fri, 20 Jul 2018 12:39:01 +0100 Subject: [PATCH 090/188] Revert "add v2 history labels endpoints" --- .../Features/History/HistoryController.coffee | 65 +++--------------- services/web/app/coffee/router.coffee | 4 -- .../test/acceptance/coffee/LabelsTests.coffee | 68 ------------------- .../helpers/MockProjectHistoryApi.coffee | 48 +------------ 4 files changed, 12 insertions(+), 173 deletions(-) delete mode 100644 services/web/test/acceptance/coffee/LabelsTests.coffee diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee index 189e07b0f5..7d9d347531 100644 --- a/services/web/app/coffee/Features/History/HistoryController.coffee +++ b/services/web/app/coffee/Features/History/HistoryController.coffee @@ -21,13 +21,6 @@ module.exports = HistoryController = req.useProjectHistory = false next() - ensureProjectHistoryEnabled: (req, res, next = (error) ->) -> - if req.useProjectHistory? - next() - else - logger.log {project_id}, "project history not enabled" - res.sendStatus(404) - proxyToHistoryApi: (req, res, next = (error) ->) -> user_id = AuthenticationController.getLoggedInUserId req url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url @@ -48,17 +41,22 @@ module.exports = HistoryController = user_id = AuthenticationController.getLoggedInUserId req url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url logger.log url: url, "proxying to history api" - HistoryController._makeRequest { + request { url: url method: req.method json: true headers: "X-User-Id": user_id - }, (error, body) -> + }, (error, response, body) -> return next(error) if error? - HistoryManager.injectUserDetails body, (error, data) -> - return next(error) if error? - res.json data + if 200 <= response.statusCode < 300 + HistoryManager.injectUserDetails body, (error, data) -> + return next(error) if error? + res.json data + else + error = new Error("history api responded with non-success code: #{response.statusCode}") + logger.error err: error, user_id: user_id, "error proxying request to history api" + next(error) buildHistoryServiceUrl: (useProjectHistory) -> # choose a history service, either document-level (trackchanges) @@ -100,46 +98,3 @@ module.exports = HistoryController = doc_id: doc._id } - getLabels: (req, res, next) -> - project_id = req.params.Project_id - user_id = AuthenticationController.getLoggedInUserId(req) - HistoryController._makeRequest { - method: "GET" - url: "#{settings.apis.project_history.url}/project/#{project_id}/labels" - json: true - }, (error, labels) -> - return next(error) if error? - res.json labels - - createLabel: (req, res, next) -> - project_id = req.params.Project_id - {comment, version} = req.body - user_id = AuthenticationController.getLoggedInUserId(req) - HistoryController._makeRequest { - method: "POST" - url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels" - json: {comment, version} - }, (error, label) -> - return next(error) if error? - res.json label - - deleteLabel: (req, res, next) -> - project_id = req.params.Project_id - label_id = req.params.label_id - user_id = AuthenticationController.getLoggedInUserId(req) - HistoryController._makeRequest { - method: "DELETE" - url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}" - }, (error) -> - return next(error) if error? - res.sendStatus 204 - - _makeRequest: (options, callback) -> - request options, (error, response, body) -> - return callback(error) if error? - if 200 <= response.statusCode < 300 - callback(null, body) - else - error = new Error("history api responded with non-success code: #{response.statusCode}") - logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}" - callback(error) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 9ae764781c..72a0ddebc4 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -239,10 +239,6 @@ module.exports = class Router webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory - webRouter.get "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels - webRouter.post "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel - webRouter.delete "/project/:Project_id/labels/:label_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.deleteLabel - webRouter.post '/project/:project_id/export/:brand_variation_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportProject webRouter.get '/project/:project_id/export/:export_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportStatus diff --git a/services/web/test/acceptance/coffee/LabelsTests.coffee b/services/web/test/acceptance/coffee/LabelsTests.coffee deleted file mode 100644 index 8832f2a326..0000000000 --- a/services/web/test/acceptance/coffee/LabelsTests.coffee +++ /dev/null @@ -1,68 +0,0 @@ -_ = require 'underscore' -{expect} = require 'chai' -{ObjectId} = require 'mongojs' -request = require './helpers/request' - -MockProjectHistoryApi = require './helpers/MockProjectHistoryApi' -User = require './helpers/User' - -describe 'Labels', -> - beforeEach (done) -> - @owner = new User() - @owner.login (error) => - throw error if error? - @owner.createProject 'example-project', {template: 'example'}, (error, @project_id) => - throw error if error? - done() - - afterEach -> - MockProjectHistoryApi.reset() - - it 'getting labels', (done) -> - label_id = new ObjectId().toString() - comment = 'a label comment' - version = 3 - MockProjectHistoryApi.addLabel @project_id, label_id, comment, version - - @owner.request { - method: 'GET' - url: "/project/#{@project_id}/labels" - json: true - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - expect(body).to.deep.equal [{ label_id, comment, version }] - done() - - it 'creating a label', (done) -> - comment = 'a label comment' - version = 3 - - @owner.request { - method: 'POST' - url: "/project/#{@project_id}/labels" - json: {comment, version} - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - {label_id} = body - expect( - MockProjectHistoryApi.getLabels(@project_id) - ).to.deep.equal [{label_id, comment, version} ] - done() - - it 'deleting a label', (done) -> - label_id = new ObjectId().toString() - comment = 'a label comment' - version = 3 - MockProjectHistoryApi.addLabel @project_id, label_id, comment, version - - @owner.request { - method: 'DELETE' - url: "/project/#{@project_id}/labels/#{label_id}" - json: true - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 204 - expect(MockProjectHistoryApi.getLabels(@project_id)).to.deep.equal [] - done() diff --git a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee index f73b6007a4..b7df202d72 100644 --- a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee @@ -1,8 +1,5 @@ -_ = require 'lodash' -express = require 'express' -bodyParser = require "body-parser" +express = require("express") app = express() -{ObjectId} = require 'mongojs' module.exports = MockProjectHistoryApi = docs: {} @@ -11,30 +8,12 @@ module.exports = MockProjectHistoryApi = projectVersions: {} - labels: {} - addOldFile: (project_id, version, pathname, content) -> @oldFiles["#{project_id}:#{version}:#{pathname}"] = content setProjectVersion: (project_id, version) -> @projectVersions[project_id] = version - addLabel: (project_id, label_id, comment, version) -> - @labels[project_id] ?= {} - @labels[project_id][label_id] = {label_id,comment,version} - - deleteLabel: (project_id, label_id) -> - delete @labels[project_id][label_id] - - getLabels: (project_id) -> - return null unless @labels[project_id]? - _.values @labels[project_id] - - reset: () -> - @oldFiles = {} - @projectVersions = {} - @labels = {} - run: () -> app.post "/project", (req, res, next) => res.json project: id: 1 @@ -54,34 +33,11 @@ module.exports = MockProjectHistoryApi = else res.send 404 - app.get "/project/:project_id/labels", (req, res, next) => - {project_id} = req.params - labels = @getLabels project_id - if labels? - res.json labels - else - res.send 404 - - app.post "/project/:project_id/user/:user_id/labels", bodyParser.json(), (req, res, next) => - {project_id} = req.params - {comment, version} = req.body - label_id = new ObjectId().toString() - @addLabel project_id, label_id, comment, version - res.json {label_id, comment, version} - - app.delete "/project/:project_id/user/:user_id/labels/:label_id", (req, res, next) => - {project_id, label_id} = req.params - label = @labels[project_id]?[label_id] - if label? - @deleteLabel project_id, label_id - res.send 204 - else - res.send 404 - app.listen 3054, (error) -> throw error if error? .on "error", (error) -> console.error "error starting MockProjectHistoryApi:", error.message process.exit(1) + MockProjectHistoryApi.run() From 8fb9554c3fcb5dcea839d6005c945b5ad36e29ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Fri, 20 Jul 2018 15:36:03 +0100 Subject: [PATCH 091/188] Revert "Revert "add v2 history labels endpoints"" --- .../Features/History/HistoryController.coffee | 65 +++++++++++++++--- services/web/app/coffee/router.coffee | 4 ++ .../test/acceptance/coffee/LabelsTests.coffee | 68 +++++++++++++++++++ .../helpers/MockProjectHistoryApi.coffee | 48 ++++++++++++- 4 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 services/web/test/acceptance/coffee/LabelsTests.coffee diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee index 7d9d347531..189e07b0f5 100644 --- a/services/web/app/coffee/Features/History/HistoryController.coffee +++ b/services/web/app/coffee/Features/History/HistoryController.coffee @@ -21,6 +21,13 @@ module.exports = HistoryController = req.useProjectHistory = false next() + ensureProjectHistoryEnabled: (req, res, next = (error) ->) -> + if req.useProjectHistory? + next() + else + logger.log {project_id}, "project history not enabled" + res.sendStatus(404) + proxyToHistoryApi: (req, res, next = (error) ->) -> user_id = AuthenticationController.getLoggedInUserId req url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url @@ -41,22 +48,17 @@ module.exports = HistoryController = user_id = AuthenticationController.getLoggedInUserId req url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url logger.log url: url, "proxying to history api" - request { + HistoryController._makeRequest { url: url method: req.method json: true headers: "X-User-Id": user_id - }, (error, response, body) -> + }, (error, body) -> return next(error) if error? - if 200 <= response.statusCode < 300 - HistoryManager.injectUserDetails body, (error, data) -> - return next(error) if error? - res.json data - else - error = new Error("history api responded with non-success code: #{response.statusCode}") - logger.error err: error, user_id: user_id, "error proxying request to history api" - next(error) + HistoryManager.injectUserDetails body, (error, data) -> + return next(error) if error? + res.json data buildHistoryServiceUrl: (useProjectHistory) -> # choose a history service, either document-level (trackchanges) @@ -98,3 +100,46 @@ module.exports = HistoryController = doc_id: doc._id } + getLabels: (req, res, next) -> + project_id = req.params.Project_id + user_id = AuthenticationController.getLoggedInUserId(req) + HistoryController._makeRequest { + method: "GET" + url: "#{settings.apis.project_history.url}/project/#{project_id}/labels" + json: true + }, (error, labels) -> + return next(error) if error? + res.json labels + + createLabel: (req, res, next) -> + project_id = req.params.Project_id + {comment, version} = req.body + user_id = AuthenticationController.getLoggedInUserId(req) + HistoryController._makeRequest { + method: "POST" + url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels" + json: {comment, version} + }, (error, label) -> + return next(error) if error? + res.json label + + deleteLabel: (req, res, next) -> + project_id = req.params.Project_id + label_id = req.params.label_id + user_id = AuthenticationController.getLoggedInUserId(req) + HistoryController._makeRequest { + method: "DELETE" + url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}" + }, (error) -> + return next(error) if error? + res.sendStatus 204 + + _makeRequest: (options, callback) -> + request options, (error, response, body) -> + return callback(error) if error? + if 200 <= response.statusCode < 300 + callback(null, body) + else + error = new Error("history api responded with non-success code: #{response.statusCode}") + logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}" + callback(error) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 72a0ddebc4..9ae764781c 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -239,6 +239,10 @@ module.exports = class Router webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory + webRouter.get "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels + webRouter.post "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel + webRouter.delete "/project/:Project_id/labels/:label_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.deleteLabel + webRouter.post '/project/:project_id/export/:brand_variation_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportProject webRouter.get '/project/:project_id/export/:export_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportStatus diff --git a/services/web/test/acceptance/coffee/LabelsTests.coffee b/services/web/test/acceptance/coffee/LabelsTests.coffee new file mode 100644 index 0000000000..8832f2a326 --- /dev/null +++ b/services/web/test/acceptance/coffee/LabelsTests.coffee @@ -0,0 +1,68 @@ +_ = require 'underscore' +{expect} = require 'chai' +{ObjectId} = require 'mongojs' +request = require './helpers/request' + +MockProjectHistoryApi = require './helpers/MockProjectHistoryApi' +User = require './helpers/User' + +describe 'Labels', -> + beforeEach (done) -> + @owner = new User() + @owner.login (error) => + throw error if error? + @owner.createProject 'example-project', {template: 'example'}, (error, @project_id) => + throw error if error? + done() + + afterEach -> + MockProjectHistoryApi.reset() + + it 'getting labels', (done) -> + label_id = new ObjectId().toString() + comment = 'a label comment' + version = 3 + MockProjectHistoryApi.addLabel @project_id, label_id, comment, version + + @owner.request { + method: 'GET' + url: "/project/#{@project_id}/labels" + json: true + }, (error, response, body) => + throw error if error? + expect(response.statusCode).to.equal 200 + expect(body).to.deep.equal [{ label_id, comment, version }] + done() + + it 'creating a label', (done) -> + comment = 'a label comment' + version = 3 + + @owner.request { + method: 'POST' + url: "/project/#{@project_id}/labels" + json: {comment, version} + }, (error, response, body) => + throw error if error? + expect(response.statusCode).to.equal 200 + {label_id} = body + expect( + MockProjectHistoryApi.getLabels(@project_id) + ).to.deep.equal [{label_id, comment, version} ] + done() + + it 'deleting a label', (done) -> + label_id = new ObjectId().toString() + comment = 'a label comment' + version = 3 + MockProjectHistoryApi.addLabel @project_id, label_id, comment, version + + @owner.request { + method: 'DELETE' + url: "/project/#{@project_id}/labels/#{label_id}" + json: true + }, (error, response, body) => + throw error if error? + expect(response.statusCode).to.equal 204 + expect(MockProjectHistoryApi.getLabels(@project_id)).to.deep.equal [] + done() diff --git a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee index b7df202d72..f73b6007a4 100644 --- a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee @@ -1,5 +1,8 @@ -express = require("express") +_ = require 'lodash' +express = require 'express' +bodyParser = require "body-parser" app = express() +{ObjectId} = require 'mongojs' module.exports = MockProjectHistoryApi = docs: {} @@ -8,12 +11,30 @@ module.exports = MockProjectHistoryApi = projectVersions: {} + labels: {} + addOldFile: (project_id, version, pathname, content) -> @oldFiles["#{project_id}:#{version}:#{pathname}"] = content setProjectVersion: (project_id, version) -> @projectVersions[project_id] = version + addLabel: (project_id, label_id, comment, version) -> + @labels[project_id] ?= {} + @labels[project_id][label_id] = {label_id,comment,version} + + deleteLabel: (project_id, label_id) -> + delete @labels[project_id][label_id] + + getLabels: (project_id) -> + return null unless @labels[project_id]? + _.values @labels[project_id] + + reset: () -> + @oldFiles = {} + @projectVersions = {} + @labels = {} + run: () -> app.post "/project", (req, res, next) => res.json project: id: 1 @@ -33,11 +54,34 @@ module.exports = MockProjectHistoryApi = else res.send 404 + app.get "/project/:project_id/labels", (req, res, next) => + {project_id} = req.params + labels = @getLabels project_id + if labels? + res.json labels + else + res.send 404 + + app.post "/project/:project_id/user/:user_id/labels", bodyParser.json(), (req, res, next) => + {project_id} = req.params + {comment, version} = req.body + label_id = new ObjectId().toString() + @addLabel project_id, label_id, comment, version + res.json {label_id, comment, version} + + app.delete "/project/:project_id/user/:user_id/labels/:label_id", (req, res, next) => + {project_id, label_id} = req.params + label = @labels[project_id]?[label_id] + if label? + @deleteLabel project_id, label_id + res.send 204 + else + res.send 404 + app.listen 3054, (error) -> throw error if error? .on "error", (error) -> console.error "error starting MockProjectHistoryApi:", error.message process.exit(1) - MockProjectHistoryApi.run() From 99e24db07564eb917e9209724a62c78cb7fd3ab4 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 23 Jul 2018 15:30:58 +0100 Subject: [PATCH 092/188] Move MathJax config to base so it is always run This prevents an issue where using MathJax would not be configured correctly unless using the mathjax directive. --- services/web/public/coffee/base.coffee | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index cb9e70b6bf..e5e0958a24 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -26,7 +26,22 @@ define [ baseUrl: window.sharelatex.sixpackDomain client_id: window.user_id }) - + + MathJax?.Hub?.Config( + extensions: ["Safe.js"] + messageStyle: "none" + imageFont:null + "HTML-CSS": { availableFonts: ["TeX"] }, + TeX: + equationNumbers: { autoNumber: "AMS" }, + useLabelIDs: false + skipStartupTypeset: true + tex2jax: + processEscapes: true, + inlineMath: [ ["\\(","\\)"] ], + displayMath: [ ['$$','$$'], ["\\[","\\]"] ] + ) + App.run ($templateCache) -> # UI Select templates are hard-coded and use Glyphicon icons (which we don't import). # The line below simply overrides the hard-coded template with our own, which is From db518e16aa596ad93bd04df0fc4a2f68bd5540cb Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 23 Jul 2018 15:32:17 +0100 Subject: [PATCH 093/188] Don't set config unless needed in directive Updates mathjax directive to only set config if it is needed. The main config is now set in base.coffee so isn't needed --- .../public/coffee/directives/mathjax.coffee | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/services/web/public/coffee/directives/mathjax.coffee b/services/web/public/coffee/directives/mathjax.coffee index 269526287e..7124318214 100644 --- a/services/web/public/coffee/directives/mathjax.coffee +++ b/services/web/public/coffee/directives/mathjax.coffee @@ -4,23 +4,16 @@ define [ App.directive "mathjax", () -> return { link: (scope, element, attrs) -> - mathjaxConfig = - extensions: ["Safe.js"] - messageStyle: "none" - imageFont:null - "HTML-CSS": { availableFonts: ["TeX"] }, - TeX: - equationNumbers: { autoNumber: "AMS" }, - useLabelIDs: false - skipStartupTypeset: true - tex2jax: - processEscapes: true, - inlineMath: [ ["\\(","\\)"] ], - displayMath: [ ['$$','$$'], ["\\[","\\]"] ] if attrs.delimiter != 'no-single-dollar' - mathjaxConfig.tex2jax.inlineMath.push(['$','$']); + inlineMathConfig = MathJax?.Hub?.config?.tex2jax.inlineMath + alreadyConfigured = _.find inlineMathConfig, (c) -> + c[0] == '$' and c[1] == '$' - MathJax?.Hub?.Config(mathjaxConfig); + if !alreadyConfigured? + MathJax?.Hub?.Config( + tex2jax: + inlineMath: inlineMathConfig.concat([['$', '$']]) + ) setTimeout () -> MathJax?.Hub?.Queue(["Typeset", MathJax?.Hub, element.get(0)]) From e79501340c9516a4cab055123a3c6d614731b5eb Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 23 Jul 2018 16:05:26 +0100 Subject: [PATCH 094/188] fix router issue where ClsiCookieManager isn't calling a function to set the backend --- services/web/app/coffee/router.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 72a0ddebc4..d3548d1ae1 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -22,7 +22,7 @@ UserPagesController = require('./Features/User/UserPagesController') DocumentController = require('./Features/Documents/DocumentController') CompileManager = require("./Features/Compile/CompileManager") CompileController = require("./Features/Compile/CompileController") -ClsiCookieManager = require("./Features/Compile/ClsiCookieManager") +ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")() HealthCheckController = require("./Features/HealthCheck/HealthCheckController") ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController" FileStoreController = require("./Features/FileStore/FileStoreController") From 8d83da2535516f928894a57009c75fab8e696bc3 Mon Sep 17 00:00:00 2001 From: Nate Stemen Date: Mon, 23 Jul 2018 12:07:05 -0400 Subject: [PATCH 095/188] add check to make sure user hasnt turned of syntax validation before stopping autocompile --- .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index a9e95ac6ec..389631c0a7 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -93,7 +93,7 @@ define [ # don't want to compile as soon as it is fixed, so reset the timeout. $scope.startedTryingAutoCompileAt = Date.now() $scope.docLastChangedAt = Date.now() - if autoCompileLintingError + if autoCompileLintingError and $scope.stop_on_validation_error return # If there's a longish compile, don't compile immediately after if user is still typing From 9af906b7dcf5573ea61eaebb3031c94370733ee8 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 23 Jul 2018 16:03:49 +0100 Subject: [PATCH 096/188] Copy across some mathjax config from v1, seems to fix artefacts in rendering --- services/web/public/coffee/base.coffee | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index e5e0958a24..0a6f47f9fa 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -31,14 +31,21 @@ define [ extensions: ["Safe.js"] messageStyle: "none" imageFont:null - "HTML-CSS": { availableFonts: ["TeX"] }, + "HTML-CSS": + availableFonts: ["TeX"] + # MathJax's automatic font scaling does not work well when we render math + # that isn't yet on the page, so we disable it and set a global font + # scale factor + scale: 110 + matchFontHeight: false TeX: - equationNumbers: { autoNumber: "AMS" }, + equationNumbers: { autoNumber: "AMS" } useLabelIDs: false skipStartupTypeset: true tex2jax: - processEscapes: true, - inlineMath: [ ["\\(","\\)"] ], + processEscapes: true + # Dollar delimiters are added by the mathjax directive + inlineMath: [ ["\\(","\\)"] ] displayMath: [ ['$$','$$'], ["\\[","\\]"] ] ) From 63c7f756ce3e8089931f99795a7d7dda904ee831 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Tue, 10 Jul 2018 21:49:24 +0200 Subject: [PATCH 097/188] refactor into InstitutionsAPI --- .../InstitutionsAPI.coffee} | 10 +++---- .../coffee/Features/User/UserCreator.coffee | 2 +- .../coffee/Features/User/UserDeleter.coffee | 2 +- .../Features/User/UserEmailsController.coffee | 2 +- .../coffee/Features/User/UserGetter.coffee | 4 +-- .../coffee/Features/User/UserUpdater.coffee | 2 +- .../InstitutionsAPITests.coffee} | 26 +++++++++---------- .../unit/coffee/User/UserCreatorTests.coffee | 2 +- .../unit/coffee/User/UserDeleterTests.coffee | 2 +- .../User/UserEmailsControllerTests.coffee | 2 +- .../unit/coffee/User/UserGetterTests.coffee | 8 +++--- .../unit/coffee/User/UserUpdaterTests.coffee | 2 +- 12 files changed, 32 insertions(+), 32 deletions(-) rename services/web/app/coffee/Features/{User/UserAffiliationsManager.coffee => Institutions/InstitutionsAPI.coffee} (91%) rename services/web/test/unit/coffee/{User/UserAffiliationsManagerTests.coffee => Institutions/InstitutionsAPITests.coffee} (81%) diff --git a/services/web/app/coffee/Features/User/UserAffiliationsManager.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee similarity index 91% rename from services/web/app/coffee/Features/User/UserAffiliationsManager.coffee rename to services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee index 77dad7a8ef..025a82bda0 100644 --- a/services/web/app/coffee/Features/User/UserAffiliationsManager.coffee +++ b/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee @@ -3,12 +3,12 @@ metrics = require("metrics-sharelatex") settings = require "settings-sharelatex" request = require "request" -module.exports = UserAffiliationsManager = - getAffiliations: (userId, callback = (error, body) ->) -> +module.exports = InstitutionsAPI = + getUserAffiliations: (userId, callback = (error, body) ->) -> makeAffiliationRequest { method: 'GET' path: "/api/v2/users/#{userId.toString()}/affiliations" - defaultErrorMessage: "Couldn't get affiliations" + defaultErrorMessage: "Couldn't get user affiliations" }, callback @@ -77,10 +77,10 @@ makeAffiliationRequest = (requestOptions, callback = (error) ->) -> callback(null, body) [ - 'getAffiliations', + 'getUserAffiliations', 'addAffiliation', 'removeAffiliation', ].map (method) -> metrics.timeAsyncMethod( - UserAffiliationsManager, method, 'mongo.UserAffiliationsManager', logger + InstitutionsAPI, method, 'mongo.InstitutionsAPI', logger ) diff --git a/services/web/app/coffee/Features/User/UserCreator.coffee b/services/web/app/coffee/Features/User/UserCreator.coffee index b0676e09e0..2dfa7f5ac0 100644 --- a/services/web/app/coffee/Features/User/UserCreator.coffee +++ b/services/web/app/coffee/Features/User/UserCreator.coffee @@ -1,7 +1,7 @@ User = require("../../models/User").User logger = require("logger-sharelatex") metrics = require('metrics-sharelatex') -{ addAffiliation } = require("./UserAffiliationsManager") +{ addAffiliation } = require("../Institutions/InstitutionsAPI") module.exports = UserCreator = diff --git a/services/web/app/coffee/Features/User/UserDeleter.coffee b/services/web/app/coffee/Features/User/UserDeleter.coffee index c09bebbd8c..149959c00f 100644 --- a/services/web/app/coffee/Features/User/UserDeleter.coffee +++ b/services/web/app/coffee/Features/User/UserDeleter.coffee @@ -4,7 +4,7 @@ ProjectDeleter = require("../Project/ProjectDeleter") logger = require("logger-sharelatex") SubscriptionHandler = require("../Subscription/SubscriptionHandler") async = require("async") -{ deleteAffiliations } = require("./UserAffiliationsManager") +{ deleteAffiliations } = require("../Institutions/InstitutionsAPI") module.exports = diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index 780960c3e2..e37a0452e7 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -3,7 +3,7 @@ UserGetter = require("./UserGetter") UserUpdater = require("./UserUpdater") EmailHelper = require("../Helpers/EmailHelper") UserEmailsConfirmationHandler = require "./UserEmailsConfirmationHandler" -{ endorseAffiliation } = require("./UserAffiliationsManager") +{ endorseAffiliation } = require("../Institutions/InstitutionsAPI") logger = require("logger-sharelatex") Errors = require "../Errors/Errors" diff --git a/services/web/app/coffee/Features/User/UserGetter.coffee b/services/web/app/coffee/Features/User/UserGetter.coffee index 13b81b399c..f3aa4ad4d6 100644 --- a/services/web/app/coffee/Features/User/UserGetter.coffee +++ b/services/web/app/coffee/Features/User/UserGetter.coffee @@ -3,7 +3,7 @@ metrics = require('metrics-sharelatex') logger = require('logger-sharelatex') db = mongojs.db ObjectId = mongojs.ObjectId -{ getAffiliations } = require("./UserAffiliationsManager") +{ getUserAffiliations } = require("../Institutions/InstitutionsAPI") Errors = require("../Errors/Errors") module.exports = UserGetter = @@ -32,7 +32,7 @@ module.exports = UserGetter = return callback error if error? return callback new Error('User not Found') unless user - getAffiliations userId, (error, affiliationsData) -> + getUserAffiliations userId, (error, affiliationsData) -> return callback error if error? callback null, decorateFullEmails(user.email, user.emails, affiliationsData) diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 958d5a4edf..8d5e3658f9 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -5,7 +5,7 @@ db = mongojs.db async = require("async") ObjectId = mongojs.ObjectId UserGetter = require("./UserGetter") -{ addAffiliation, removeAffiliation } = require("./UserAffiliationsManager") +{ addAffiliation, removeAffiliation } = require("../Institutions/InstitutionsAPI") FeaturesUpdater = require("../Subscription/FeaturesUpdater") EmailHelper = require "../Helpers/EmailHelper" Errors = require "../Errors/Errors" diff --git a/services/web/test/unit/coffee/User/UserAffiliationsManagerTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee similarity index 81% rename from services/web/test/unit/coffee/User/UserAffiliationsManagerTests.coffee rename to services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee index ff3d96a283..407c373462 100644 --- a/services/web/test/unit/coffee/User/UserAffiliationsManagerTests.coffee +++ b/services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee @@ -4,16 +4,16 @@ SandboxedModule = require('sandboxed-module') assert = require('assert') path = require('path') sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/User/UserAffiliationsManager" +modulePath = path.join __dirname, "../../../../app/js/Features/Institutions/InstitutionsAPI" expect = require("chai").expect -describe "UserAffiliationsManager", -> +describe "InstitutionsAPI", -> beforeEach -> @logger = err: sinon.stub(), log: -> settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } } @request = sinon.stub() - @UserAffiliationsManager = SandboxedModule.require modulePath, requires: + @InstitutionsAPI = SandboxedModule.require modulePath, requires: "logger-sharelatex": @logger "metrics-sharelatex": timeAsyncMethod: sinon.stub() 'settings-sharelatex': settings @@ -25,11 +25,11 @@ describe "UserAffiliationsManager", -> email:"hello@world.com" @newEmail = "bob@bob.com" - describe 'getAffiliations', -> + describe 'getUserAffiliations', -> it 'get affiliations', (done)-> responseBody = [{ foo: 'bar' }] @request.callsArgWith(1, null, { statusCode: 201 }, responseBody) - @UserAffiliationsManager.getAffiliations @stubbedUser._id, (err, body) => + @InstitutionsAPI.getUserAffiliations @stubbedUser._id, (err, body) => should.not.exist(err) @request.calledOnce.should.equal true requestOptions = @request.lastCall.args[0] @@ -43,7 +43,7 @@ describe "UserAffiliationsManager", -> it 'handle error', (done)-> body = errors: 'affiliation error message' @request.callsArgWith(1, null, { statusCode: 503 }, body) - @UserAffiliationsManager.getAffiliations @stubbedUser._id, (err) => + @InstitutionsAPI.getUserAffiliations @stubbedUser._id, (err) => should.exist(err) err.message.should.have.string 503 err.message.should.have.string body.errors @@ -58,7 +58,7 @@ describe "UserAffiliationsManager", -> university: { id: 1 } role: 'Prof' department: 'Math' - @UserAffiliationsManager.addAffiliation @stubbedUser._id, @newEmail, affiliationOptions, (err)=> + @InstitutionsAPI.addAffiliation @stubbedUser._id, @newEmail, affiliationOptions, (err)=> should.not.exist(err) @request.calledOnce.should.equal true requestOptions = @request.lastCall.args[0] @@ -77,7 +77,7 @@ describe "UserAffiliationsManager", -> it 'handle error', (done)-> body = errors: 'affiliation error message' @request.callsArgWith(1, null, { statusCode: 422 }, body) - @UserAffiliationsManager.addAffiliation @stubbedUser._id, @newEmail, {}, (err)=> + @InstitutionsAPI.addAffiliation @stubbedUser._id, @newEmail, {}, (err)=> should.exist(err) err.message.should.have.string 422 err.message.should.have.string body.errors @@ -88,7 +88,7 @@ describe "UserAffiliationsManager", -> @request.callsArgWith(1, null, { statusCode: 404 }) it 'remove affiliation', (done)-> - @UserAffiliationsManager.removeAffiliation @stubbedUser._id, @newEmail, (err)=> + @InstitutionsAPI.removeAffiliation @stubbedUser._id, @newEmail, (err)=> should.not.exist(err) @request.calledOnce.should.equal true requestOptions = @request.lastCall.args[0] @@ -100,7 +100,7 @@ describe "UserAffiliationsManager", -> it 'handle error', (done)-> @request.callsArgWith(1, null, { statusCode: 500 }) - @UserAffiliationsManager.removeAffiliation @stubbedUser._id, @newEmail, (err)=> + @InstitutionsAPI.removeAffiliation @stubbedUser._id, @newEmail, (err)=> should.exist(err) err.message.should.exist done() @@ -108,7 +108,7 @@ describe "UserAffiliationsManager", -> describe 'deleteAffiliations', -> it 'delete affiliations', (done)-> @request.callsArgWith(1, null, { statusCode: 200 }) - @UserAffiliationsManager.deleteAffiliations @stubbedUser._id, (err) => + @InstitutionsAPI.deleteAffiliations @stubbedUser._id, (err) => should.not.exist(err) @request.calledOnce.should.equal true requestOptions = @request.lastCall.args[0] @@ -120,7 +120,7 @@ describe "UserAffiliationsManager", -> it 'handle error', (done)-> body = errors: 'affiliation error message' @request.callsArgWith(1, null, { statusCode: 518 }, body) - @UserAffiliationsManager.deleteAffiliations @stubbedUser._id, (err) => + @InstitutionsAPI.deleteAffiliations @stubbedUser._id, (err) => should.exist(err) err.message.should.have.string 518 err.message.should.have.string body.errors @@ -131,7 +131,7 @@ describe "UserAffiliationsManager", -> @request.callsArgWith(1, null, { statusCode: 204 }) it 'endorse affiliation', (done)-> - @UserAffiliationsManager.endorseAffiliation @stubbedUser._id, @newEmail, 'Student','Physics', (err)=> + @InstitutionsAPI.endorseAffiliation @stubbedUser._id, @newEmail, 'Student','Physics', (err)=> should.not.exist(err) @request.calledOnce.should.equal true requestOptions = @request.lastCall.args[0] diff --git a/services/web/test/unit/coffee/User/UserCreatorTests.coffee b/services/web/test/unit/coffee/User/UserCreatorTests.coffee index 1945453b9e..f9d88e4cf8 100644 --- a/services/web/test/unit/coffee/User/UserCreatorTests.coffee +++ b/services/web/test/unit/coffee/User/UserCreatorTests.coffee @@ -22,7 +22,7 @@ describe "UserCreator", -> "../../models/User": User:@UserModel "logger-sharelatex":{log:->} 'metrics-sharelatex': {timeAsyncMethod: ()->} - "./UserAffiliationsManager": addAffiliation: @addAffiliation + "../Institutions/InstitutionsAPI": addAffiliation: @addAffiliation @email = "bob.oswald@gmail.com" diff --git a/services/web/test/unit/coffee/User/UserDeleterTests.coffee b/services/web/test/unit/coffee/User/UserDeleterTests.coffee index d2be2d211b..fc5f2ef370 100644 --- a/services/web/test/unit/coffee/User/UserDeleterTests.coffee +++ b/services/web/test/unit/coffee/User/UserDeleterTests.coffee @@ -31,7 +31,7 @@ describe "UserDeleter", -> "../Newsletter/NewsletterManager": @NewsletterManager "../Subscription/SubscriptionHandler": @SubscriptionHandler "../Project/ProjectDeleter": @ProjectDeleter - "./UserAffiliationsManager": + "../Institutions/InstitutionsAPI": deleteAffiliations: @deleteAffiliations "logger-sharelatex": @logger = { log: sinon.stub() } diff --git a/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee b/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee index 2f67131c65..800a44336f 100644 --- a/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee +++ b/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee @@ -34,7 +34,7 @@ describe "UserEmailsController", -> "./UserUpdater": @UserUpdater "../Helpers/EmailHelper": @EmailHelper "./UserEmailsConfirmationHandler": @UserEmailsConfirmationHandler = {} - "./UserAffiliationsManager": endorseAffiliation: @endorseAffiliation + "../Institutions/InstitutionsAPI": endorseAffiliation: @endorseAffiliation "../Errors/Errors": Errors "logger-sharelatex": log: -> console.log(arguments) diff --git a/services/web/test/unit/coffee/User/UserGetterTests.coffee b/services/web/test/unit/coffee/User/UserGetterTests.coffee index c754b9830b..79d9032283 100644 --- a/services/web/test/unit/coffee/User/UserGetterTests.coffee +++ b/services/web/test/unit/coffee/User/UserGetterTests.coffee @@ -22,15 +22,15 @@ describe "UserGetter", -> db: users: findOne: @findOne ObjectId: (id) -> return id settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } } - @getAffiliations = sinon.stub().callsArgWith(1, null, []) + @getUserAffiliations = sinon.stub().callsArgWith(1, null, []) @UserGetter = SandboxedModule.require modulePath, requires: "logger-sharelatex": log:-> "../../infrastructure/mongojs": @Mongo "metrics-sharelatex": timeAsyncMethod: sinon.stub() 'settings-sharelatex': settings - './UserAffiliationsManager': - getAffiliations: @getAffiliations + '../Institutions/InstitutionsAPI': + getUserAffiliations: @getUserAffiliations "../Errors/Errors": Errors describe "getUser", -> @@ -77,7 +77,7 @@ describe "UserGetter", -> institution: { name: 'University Name', isUniversity: true } } ] - @getAffiliations.callsArgWith(1, null, affiliationsData) + @getUserAffiliations.callsArgWith(1, null, affiliationsData) @UserGetter.getUserFullEmails @fakeUser._id, (error, fullEmails) => assert.deepEqual fullEmails, [ { diff --git a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee index ae49d1d780..f2ac951727 100644 --- a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee +++ b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee @@ -25,7 +25,7 @@ describe "UserUpdater", -> @UserUpdater = SandboxedModule.require modulePath, requires: "logger-sharelatex": @logger "./UserGetter": @UserGetter - './UserAffiliationsManager': + '../Institutions/InstitutionsAPI': addAffiliation: @addAffiliation removeAffiliation: @removeAffiliation '../Subscription/FeaturesUpdater': refreshFeatures: @refreshFeatures From 66bcdbcac954d85a3cb956c7939829bc30254439 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Tue, 10 Jul 2018 23:53:06 +0200 Subject: [PATCH 098/188] institution upgrade --- .../Institutions/InstitutionsAPI.coffee | 9 ++++++ .../Institutions/InstitutionsManager.coffee | 17 +++++++++++ .../web/scripts/upgrade_institution_users.js | 16 ++++++++++ .../Institutions/InstitutionsAPITests.coffee | 16 ++++++++++ .../InstitutionsManagerTests.coffee | 30 +++++++++++++++++++ 5 files changed, 88 insertions(+) create mode 100644 services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee create mode 100644 services/web/scripts/upgrade_institution_users.js create mode 100644 services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee index 025a82bda0..8ce39d68e3 100644 --- a/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee +++ b/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee @@ -4,6 +4,14 @@ settings = require "settings-sharelatex" request = require "request" module.exports = InstitutionsAPI = + getInstitutionAffiliations: (institutionId, callback = (error, body) ->) -> + makeAffiliationRequest { + method: 'GET' + path: "/api/v2/institutions/#{institutionId.toString()}/affiliations" + defaultErrorMessage: "Couldn't get institution affiliations" + }, callback + + getUserAffiliations: (userId, callback = (error, body) ->) -> makeAffiliationRequest { method: 'GET' @@ -77,6 +85,7 @@ makeAffiliationRequest = (requestOptions, callback = (error) ->) -> callback(null, body) [ + 'getInstitutionAffiliations' 'getUserAffiliations', 'addAffiliation', 'removeAffiliation', diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee new file mode 100644 index 0000000000..15233c8849 --- /dev/null +++ b/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee @@ -0,0 +1,17 @@ +logger = require 'logger-sharelatex' +async = require 'async' +db = require("../../infrastructure/mongojs").db +ObjectId = require("../../infrastructure/mongojs").ObjectId +{ getInstitutionAffiliations } = require('./InstitutionsAPI') +FeaturesUpdater = require('../Subscription/FeaturesUpdater') + +ASYNC_LIMIT = 10 +module.exports = InstitutionsManager = + upgradeInstitutionUsers: (institutionId, callback = (error) ->) -> + getInstitutionAffiliations institutionId, (error, affiliations) -> + return callback(error) if error + async.eachLimit affiliations, ASYNC_LIMIT, refreshFeatures, callback + +refreshFeatures = (affiliation, callback) -> + userId = ObjectId(affiliation.user_id) + FeaturesUpdater.refreshFeatures(userId, true, callback) diff --git a/services/web/scripts/upgrade_institution_users.js b/services/web/scripts/upgrade_institution_users.js new file mode 100644 index 0000000000..8cf098ca2b --- /dev/null +++ b/services/web/scripts/upgrade_institution_users.js @@ -0,0 +1,16 @@ +const InstitutionsManager = require( + '../app/js/Features/Institutions/InstitutionsManager' +) + +const institutionId = parseInt(process.argv[2]) +if (isNaN(institutionId)) throw new Error('No institution id') +console.log('Upgrading users of institution', institutionId) + +InstitutionsManager.upgradeInstitutionUsers(institutionId, function (error) { + if (error) { + console.log(error) + } else { + console.log('DONE 👌') + } + process.exit() +}) diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee index 407c373462..7500e46e40 100644 --- a/services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee +++ b/services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee @@ -25,6 +25,22 @@ describe "InstitutionsAPI", -> email:"hello@world.com" @newEmail = "bob@bob.com" + describe 'getInstitutionAffiliations', -> + it 'get affiliations', (done)-> + @institutionId = 123 + responseBody = ['123abc', '456def'] + @request.yields(null, { statusCode: 200 }, responseBody) + @InstitutionsAPI.getInstitutionAffiliations @institutionId, (err, body) => + should.not.exist(err) + @request.calledOnce.should.equal true + requestOptions = @request.lastCall.args[0] + expectedUrl = "v1.url/api/v2/institutions/#{@institutionId}/affiliations" + requestOptions.url.should.equal expectedUrl + requestOptions.method.should.equal 'GET' + should.not.exist(requestOptions.body) + body.should.equal responseBody + done() + describe 'getUserAffiliations', -> it 'get affiliations', (done)-> responseBody = [{ foo: 'bar' }] diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee new file mode 100644 index 0000000000..acf0478ae2 --- /dev/null +++ b/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee @@ -0,0 +1,30 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +path = require('path') +sinon = require('sinon') +modulePath = path.join __dirname, "../../../../app/js/Features/Institutions/InstitutionsManager" + +describe "InstitutionsManager", -> + beforeEach -> + @institutionId = 123 + @logger = log: -> + @getInstitutionAffiliations = sinon.stub() + @refreshFeatures = sinon.stub().yields() + @InstitutionsManager = SandboxedModule.require modulePath, requires: + 'logger-sharelatex': @logger + './InstitutionsAPI': + getInstitutionAffiliations: @getInstitutionAffiliations + '../Subscription/FeaturesUpdater': + refreshFeatures: @refreshFeatures + + describe 'upgradeInstitutionUsers', -> + it 'refresh all users Features', (done) -> + affiliations = [ + { user_id: '123abc123abc123abc123abc' } + { user_id: '456def456def456def456def' } + ] + @getInstitutionAffiliations.yields(null, affiliations) + @InstitutionsManager.upgradeInstitutionUsers @institutionId, (error) => + should.not.exist(error) + sinon.assert.calledTwice(@refreshFeatures) + done() From d484c36c6dcb55598ae45f63381beabc459ce2ba Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 26 Jul 2018 16:04:14 +0100 Subject: [PATCH 099/188] improve clsi logging --- services/web/app/coffee/Features/Compile/ClsiManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index c62e2d570d..9a4e61a063 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -117,7 +117,7 @@ module.exports = ClsiManager = statusCodeSame = results.newBackend?.response?.statusCode == results.currentBackend?.response?.statusCode currentCompileTime = results.currentBackend?.finishTime newBackendCompileTime = results.newBackend?.finishTime - logger.log {statusCodeSame, timeDifference, currentCompileTime, newBackendCompileTime}, "both clsi requests returned" + logger.log {statusCodeSame, timeDifference, currentCompileTime, newBackendCompileTime, project_id}, "both clsi requests returned" From d990ae1a1a24080621993b9a4c3a34c72b4313ce Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 25 Jul 2018 13:59:21 -0500 Subject: [PATCH 100/188] Style for portals --- .../web/public/stylesheets/app/portals.less | 161 ++++++++++++++++++ .../components/embed-responsive.less | 26 +++ .../public/stylesheets/components/icons.less | 9 + .../stylesheets/core/_common-variables.less | 9 +- .../public/stylesheets/core/ol-variables.less | 22 +++ services/web/public/stylesheets/ol-style.less | 5 +- 6 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 services/web/public/stylesheets/app/portals.less create mode 100644 services/web/public/stylesheets/components/embed-responsive.less create mode 100644 services/web/public/stylesheets/components/icons.less diff --git a/services/web/public/stylesheets/app/portals.less b/services/web/public/stylesheets/app/portals.less new file mode 100644 index 0000000000..ae6fcdf230 --- /dev/null +++ b/services/web/public/stylesheets/app/portals.less @@ -0,0 +1,161 @@ +.content-portal { + padding-top: @navbar-height!important; + + /* + Begin Header + */ + .banner-image { + background-size: cover; + background-position: 50% 50%; + background-repeat: no-repeat; + height: 375px; + } + + .image-fill { + display: inline-block; + height: 100%; + vertical-align: middle; + } + + .institution-logo { + left: 50%; + margin-left: -100px; + padding: 0; + position: absolute; + div { + background-color: @white; + box-shadow: 1px 11px 22px -9px @black-alpha-strong; + display: inline-block; + height: 125px; + overflow: hidden; + position: absolute; + text-align: center; + top: -110px; + white-space: nowrap; + width: @btn-portal-width; + } + img { + max-height: 75px; + max-width: 150px; + vertical-align: middle; + } + } + + .portal-name { + background-color: @ol-blue-gray-0; + padding-bottom: @line-height-computed; //- center header when no tabs + padding-top: @padding-md; + text-align: center; + width: 100%; + } + // End Header + + /* + Begin Layout + */ + .button-pull, + .content-pull { + float: left; + } + + .button-pull { + text-align: right; + > a.btn { + white-space: normal; + width: @btn-portal-width; + text-align: center; + } + } + .content-pull { + padding-right: @padding-sm; + width: calc(~"100% - "@btn-portal-width); + } + // End Layout + + /* + Begin Card + */ + .card { + margin-bottom: @margin-md; + } + // End Card + + /* + Begin Actions + */ + .portal-actions { + i { + margin-bottom: @margin-sm; + } + } + // End Actions + + /* + Begin Print + */ + .print { + .hidden-print { + display: none; + } + } + // End Print + + /* + Begin Tabs + */ + .nav-tabs { + // Overrides for nav.less + background-color: @ol-blue-gray-0; + border: 0!important; + margin-bottom: @margin-md; + margin-top: -@line-height-computed; //- adjusted for portal-name + padding: @padding-lg 0 @padding-md; + text-align: center; + + a { + color: @link-color; + &:hover { + background-color: transparent!important; + border: 0!important; + color: @link-hover-color!important; + } + } + + li { + display: inline-block; + float: none; + a { + border: 0; + } + } + + li.active > a { + background-color: transparent!important; + border: 0; + border-bottom: 1px solid @accent-color-secondary!important; + color: @accent-color-secondary; + &:hover { + color: @accent-color-secondary!important; + } + } + } + + .tab-content:extend(.container) { + background-color: transparent!important; + border: none!important; + } + // End Tabs + + @media (max-width: @screen-size-sm-max) { + .content-pull { + padding: 0; + width: auto; + } + + .button-pull { + > a.btn { + width: auto; + } + } + } +} diff --git a/services/web/public/stylesheets/components/embed-responsive.less b/services/web/public/stylesheets/components/embed-responsive.less new file mode 100644 index 0000000000..997f100449 --- /dev/null +++ b/services/web/public/stylesheets/components/embed-responsive.less @@ -0,0 +1,26 @@ +.embed-responsive { + display: block; + height: 0; + overflow: hidden; + padding: 0; + position: relative; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} +.embed-responsive-16by9 { + padding-bottom: 56.25% !important; +} +.embed-responsive-4by3 { + padding-bottom: 75% !important; +} \ No newline at end of file diff --git a/services/web/public/stylesheets/components/icons.less b/services/web/public/stylesheets/components/icons.less new file mode 100644 index 0000000000..fa808ad5ae --- /dev/null +++ b/services/web/public/stylesheets/components/icons.less @@ -0,0 +1,9 @@ +// Colors +.icon-accent { + color: @accent-color-secondary; +} + +// Sizes +.icon-lg { + font-size: @font-size-h1; +} \ No newline at end of file diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index fefe12da73..db6fbf3a7e 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -62,9 +62,12 @@ // //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). -@margin-sm: 10px; -@margin-md: 20px; -@margin-lg: 30px; +@margin-xs: 5px; +@margin-sm: 10px; +@margin-md: 20px; +@margin-lg: 30px; +@margin-xl: 40px; +@margin-xxl: 50px; @padding-base-vertical: 5px; @padding-base-horizontal: 16px; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 0e05ccdd68..d3c21dff6a 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -6,6 +6,7 @@ @footer-height: 50px; // Styleguide colors +@ol-blue-gray-0 : #f4f5f8; @ol-blue-gray-1 : #E4E8EE; @ol-blue-gray-2 : #9DA7B7; @ol-blue-gray-3 : #5D6879; @@ -21,6 +22,8 @@ @ol-dark-red : #A6312B; @ol-type-color : @ol-blue-gray-3; +@accent-color-primary: @ol-green; +@accent-color-secondary: @ol-dark-green; // Navbar customization @navbar-title-color : @ol-blue-gray-1; @@ -65,8 +68,14 @@ @btn-info-bg : @ol-blue; @btn-info-border : transparent; +// Padding @padding-xs-horizontal : 8px; +@padding-sm: 10px; +@padding-md: 20px; +@padding-lg: 30px; +@padding-xl: 40px; + // Alerts @alert-padding : 15px; @alert-border-radius : @border-radius-base; @@ -167,6 +176,9 @@ @folders-tag-menu-hover : rgba(0, 0, 0, .1); @folders-tag-menu-active-hover : rgba(0, 0, 0, .1); +// Portal +@btn-portal-width : 200px; + // Project table @structured-list-line-height : 2.5; @structured-list-link-color : @ol-blue; @@ -273,6 +285,9 @@ @log-line-no-color : #FFF; @log-hints-color : @ol-blue-gray-4; +// Portals +@black-alpha-strong : rgba(0,0,0,0.8); + // v2 History @history-base-font-size : @font-size-small; @@ -284,6 +299,12 @@ @history-toolbar-bg-color : @editor-toolbar-bg; @history-toolbar-color : #FFF; +// Screens +// added -size to not conflict with common_variables +@screen-size-sm-max : 767px; +@screen-size-md-min : 768px; +@screen-size-md-max : 991px; + // System messages @sys-msg-background : @ol-blue; @sys-msg-color : #FFF; @@ -300,6 +321,7 @@ @gray-light: #a4a4a4; @gray-lighter: #cfcfcf; @gray-lightest: #f0f0f0; +@white: #ffffff; @blue: #405ebf; @blueDark: #040D2D; diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less index 03e2bb3ed6..774e70a2ab 100644 --- a/services/web/public/stylesheets/ol-style.less +++ b/services/web/public/stylesheets/ol-style.less @@ -6,8 +6,11 @@ @import "app/ol-style-guide.less"; @import "_style_includes.less"; @import "_ol_style_includes.less"; +@import "components/embed-responsive.less"; +@import "components/icons.less"; // Pages @import "app/about.less"; @import "app/blog-posts.less"; -@import "app/cms-page.less"; \ No newline at end of file +@import "app/cms-page.less"; +@import "app/portals.less"; \ No newline at end of file From c7941ac00e474cab6dede5003ef1f09e80ce3f61 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 25 Jul 2018 14:39:40 -0500 Subject: [PATCH 101/188] Links mixins --- services/web/app/views/_mixins_links.pug | 75 ++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 services/web/app/views/_mixins_links.pug diff --git a/services/web/app/views/_mixins_links.pug b/services/web/app/views/_mixins_links.pug new file mode 100644 index 0000000000..18b208270c --- /dev/null +++ b/services/web/app/views/_mixins_links.pug @@ -0,0 +1,75 @@ +mixin linkAdvisors(linkText, linkClass, tracked) + //- To Do: verify path + //- To Do: track + a(href="/advisors" + class=linkClass ? linkClass : '' + eventTrackingGa=track ? 'advisors' : null + ) + | #{linkText ? linkText : 'advisor programme'} + +mixin linkBenefits(linkText, linkClass) + //- To Do: verify path + a(href="/benefits" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'benefits'} + +mixin linkBlog(linkText, linkClass, slug) + if slug + a(href="/blog/#{slug}" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'blog'} + +mixin linkContact(linkText, linkClass) + a(href="/contact" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'contact'} + +mixin linkEducation(linkText, linkClass) + //- To Do: verify path + a(href="/plans" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'teaching toolkit'} + +mixin linkEmail(linkText, linkClass, email) + //- To Do: env var? + - var emailDomain = 'overleaf.com' + a(href="mailto:#{email ? email : 'contact'}@#{emailDomain}" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'email'} + +mixin linkInvite(linkText, linkClass) + a(href="/user/bonus" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'invite your friends'} + +mixin linkPlansAndPricing(linkText, linkClass) + //- To Do: verify path + a(href="/plans" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'plans and pricing'} + +mixin linkPrintNewTab(linkText, linkClass, icon, track) + //- To Do: track + a(href='?media=print' + class=linkClass ? linkClass : '' + eventTrackingGa=track ? 'print' : null + target="_BLANK" + ) + if icon + i(class="fa fa-print") + |   + | #{linkText ? linkText : 'print'} + +mixin linkSignIn(linkText, linkClass) + a(href="/login" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'sign in'} + +mixin linkSignUp(linkText, linkClass) + a(href="/register" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'sign up'} + +mixin linkTweet(linkText, linkClass, tweetText, track) + //- twitter-share-button is required by twitter + //- To Do: track + a(class="twitter-share-button " + linkClass + href="https://twitter.com/intent/tweet?text=" + tweetText + target="_BLANK" + ) #{linkText ? linkText : 'tweet'} + +mixin linkUniversities(linkText, linkClass) + //- To Do: verify path + a(href="/universities" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'universities'} From 96a49a7fe6c574c89fcb8dfe81bef82e00faf1a5 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Thu, 26 Jul 2018 12:53:28 +0100 Subject: [PATCH 102/188] cleaning up date range picker icons and buttons --- .../components/daterange-picker.less | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/services/web/public/stylesheets/components/daterange-picker.less b/services/web/public/stylesheets/components/daterange-picker.less index 2d15d2a36d..f0dcc01d27 100644 --- a/services/web/public/stylesheets/components/daterange-picker.less +++ b/services/web/public/stylesheets/components/daterange-picker.less @@ -34,7 +34,7 @@ @daterangepicker-in-range-bg-color: #ebf4f8; @daterangepicker-active-color: #fff; -@daterangepicker-active-bg-color: #a93529; +@daterangepicker-active-bg-color: #4F9C45; @daterangepicker-active-border-color: transparent; @daterangepicker-unselected-color: #999; @@ -393,13 +393,14 @@ .daterangepicker_input { position: relative; + padding-left: 0; i { position: absolute; // NOTE: These appear to be eyeballed to me... left: 8px; - top: 8px; + top: 10px; } } &.rtl { @@ -476,6 +477,19 @@ /* Larger Screen Styling */ @media (min-width: 564px) { .daterangepicker { + .glyphicon { + font-family: FontAwesome; + } + .glyphicon-chevron-left:before{ + content: "\f053"; + } + .glyphicon-chevron-right:before{ + content: "\f054"; + } + .glyphicon-calendar:before{ + content: "\f073"; + } + width: auto; .ranges { From 4a22129aa0d94afbf201177186f648e57c9ef628 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Thu, 26 Jul 2018 16:50:48 +0100 Subject: [PATCH 103/188] add missing v1 metrics styling for graphs and updates --- .../public/stylesheets/_style_includes.less | 2 + .../web/public/stylesheets/app/metrics.less | 5 + .../public/stylesheets/components/nvd3.less | 674 ++++++++++++++++++ .../stylesheets/components/nvd3_override.less | 16 + 4 files changed, 697 insertions(+) create mode 100755 services/web/public/stylesheets/components/nvd3.less create mode 100644 services/web/public/stylesheets/components/nvd3_override.less diff --git a/services/web/public/stylesheets/_style_includes.less b/services/web/public/stylesheets/_style_includes.less index 968304ba3d..88a4ad59bc 100644 --- a/services/web/public/stylesheets/_style_includes.less +++ b/services/web/public/stylesheets/_style_includes.less @@ -43,6 +43,8 @@ @import "components/hover.less"; @import "components/ui-select.less"; @import "components/input-suggestions.less"; +@import "components/nvd3.less"; +@import "components/nvd3_override.less"; // Components w/ JavaScript @import "components/modals.less"; diff --git a/services/web/public/stylesheets/app/metrics.less b/services/web/public/stylesheets/app/metrics.less index 23d121088d..244021bcad 100644 --- a/services/web/public/stylesheets/app/metrics.less +++ b/services/web/public/stylesheets/app/metrics.less @@ -87,6 +87,11 @@ margin-top: 0; margin-bottom: 0; } + + .metric-tooltip { + top: -1em; + font-size: .5em; + } } // END: Metrics header diff --git a/services/web/public/stylesheets/components/nvd3.less b/services/web/public/stylesheets/components/nvd3.less new file mode 100755 index 0000000000..5ddcdc7a01 --- /dev/null +++ b/services/web/public/stylesheets/components/nvd3.less @@ -0,0 +1,674 @@ +/* nvd3 version 1.8.4 (https://github.com/novus/nvd3) 2016-07-03 */ +.nvd3 .nv-axis { + pointer-events:none; + opacity: 1; +} + +.nvd3 .nv-axis path { + fill: none; + stroke: #000; + stroke-opacity: .75; + shape-rendering: crispEdges; +} + +.nvd3 .nv-axis path.domain { + stroke-opacity: .75; +} + +.nvd3 .nv-axis.nv-x path.domain { + stroke-opacity: 0; +} + +.nvd3 .nv-axis line { + fill: none; + stroke: #e5e5e5; + shape-rendering: crispEdges; +} + +.nvd3 .nv-axis .zero line, + /*this selector may not be necessary*/ .nvd3 .nv-axis line.zero { + stroke-opacity: .75; +} + +.nvd3 .nv-axis .nv-axisMaxMin text { + font-weight: bold; +} + +.nvd3 .x .nv-axis .nv-axisMaxMin text, +.nvd3 .x2 .nv-axis .nv-axisMaxMin text, +.nvd3 .x3 .nv-axis .nv-axisMaxMin text { + text-anchor: middle +} + +.nvd3 .nv-axis.nv-disabled { + opacity: 0; +} + +.nvd3 .nv-bars rect { + fill-opacity: .75; + + transition: fill-opacity 250ms linear; + -moz-transition: fill-opacity 250ms linear; + -webkit-transition: fill-opacity 250ms linear; +} + +.nvd3 .nv-bars rect.hover { + fill-opacity: 1; +} + +.nvd3 .nv-bars .hover rect { + fill: lightblue; +} + +.nvd3 .nv-bars text { + fill: rgba(0,0,0,0); +} + +.nvd3 .nv-bars .hover text { + fill: rgba(0,0,0,1); +} + +.nvd3 .nv-multibar .nv-groups rect, +.nvd3 .nv-multibarHorizontal .nv-groups rect, +.nvd3 .nv-discretebar .nv-groups rect { + stroke-opacity: 0; + + transition: fill-opacity 250ms linear; + -moz-transition: fill-opacity 250ms linear; + -webkit-transition: fill-opacity 250ms linear; +} + +.nvd3 .nv-multibar .nv-groups rect:hover, +.nvd3 .nv-multibarHorizontal .nv-groups rect:hover, +.nvd3 .nv-candlestickBar .nv-ticks rect:hover, +.nvd3 .nv-discretebar .nv-groups rect:hover { + fill-opacity: 1; +} + +.nvd3 .nv-discretebar .nv-groups text, +.nvd3 .nv-multibarHorizontal .nv-groups text { + font-weight: bold; + fill: rgba(0,0,0,1); + stroke: rgba(0,0,0,0); +} + +/* boxplot CSS */ +.nvd3 .nv-boxplot circle { + fill-opacity: 0.5; +} + +.nvd3 .nv-boxplot circle:hover { + fill-opacity: 1; +} + +.nvd3 .nv-boxplot rect:hover { + fill-opacity: 1; +} + +.nvd3 line.nv-boxplot-median { + stroke: black; +} + +.nv-boxplot-tick:hover { + stroke-width: 2.5px; +} +/* bullet */ +.nvd3.nv-bullet { font: 10px sans-serif; } +.nvd3.nv-bullet .nv-measure { fill-opacity: .8; } +.nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; } +.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; } +.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; } +.nvd3.nv-bullet .nv-markerLine { stroke: #000; stroke-width: 1.5px; } +.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; } +.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; } +.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; } +.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; } +.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; } +.nvd3.nv-bullet .nv-subtitle { fill: #999; } + + +.nvd3.nv-bullet .nv-range { + fill: #bababa; + fill-opacity: .4; +} +.nvd3.nv-bullet .nv-range:hover { + fill-opacity: .7; +} + +.nvd3.nv-candlestickBar .nv-ticks .nv-tick { + stroke-width: 1px; +} + +.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover { + stroke-width: 2px; +} + +.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect { + stroke: #2ca02c; + fill: #2ca02c; +} + +.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect { + stroke: #d62728; + fill: #d62728; +} + +.with-transitions .nv-candlestickBar .nv-ticks .nv-tick { + transition: stroke-width 250ms linear, stroke-opacity 250ms linear; + -moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; + -webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; + +} + +.nvd3.nv-candlestickBar .nv-ticks line { + stroke: #333; +} + + +.nv-force-node { + stroke: #fff; + stroke-width: 1.5px; +} +.nv-force-link { + stroke: #999; + stroke-opacity: .6; +} +.nv-force-node text { + stroke-width: 0px +} + +.nvd3 .nv-legend .nv-disabled rect { + /*fill-opacity: 0;*/ +} + +.nvd3 .nv-check-box .nv-box { + fill-opacity:0; + stroke-width:2; +} + +.nvd3 .nv-check-box .nv-check { + fill-opacity:0; + stroke-width:4; +} + +.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check { + fill-opacity:0; + stroke-opacity:0; +} + +.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check { + opacity: 0; +} + +/* line plus bar */ +.nvd3.nv-linePlusBar .nv-bar rect { + fill-opacity: .75; +} + +.nvd3.nv-linePlusBar .nv-bar rect:hover { + fill-opacity: 1; +} +.nvd3 .nv-groups path.nv-line { + fill: none; +} + +.nvd3 .nv-groups path.nv-area { + stroke: none; +} + +.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point { + fill-opacity: 0; + stroke-opacity: 0; +} + +.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point { + fill-opacity: .5 !important; + stroke-opacity: .5 !important; +} + + +.with-transitions .nvd3 .nv-groups .nv-point { + transition: stroke-width 250ms linear, stroke-opacity 250ms linear; + -moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; + -webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; + +} + +.nvd3.nv-scatter .nv-groups .nv-point.hover, +.nvd3 .nv-groups .nv-point.hover { + stroke-width: 7px; + fill-opacity: .95 !important; + stroke-opacity: .95 !important; +} + + +.nvd3 .nv-point-paths path { + stroke: #aaa; + stroke-opacity: 0; + fill: #eee; + fill-opacity: 0; +} + + + +.nvd3 .nv-indexLine { + cursor: ew-resize; +} + +/******************** + * SVG CSS + */ + +/******************** + Default CSS for an svg element nvd3 used +*/ +svg.nvd3-svg { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -ms-user-select: none; + -moz-user-select: none; + user-select: none; + display: block; + width:100%; + height:100%; +} + +/******************** + Box shadow and border radius styling +*/ +.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip { + -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); + -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); + box-shadow: 0 5px 10px rgba(0,0,0,.2); + + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + + +.nvd3 text { + font: normal 12px Arial; +} + +.nvd3 .title { + font: bold 14px Arial; +} + +.nvd3 .nv-background { + fill: white; + fill-opacity: 0; +} + +.nvd3.nv-noData { + font-size: 18px; + font-weight: bold; +} + + +/********** +* Brush +*/ + +.nv-brush .extent { + fill-opacity: .125; + shape-rendering: crispEdges; +} + +.nv-brush .resize path { + fill: #eee; + stroke: #666; +} + + +/********** +* Legend +*/ + +.nvd3 .nv-legend .nv-series { + cursor: pointer; +} + +.nvd3 .nv-legend .nv-disabled circle { + fill-opacity: 0; +} + +/* focus */ +.nvd3 .nv-brush .extent { + fill-opacity: 0 !important; +} + +.nvd3 .nv-brushBackground rect { + stroke: #000; + stroke-width: .4; + fill: #fff; + fill-opacity: .7; +} + +/********** +* Print +*/ + +@media print { + .nvd3 text { + stroke-width: 0; + fill-opacity: 1; + } +} + +.nvd3.nv-ohlcBar .nv-ticks .nv-tick { + stroke-width: 1px; +} + +.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover { + stroke-width: 2px; +} + +.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive { + stroke: #2ca02c; +} + +.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative { + stroke: #d62728; +} + + +.nvd3 .background path { + fill: none; + stroke: #EEE; + stroke-opacity: .4; + shape-rendering: crispEdges; +} + +.nvd3 .foreground path { + fill: none; + stroke-opacity: .7; +} + +.nvd3 .nv-parallelCoordinates-brush .extent +{ + fill: #fff; + fill-opacity: .6; + stroke: gray; + shape-rendering: crispEdges; +} + +.nvd3 .nv-parallelCoordinates .hover { + fill-opacity: 1; + stroke-width: 3px; +} + + +.nvd3 .missingValuesline line { + fill: none; + stroke: black; + stroke-width: 1; + stroke-opacity: 1; + stroke-dasharray: 5, 5; +} +.nvd3.nv-pie path { + stroke-opacity: 0; + transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; + -moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; + -webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; + +} + +.nvd3.nv-pie .nv-pie-title { + font-size: 24px; + fill: rgba(19, 196, 249, 0.59); +} + +.nvd3.nv-pie .nv-slice text { + stroke: #000; + stroke-width: 0; +} + +.nvd3.nv-pie path { + stroke: #fff; + stroke-width: 1px; + stroke-opacity: 1; +} + +.nvd3.nv-pie path { + fill-opacity: .7; +} +.nvd3.nv-pie .hover path { + fill-opacity: 1; +} +.nvd3.nv-pie .nv-label { + pointer-events: none; +} +.nvd3.nv-pie .nv-label rect { + fill-opacity: 0; + stroke-opacity: 0; +} + +/* scatter */ +.nvd3 .nv-groups .nv-point.hover { + stroke-width: 20px; + stroke-opacity: .5; +} + +.nvd3 .nv-scatter .nv-point.hover { + fill-opacity: 1; +} +.nv-noninteractive { + pointer-events: none; +} + +.nv-distx, .nv-disty { + pointer-events: none; +} + +/* sparkline */ +.nvd3.nv-sparkline path { + fill: none; +} + +.nvd3.nv-sparklineplus g.nv-hoverValue { + pointer-events: none; +} + +.nvd3.nv-sparklineplus .nv-hoverValue line { + stroke: #333; + stroke-width: 1.5px; +} + +.nvd3.nv-sparklineplus, +.nvd3.nv-sparklineplus g { + pointer-events: all; +} + +.nvd3 .nv-hoverArea { + fill-opacity: 0; + stroke-opacity: 0; +} + +.nvd3.nv-sparklineplus .nv-xValue, +.nvd3.nv-sparklineplus .nv-yValue { + stroke-width: 0; + font-size: .9em; + font-weight: normal; +} + +.nvd3.nv-sparklineplus .nv-yValue { + stroke: #f66; +} + +.nvd3.nv-sparklineplus .nv-maxValue { + stroke: #2ca02c; + fill: #2ca02c; +} + +.nvd3.nv-sparklineplus .nv-minValue { + stroke: #d62728; + fill: #d62728; +} + +.nvd3.nv-sparklineplus .nv-currentValue { + font-weight: bold; + font-size: 1.1em; +} +/* stacked area */ +.nvd3.nv-stackedarea path.nv-area { + fill-opacity: .7; + stroke-opacity: 0; + transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; + -moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; + -webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; +} + +.nvd3.nv-stackedarea path.nv-area.hover { + fill-opacity: .9; +} + + +.nvd3.nv-stackedarea .nv-groups .nv-point { + stroke-opacity: 0; + fill-opacity: 0; +} + + +.nvtooltip { + position: absolute; + background-color: rgba(255,255,255,1.0); + color: rgba(0,0,0,1.0); + padding: 1px; + border: 1px solid rgba(0,0,0,.2); + z-index: 10000; + display: block; + + font-family: Arial; + font-size: 13px; + text-align: left; + pointer-events: none; + + white-space: nowrap; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.nvtooltip { + background: rgba(255,255,255, 0.8); + border: 1px solid rgba(0,0,0,0.5); + border-radius: 4px; +} + +/*Give tooltips that old fade in transition by + putting a "with-transitions" class on the container div. +*/ +.nvtooltip.with-transitions, .with-transitions .nvtooltip { + transition: opacity 50ms linear; + -moz-transition: opacity 50ms linear; + -webkit-transition: opacity 50ms linear; + + transition-delay: 200ms; + -moz-transition-delay: 200ms; + -webkit-transition-delay: 200ms; +} + +.nvtooltip.x-nvtooltip, +.nvtooltip.y-nvtooltip { + padding: 8px; +} + +.nvtooltip h3 { + margin: 0; + padding: 4px 14px; + line-height: 18px; + font-weight: normal; + background-color: rgba(247,247,247,0.75); + color: rgba(0,0,0,1.0); + text-align: center; + + border-bottom: 1px solid #ebebeb; + + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.nvtooltip p { + margin: 0; + padding: 5px 14px; + text-align: center; +} + +.nvtooltip span { + display: inline-block; + margin: 2px 0; +} + +.nvtooltip table { + margin: 6px; + border-spacing:0; +} + + +.nvtooltip table td { + padding: 2px 9px 2px 0; + vertical-align: middle; +} + +.nvtooltip table td.key { + font-weight: normal; +} +.nvtooltip table td.key.total { + font-weight: bold; +} +.nvtooltip table td.value { + text-align: right; + font-weight: bold; +} + +.nvtooltip table td.percent { + color: darkgray; +} + +.nvtooltip table tr.highlight td { + padding: 1px 9px 1px 0; + border-bottom-style: solid; + border-bottom-width: 1px; + border-top-style: solid; + border-top-width: 1px; +} + +.nvtooltip table td.legend-color-guide div { + width: 8px; + height: 8px; + vertical-align: middle; +} + +.nvtooltip table td.legend-color-guide div { + width: 12px; + height: 12px; + border: 1px solid #999; +} + +.nvtooltip .footer { + padding: 3px; + text-align: center; +} + +.nvtooltip-pending-removal { + pointer-events: none; + display: none; +} + + +/**** +Interactive Layer +*/ +.nvd3 .nv-interactiveGuideLine { + pointer-events:none; +} +.nvd3 line.nv-guideline { + stroke: #ccc; +} diff --git a/services/web/public/stylesheets/components/nvd3_override.less b/services/web/public/stylesheets/components/nvd3_override.less new file mode 100644 index 0000000000..2a84f27ddc --- /dev/null +++ b/services/web/public/stylesheets/components/nvd3_override.less @@ -0,0 +1,16 @@ +.nvd3 { + .nv-axis { + .tick { + line { + opacity: 0; + } + } + path.domain { + opacity: 0; + } + } +} + +svg.nvd3-iddle { + @extend svg.nvd3-svg; +} From 2dcd2339ed71248148e6ba8bda9175a703b165c0 Mon Sep 17 00:00:00 2001 From: Nate Stemen Date: Thu, 26 Jul 2018 15:03:55 -0400 Subject: [PATCH 104/188] still trigger autocomplete if % is escaped --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 638ab20b30..e91c92f643 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -162,7 +162,7 @@ define [ cursorPosition = @editor.getCursorPosition() end = change.end {lineUpToCursor, commandFragment} = Helpers.getContext(@editor, end) - if lineUpToCursor.match(/.*%.*/) + if lineUpToCursor.match(/.*((?![\\]).|^)%.*/) return lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\" lastTwoChars = lineUpToCursor.slice(-2) From ddbe1a5b22366d917237692acd5f36a1ae020e24 Mon Sep 17 00:00:00 2001 From: Michael Mazour Date: Fri, 27 Jul 2018 15:42:37 +0100 Subject: [PATCH 105/188] Add compiler and imageName to export API request --- .../web/app/coffee/Features/Exports/ExportsHandler.coffee | 3 +++ .../test/unit/coffee/Exports/ExportsHandlerTests.coffee | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee index 17a0798ead..0594b9fc2b 100644 --- a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee +++ b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee @@ -57,6 +57,9 @@ module.exports = ExportsHandler = self = historyId: project.overleaf?.history?.id historyVersion: historyVersion v1ProjectId: project.overleaf?.id + metadata: + compiler: project.compiler + imageName: project.imageName user: id: user_id firstName: user.first_name diff --git a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee index a210532e9b..974e8e0c1b 100644 --- a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee +++ b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee @@ -63,6 +63,8 @@ describe 'ExportsHandler', -> beforeEach (done) -> @project = id: @project_id + compiler: 'pdflatex' + imageName: 'mock-image-name' overleaf: id: @project_history_id # for projects imported from v1 history: @@ -100,6 +102,9 @@ describe 'ExportsHandler', -> historyId: @project_history_id historyVersion: @historyVersion v1ProjectId: @project_history_id + metadata: + compiler: 'pdflatex' + imageName: 'mock-image-name' user: id: @user_id firstName: @user.first_name @@ -132,6 +137,9 @@ describe 'ExportsHandler', -> historyId: @project_history_id historyVersion: @historyVersion v1ProjectId: @project_history_id + metadata: + compiler: 'pdflatex' + imageName: 'mock-image-name' user: id: @user_id firstName: @custom_first_name From b04c7d2ffe08922794049c2b1d5363f2f33d2f38 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 30 Jul 2018 18:16:52 +0100 Subject: [PATCH 106/188] send clsi response codes to graphite --- services/web/app/coffee/Features/Compile/ClsiManager.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 9a4e61a063..59822c7547 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -100,6 +100,7 @@ module.exports = ClsiManager = timer = new Metrics.Timer("compile.currentBackend") request opts, (err, response, body)-> timer.done() + metrics.inc "compile.currentBackend.response.#{response.statusCode}" if err? logger.err err:err, project_id:project_id, url:opts?.url, "error making request to clsi" return callback(err) @@ -111,6 +112,7 @@ module.exports = ClsiManager = newBackend: (cb)-> startTime = new Date() ClsiManager._makeNewBackendRequest project_id, opts, (err, response, body)-> + metrics.inc "compile.newBackend.response.#{response.statusCode}" cb(err, {response:response, body:body, finishTime:new Date() - startTime}) }, (err, results)-> timeDifference = results.newBackend?.finishTime - results.currentBackend?.finishTime From 3532a29d50e733d951b5691202c341a2a264294e Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Mon, 30 Jul 2018 19:14:59 +0100 Subject: [PATCH 107/188] allow fetching of export zips --- .../Features/Exports/ExportsController.coffee | 7 +++++++ .../Features/Exports/ExportsHandler.coffee | 16 ++++++++++++++++ services/web/app/coffee/router.coffee | 1 + 3 files changed, 24 insertions(+) diff --git a/services/web/app/coffee/Features/Exports/ExportsController.coffee b/services/web/app/coffee/Features/Exports/ExportsController.coffee index 7c74d14aed..fab953c298 100644 --- a/services/web/app/coffee/Features/Exports/ExportsController.coffee +++ b/services/web/app/coffee/Features/Exports/ExportsController.coffee @@ -37,3 +37,10 @@ module.exports = status_detail: parsed_export.status_detail } res.send export_json: json + + exportZip: (req, res) -> + {export_id} = req.params + ExportsHandler.fetchZip export_id, (err, export_zip_url) -> + return err if err? + + res.redirect export_zip_url diff --git a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee index 17a0798ead..32873bb6a0 100644 --- a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee +++ b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee @@ -115,3 +115,19 @@ module.exports = ExportsHandler = self = err = new Error("v1 export returned a failure status code: #{res.statusCode}") logger.err err:err, export:export_id, "v1 export returned failure status code: #{res.statusCode}" callback err + + fetchZip: (export_id, callback=(err, zip_url) ->) -> + console.log("#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/zip_url") + request.get { + url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/zip_url" + auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass } + }, (err, res, body) -> + if err? + logger.err err:err, export:export_id, "error making request to v1 export" + callback err + else if 200 <= res.statusCode < 300 + callback null, body + else + err = new Error("v1 export returned a failure status code: #{res.statusCode}") + logger.err err:err, export:export_id, "v1 export zip fetch returned failure status code: #{res.statusCode}" + callback err diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index f35cd6c0ab..f48882a634 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -245,6 +245,7 @@ module.exports = class Router webRouter.post '/project/:project_id/export/:brand_variation_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportProject webRouter.get '/project/:project_id/export/:export_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportStatus + webRouter.get '/project/:project_id/export/:export_id/zip', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportZip webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects From 395406407e917123b0f32fb29077e1173b54aa97 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Mon, 30 Jul 2018 19:39:35 +0100 Subject: [PATCH 108/188] add test for export fetch zip --- .../coffee/Exports/ExportsHandlerTests.coffee | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee index a210532e9b..50c95282f5 100644 --- a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee +++ b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee @@ -273,3 +273,32 @@ describe 'ExportsHandler', -> @callback.calledWith(null, { body: @body }) .should.equal true + describe 'fetchZip', -> + beforeEach (done) -> + @settings.apis = + v1: + url: 'http://localhost:5000' + user: 'overleaf' + pass: 'pass' + @export_id = 897 + @body = "https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee" + @stubGet = sinon.stub().yields(null, {statusCode: 200}, { body: @body }) + done() + + describe "when all goes well", -> + beforeEach (done) -> + @stubRequest.get = @stubGet + @ExportsHandler.fetchZip @export_id, (error, body) => + @callback(error, body) + done() + + it 'should issue the request', -> + expect(@stubGet.getCall(0).args[0]).to.deep.equal + url: @settings.apis.v1.url + '/api/v1/sharelatex/exports/' + @export_id + '/zip_url' + auth: + user: @settings.apis.v1.user + pass: @settings.apis.v1.pass + + it 'should return the v1 export id', -> + @callback.calledWith(null, { body: @body }) + .should.equal true From 8792e600d2d0dab130b33345fa2cc886368f40cd Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Tue, 31 Jul 2018 12:43:39 +0100 Subject: [PATCH 109/188] authenticate publish menu zip downloads --- .../web/app/coffee/Features/Exports/ExportsController.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/Features/Exports/ExportsController.coffee b/services/web/app/coffee/Features/Exports/ExportsController.coffee index fab953c298..e724951d2d 100644 --- a/services/web/app/coffee/Features/Exports/ExportsController.coffee +++ b/services/web/app/coffee/Features/Exports/ExportsController.coffee @@ -40,6 +40,7 @@ module.exports = exportZip: (req, res) -> {export_id} = req.params + AuthenticationController.getLoggedInUserId(req) ExportsHandler.fetchZip export_id, (err, export_zip_url) -> return err if err? From 5bb3acc1ebaccfce5e9746a4b04d67dbd8defd5a Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 31 Jul 2018 12:56:35 +0100 Subject: [PATCH 110/188] fix metrics sending to clsi --- services/web/app/coffee/Features/Compile/ClsiManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 59822c7547..bfa508a3d9 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -100,7 +100,7 @@ module.exports = ClsiManager = timer = new Metrics.Timer("compile.currentBackend") request opts, (err, response, body)-> timer.done() - metrics.inc "compile.currentBackend.response.#{response.statusCode}" + Metrics.inc "compile.currentBackend.response.#{response.statusCode}" if err? logger.err err:err, project_id:project_id, url:opts?.url, "error making request to clsi" return callback(err) @@ -112,7 +112,7 @@ module.exports = ClsiManager = newBackend: (cb)-> startTime = new Date() ClsiManager._makeNewBackendRequest project_id, opts, (err, response, body)-> - metrics.inc "compile.newBackend.response.#{response.statusCode}" + Metrics.inc "compile.newBackend.response.#{response.statusCode}" cb(err, {response:response, body:body, finishTime:new Date() - startTime}) }, (err, results)-> timeDifference = results.newBackend?.finishTime - results.currentBackend?.finishTime From 89efa7e83ed681564f32f43928dc22ca7ab7cc62 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 31 Jul 2018 13:07:57 +0100 Subject: [PATCH 111/188] add null check on response --- services/web/app/coffee/Features/Compile/ClsiManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index bfa508a3d9..8d5c5b2baa 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -100,7 +100,7 @@ module.exports = ClsiManager = timer = new Metrics.Timer("compile.currentBackend") request opts, (err, response, body)-> timer.done() - Metrics.inc "compile.currentBackend.response.#{response.statusCode}" + Metrics.inc "compile.currentBackend.response.#{response?.statusCode}" if err? logger.err err:err, project_id:project_id, url:opts?.url, "error making request to clsi" return callback(err) @@ -112,7 +112,7 @@ module.exports = ClsiManager = newBackend: (cb)-> startTime = new Date() ClsiManager._makeNewBackendRequest project_id, opts, (err, response, body)-> - Metrics.inc "compile.newBackend.response.#{response.statusCode}" + Metrics.inc "compile.newBackend.response.#{response?.statusCode}" cb(err, {response:response, body:body, finishTime:new Date() - startTime}) }, (err, results)-> timeDifference = results.newBackend?.finishTime - results.currentBackend?.finishTime From 05aedf1e099e92b9c792ab579a01d1165cdbef82 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 31 Jul 2018 16:54:09 +0100 Subject: [PATCH 112/188] add script to increase paid compile timeouts to 240s --- .../web/scripts/increase_compile_timeouts.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 services/web/scripts/increase_compile_timeouts.js diff --git a/services/web/scripts/increase_compile_timeouts.js b/services/web/scripts/increase_compile_timeouts.js new file mode 100644 index 0000000000..8aa451545a --- /dev/null +++ b/services/web/scripts/increase_compile_timeouts.js @@ -0,0 +1,64 @@ +const mongojs = require('../app/js/infrastructure/mongojs') +const { db } = mongojs +const async = require('async') +const minilist = require('minimist') + +const newTimeout = 240 +const oldTimeoutLimits = {$gt: 60, $lt: 240} + +const updateUser = function (user, callback) { + console.log(`Updating user ${user._id}`) + const update = { + $set: { + 'features.compileTimeout': newTimeout + } + } + db.users.update({ + _id: user._id, + 'features.compileTimeout': oldTimeoutLimits + }, update, callback) +} + +const updateUsers = (users, callback) => + async.eachLimit(users, ASYNC_LIMIT, updateUser, function (error) { + if (error) { + callback(error) + return + } + counter += users.length + console.log(`${counter} users updated`) + loopForUsers(callback) + }) + +var loopForUsers = callback => + db.users.find( + { 'features.compileTimeout': oldTimeoutLimits }, + { 'features.compileTimeout': 1 } + ).limit(FETCH_LIMIT, function (error, users) { + if (error) { + callback(error) + return + } + if (users.length === 0) { + console.log(`DONE (${counter} users updated)`) + return callback() + } + updateUsers(users, callback) + }) + +var counter = 0 +var run = () => + loopForUsers(function (error) { + if (error) { throw error } + process.exit() + }) + +let FETCH_LIMIT, ASYNC_LIMIT +var setup = function () { + let args = minilist(process.argv.slice(2)) + FETCH_LIMIT = (args.fetch) ? args.fetch : 100 + ASYNC_LIMIT = (args.async) ? args.async : 10 +} + +setup() +run() From 7c9c0fbf06dc2420866e9e5441763b058a21fa0d Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 2 Aug 2018 13:49:56 +0100 Subject: [PATCH 113/188] add --all option for increase_compile_timeouts script --- services/web/scripts/increase_compile_timeouts.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/services/web/scripts/increase_compile_timeouts.js b/services/web/scripts/increase_compile_timeouts.js index 8aa451545a..69323134e9 100644 --- a/services/web/scripts/increase_compile_timeouts.js +++ b/services/web/scripts/increase_compile_timeouts.js @@ -26,8 +26,13 @@ const updateUsers = (users, callback) => return } counter += users.length - console.log(`${counter} users updated`) - loopForUsers(callback) + console.log(`${counter} users updated`); + if (DO_ALL) { + return loopForUsers(callback) + } else { + console.log('run again to continue updating'); + return callback() + } }) var loopForUsers = callback => @@ -53,11 +58,15 @@ var run = () => process.exit() }) -let FETCH_LIMIT, ASYNC_LIMIT +let FETCH_LIMIT, ASYNC_LIMIT, DO_ALL var setup = function () { let args = minilist(process.argv.slice(2)) + // --fetch N get N users each time FETCH_LIMIT = (args.fetch) ? args.fetch : 100 + // --async M run M updates in parallel ASYNC_LIMIT = (args.async) ? args.async : 10 + // --all means run to completion + DO_ALL = (args.all) } setup() From d6ab6ce6eececf0627b4439d98fc9418e5550916 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 2 Aug 2018 14:06:51 +0100 Subject: [PATCH 114/188] improve --all switch to remove fetch limit --- .../web/scripts/increase_compile_timeouts.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/services/web/scripts/increase_compile_timeouts.js b/services/web/scripts/increase_compile_timeouts.js index 69323134e9..6889f26962 100644 --- a/services/web/scripts/increase_compile_timeouts.js +++ b/services/web/scripts/increase_compile_timeouts.js @@ -30,7 +30,7 @@ const updateUsers = (users, callback) => if (DO_ALL) { return loopForUsers(callback) } else { - console.log('run again to continue updating'); + console.log('*** run again to continue updating ***') return callback() } }) @@ -66,7 +66,19 @@ var setup = function () { // --async M run M updates in parallel ASYNC_LIMIT = (args.async) ? args.async : 10 // --all means run to completion - DO_ALL = (args.all) + if (args.all) { + if (args.fetch) { + console.error('error: do not use --fetch with --all') + process.exit(1) + } else { + DO_ALL = true + // if we are updating for all users then ignore the fetch limit. + FETCH_LIMIT = 0 + // A limit() value of 0 (i.e. .limit(0)) is equivalent to setting + // no limit. + // https://docs.mongodb.com/manual/reference/method/cursor.limit + } + } } setup() From b9597358a959b22e9b2b002b5f8ff1108b57e57e Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 2 Aug 2018 14:14:23 +0100 Subject: [PATCH 115/188] lint fix --- services/web/scripts/increase_compile_timeouts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/scripts/increase_compile_timeouts.js b/services/web/scripts/increase_compile_timeouts.js index 6889f26962..185c563277 100644 --- a/services/web/scripts/increase_compile_timeouts.js +++ b/services/web/scripts/increase_compile_timeouts.js @@ -26,7 +26,7 @@ const updateUsers = (users, callback) => return } counter += users.length - console.log(`${counter} users updated`); + console.log(`${counter} users updated`) if (DO_ALL) { return loopForUsers(callback) } else { From 3a80c3407891c828c227fecdfcee6aa6678dbcd4 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 26 Jul 2018 16:49:04 +0100 Subject: [PATCH 116/188] Basic label support (showing labels in the entries list; creating labels via a modal). --- .../project/editor/history/entriesListV2.pug | 6 +++ .../project/editor/history/toolbarV2.pug | 47 +++++++++++++++++-- .../ide/history/HistoryV2Manager.coffee | 39 +++++++++++---- .../HistoryV2AddLabelModalController.coffee | 28 +++++++++++ .../HistoryV2ToolbarController.coffee | 10 ++++ services/web/public/robots.txt | 21 ++++++++- .../stylesheets/app/editor/history-v2.less | 10 ++++ .../stylesheets/core/_common-variables.less | 1 + .../public/stylesheets/core/ol-variables.less | 1 + 9 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 services/web/public/coffee/ide/history/controllers/HistoryV2AddLabelModalController.coffee create mode 100644 services/web/public/coffee/ide/history/controllers/HistoryV2ToolbarController.coffee diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index fa7a90b20e..155ad6a3fd 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -129,6 +129,12 @@ script(type="text/ng-template", id="historyEntryTpl") time.history-entry-day(ng-if="::$ctrl.entry.meta.first_in_day") {{ ::$ctrl.entry.meta.end_ts | relativeDate }} .history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })") + .history-entry-label( + ng-if="$ctrl.entry.labels.length > 0" + ng-repeat="label in ::$ctrl.entry.labels" + ) + i.fa.fa-tag + |  {{ label.comment }} ol.history-entry-changes li.history-entry-change( ng-repeat="pathname in ::$ctrl.entry.pathnames" diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index 799a7136f3..b53e54f95c 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -1,4 +1,5 @@ .history-toolbar( + ng-controller="HistoryV2ToolbarController" ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME" ) span(ng-show="history.loadingFileTree") @@ -6,8 +7,46 @@ |    #{translate("loading")}... span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}  time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }} - .history-toolbar-btn( - ng-click="toggleHistoryViewMode();" + button.history-toolbar-btn( + ng-click="showAddLabelDialog();" + ng-disabled="history.loadingFileTree" ) - i.fa - | #{translate("compare_to_another_version")} \ No newline at end of file + i.fa.fa-tag + |  Label this version + button.history-toolbar-btn( + ng-click="toggleHistoryViewMode();" + ng-disabled="history.loadingFileTree" + ) + i.fa.fa-exchange + |  #{translate("compare_to_another_version")} + +script(type="text/ng-template", id="historyV2AddLabelModalTemplate") + form( + name="addLabelModalForm" + ng-submit="addLabelModalFormSubmit();" + novalidate + ) + .modal-header + h3 Label this version + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + .form-group + input.form-control( + type="text" + placeholder="New label name" + ng-model="inputs.labelName" + focus-on="open" + required + ) + .modal-footer + button.btn.btn-default( + type="button" + ng-disabled="state.inflight" + ng-click="$dismiss()" + ) #{translate("cancel")} + input.btn.btn-primary( + ng-disabled="addLabelModalForm.$invalid || state.inflight" + ng-value="state.inflight ? 'Adding label' : 'Add label'" + type="submit" + ) \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index 7c8f476e39..e3623bb1fa 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -6,6 +6,8 @@ define [ "ide/history/controllers/HistoryV2ListController" "ide/history/controllers/HistoryV2DiffController" "ide/history/controllers/HistoryV2FileTreeController" + "ide/history/controllers/HistoryV2ToolbarController" + "ide/history/controllers/HistoryV2AddLabelModalController" "ide/history/directives/infiniteScroll" "ide/history/components/historyEntriesList" "ide/history/components/historyEntry" @@ -68,6 +70,7 @@ define [ toV: null } } + labels: null files: [] diff: null # When history.viewMode == HistoryViewModes.COMPARE selectedFile: null # When history.viewMode == HistoryViewModes.POINT_IN_TIME @@ -126,18 +129,25 @@ define [ BATCH_SIZE: 10 fetchNextBatchOfUpdates: () -> - url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}" + updatesURL = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}" if @$scope.history.nextBeforeTimestamp? - url += "&before=#{@$scope.history.nextBeforeTimestamp}" + updatesURL += "&before=#{@$scope.history.nextBeforeTimestamp}" + @$scope.history.loading = true @$scope.history.loadingFileTree = true - @ide.$http - .get(url) - .then (response) => - { data } = response - @_loadUpdates(data.updates) - @$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp - if !data.nextBeforeTimestamp? + + requests = + updates: @ide.$http.get updatesURL + + if !@$scope.history.labels? + requests.labels = @ide.$http.get "/project/#{@ide.project_id}/labels" + + @ide.$q.all requests + .then (responses) => + updatesData = responses.updates.data + @_loadUpdates(updatesData.updates) + @$scope.history.nextBeforeTimestamp = updatesData.nextBeforeTimestamp + if !updatesData.nextBeforeTimestamp? @$scope.history.atEnd = true @$scope.history.loading = false @@ -199,6 +209,9 @@ define [ diff.loading = false diff.error = true + labelCurrentVersion: (labelComment) => + @_labelVersion labelComment, @$scope.history.selection.updates[0].toV + _parseDiff: (diff) -> if diff.binary return { binary: true } @@ -278,6 +291,14 @@ define [ else @autoSelectLastUpdate() + _labelVersion: (comment, version) -> + url = "/project/#{@$scope.project_id}/labels" + @ide.$http.post url, { + comment, + version, + _csrf: window.csrfToken + } + _perDocSummaryOfUpdates: (updates) -> # Track current_pathname -> original_pathname # create bare object for use as Map diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2AddLabelModalController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2AddLabelModalController.coffee new file mode 100644 index 0000000000..39b83d8998 --- /dev/null +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2AddLabelModalController.coffee @@ -0,0 +1,28 @@ +define [ + "base", +], (App) -> + App.controller "HistoryV2AddLabelModalController", ["$scope", "$modalInstance", "ide", ($scope, $modalInstance, ide) -> + $scope.inputs = + labelName: null + $scope.state = + inflight: false + error: false + + $modalInstance.opened.then () -> + $scope.$applyAsync () -> + $scope.$broadcast "open" + + $scope.addLabelModalFormSubmit = () -> + $scope.state.inflight = true + ide.historyManager.labelCurrentVersion $scope.inputs.labelName + .then (response) -> + $scope.state.inflight = false + $modalInstance.close() + .catch (response) -> + { data, status } = response + $scope.state.inflight = false + if status == 400 + $scope.state.error = { message: data } + else + $scope.state.error = true + ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ToolbarController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ToolbarController.coffee new file mode 100644 index 0000000000..ed8dee63ec --- /dev/null +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ToolbarController.coffee @@ -0,0 +1,10 @@ +define [ + "base", +], (App) -> + App.controller "HistoryV2ToolbarController", ["$scope", "$modal", "ide", ($scope, $modal, ide) -> + $scope.showAddLabelDialog = () -> + $modal.open( + templateUrl: "historyV2AddLabelModalTemplate" + controller: "HistoryV2AddLabelModalController" + ) + ] \ No newline at end of file diff --git a/services/web/public/robots.txt b/services/web/public/robots.txt index 77470cb39f..e241feebcc 100644 --- a/services/web/public/robots.txt +++ b/services/web/public/robots.txt @@ -1,2 +1,21 @@ +# robots.txt for https://www.sharelatex.com/ + User-agent: * -Disallow: / \ No newline at end of file +Disallow: /project/* +Disallow: /github/repos/* +Disallow: /recurly.com +Disallow: /user/password/set +Disallow: /learn-scripts/* +Allow: / + +User-Agent: AhrefsBot +Disallow: / + +User-Agent: XoviBot +Disallow: / + +User-Agent: RankSonicBot +Disallow: / + +User-Agent: SMTBot +Disallow: / diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index 9089ed1ae9..3e0d5f7ebc 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -53,6 +53,16 @@ color: #FFF; } } + + .history-entry-label { + color: @history-entry-label-color; + font-weight: bold; + margin-bottom: 3px; + .history-entry-selected & { + color: #FFF; + } + } + .history-entry-changes { .list-unstyled; margin-bottom: 3px; diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index fefe12da73..ac2afdca45 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -988,6 +988,7 @@ // v2 History @history-base-font-size : @font-size-small; @history-base-bg : @gray-lightest; +@history-entry-label-color : @red; @history-entry-day-bg : @gray; @history-entry-selected-bg : @red; @history-base-color : @gray-light; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 0e05ccdd68..40e53a0c71 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -277,6 +277,7 @@ // v2 History @history-base-font-size : @font-size-small; @history-base-bg : @ol-blue-gray-1; +@history-entry-label-color : @ol-blue; @history-entry-day-bg : @ol-blue-gray-2; @history-entry-selected-bg : @ol-green; @history-base-color : @ol-blue-gray-2; From 5a64313e245606315ab9c5c09625fcf6d81079fd Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 1 Aug 2018 12:17:29 +0100 Subject: [PATCH 117/188] Add label delete functionality; styles. --- .../project/editor/history/entriesListV2.pug | 12 +++++-- .../project/editor/history/toolbarV2.pug | 29 +++++++++++++++-- .../ide/history/HistoryV2Manager.coffee | 31 +++++++++++++------ .../components/historyEntriesList.coffee | 1 + .../history/components/historyEntry.coffee | 1 + .../HistoryV2AddLabelModalController.coffee | 3 +- ...HistoryV2DeleteLabelModalController.coffee | 23 ++++++++++++++ .../HistoryV2ListController.coffee | 11 +++++-- .../HistoryV2ToolbarController.coffee | 2 ++ .../stylesheets/app/editor/history-v2.less | 30 ++++++++++++++++-- .../stylesheets/core/_common-variables.less | 21 +++++++------ .../public/stylesheets/core/ol-variables.less | 21 +++++++------ 12 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index 155ad6a3fd..ba7492663c 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -10,6 +10,7 @@ aside.change-list( load-initialize="ui.view == 'history'" is-loading="history.loading" on-entry-select="handleEntrySelect(selectedEntry)" + on-label-delete="handleLabelDelete(label)" ) aside.change-list( @@ -107,6 +108,7 @@ script(type="text/ng-template", id="historyEntriesListTpl") entry="entry" current-user="$ctrl.currentUser" on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })" + on-label-delete="$ctrl.onLabelDelete({ label: label })" ng-show="!$ctrl.isLoading" ) .loading(ng-show="$ctrl.isLoading") @@ -133,8 +135,14 @@ script(type="text/ng-template", id="historyEntryTpl") ng-if="$ctrl.entry.labels.length > 0" ng-repeat="label in ::$ctrl.entry.labels" ) - i.fa.fa-tag - |  {{ label.comment }} + span.history-entry-label-comment + i.fa.fa-tag + |  {{ label.comment }} + button.history-entry-label-delete-btn( + stop-propagation="click" + ng-click="$ctrl.onLabelDelete({ label: label })" + ) × + ol.history-entry-changes li.history-entry-change( ng-repeat="pathname in ::$ctrl.entry.pathnames" diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index b53e54f95c..1b8b733154 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -27,7 +27,7 @@ script(type="text/ng-template", id="historyV2AddLabelModalTemplate") novalidate ) .modal-header - h3 Label this version + h3 Add label .modal-body .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} @@ -39,6 +39,9 @@ script(type="text/ng-template", id="historyV2AddLabelModalTemplate") focus-on="open" required ) + p.help-block(ng-if="update") + | A new label will be added as of + strong {{ update.meta.end_ts | formatDate:'ddd Do MMM YYYY, h:mm a' }} .modal-footer button.btn.btn-default( type="button" @@ -49,4 +52,26 @@ script(type="text/ng-template", id="historyV2AddLabelModalTemplate") ng-disabled="addLabelModalForm.$invalid || state.inflight" ng-value="state.inflight ? 'Adding label' : 'Add label'" type="submit" - ) \ No newline at end of file + ) + +script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate") + .modal-header + h3 Delete label + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + p.help-block(ng-if="labelDetails") + | Are you sure you want to delete the following label: + strong "{{ labelDetails.comment }}" + | ? + .modal-footer + button.btn.btn-default( + type="button" + ng-disabled="state.inflight" + ng-click="$dismiss()" + ) #{translate("cancel")} + button.btn.btn-primary( + type="button" + ng-click="deleteLabel()" + ng-disabled="state.inflight" + ) {{ state.inflight ? 'Deleting label' : 'Delete label' }} \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index e3623bb1fa..3a8640ac9a 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -8,6 +8,7 @@ define [ "ide/history/controllers/HistoryV2FileTreeController" "ide/history/controllers/HistoryV2ToolbarController" "ide/history/controllers/HistoryV2AddLabelModalController" + "ide/history/controllers/HistoryV2DeleteLabelModalController" "ide/history/directives/infiniteScroll" "ide/history/components/historyEntriesList" "ide/history/components/historyEntry" @@ -136,15 +137,9 @@ define [ @$scope.history.loading = true @$scope.history.loadingFileTree = true - requests = - updates: @ide.$http.get updatesURL - - if !@$scope.history.labels? - requests.labels = @ide.$http.get "/project/#{@ide.project_id}/labels" - - @ide.$q.all requests - .then (responses) => - updatesData = responses.updates.data + @ide.$http.get updatesURL + .then (response) => + updatesData = response.data @_loadUpdates(updatesData.updates) @$scope.history.nextBeforeTimestamp = updatesData.nextBeforeTimestamp if !updatesData.nextBeforeTimestamp? @@ -212,6 +207,24 @@ define [ labelCurrentVersion: (labelComment) => @_labelVersion labelComment, @$scope.history.selection.updates[0].toV + deleteLabel: (labelId) => + url = "/project/#{@$scope.project_id}/labels/#{labelId}" + + @ide.$http({ + url, + method: "DELETE" + headers: + "X-CSRF-Token": window.csrfToken + }).then (response) => + @_deleteLabelFromLocalCollection @$scope.history.updates, labelId + @_deleteLabelFromLocalCollection @$scope.history.selection, labelId + + + _deleteLabelFromLocalCollection: (collection, labelId) -> + for update in collection + update.labels = _.filter update.labels, (label) -> + label.id != labelId + _parseDiff: (diff) -> if diff.binary return { binary: true } diff --git a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee index 5022724714..82331258ec 100644 --- a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee @@ -14,6 +14,7 @@ define [ isLoading: "<" currentUser: "<" onEntrySelect: "&" + onLabelDelete: "&" controller: historyEntriesListController templateUrl: "historyEntriesListTpl" } diff --git a/services/web/public/coffee/ide/history/components/historyEntry.coffee b/services/web/public/coffee/ide/history/components/historyEntry.coffee index e2692b7dee..46018d3a32 100644 --- a/services/web/public/coffee/ide/history/components/historyEntry.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntry.coffee @@ -22,6 +22,7 @@ define [ entry: "<" currentUser: "<" onSelect: "&" + onLabelDelete: "&" controller: historyEntryController templateUrl: "historyEntryTpl" } \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2AddLabelModalController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2AddLabelModalController.coffee index 39b83d8998..cbe72b0dce 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2AddLabelModalController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2AddLabelModalController.coffee @@ -1,7 +1,8 @@ define [ "base", ], (App) -> - App.controller "HistoryV2AddLabelModalController", ["$scope", "$modalInstance", "ide", ($scope, $modalInstance, ide) -> + App.controller "HistoryV2AddLabelModalController", ["$scope", "$modalInstance", "ide", "update", ($scope, $modalInstance, ide, update) -> + $scope.update = update $scope.inputs = labelName: null $scope.state = diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee new file mode 100644 index 0000000000..d03786c2c6 --- /dev/null +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee @@ -0,0 +1,23 @@ +define [ + "base", +], (App) -> + App.controller "HistoryV2DeleteLabelModalController", ["$scope", "$modalInstance", "ide", "labelDetails", ($scope, $modalInstance, ide, labelDetails) -> + $scope.labelDetails = labelDetails + $scope.state = + inflight: false + error: false + + $scope.deleteLabel = () -> + $scope.state.inflight = true + ide.historyManager.deleteLabel labelDetails.id + .then (response) -> + $scope.state.inflight = false + $modalInstance.close() + .catch (response) -> + { data, status } = response + $scope.state.inflight = false + if status == 400 + $scope.state.error = { message: data } + else + $scope.state.error = true + ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee index eaf7fbc884..d63f9329ae 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee @@ -3,17 +3,24 @@ define [ "ide/history/util/displayNameForUser" ], (App, displayNameForUser) -> - App.controller "HistoryV2ListController", ["$scope", "ide", ($scope, ide) -> + App.controller "HistoryV2ListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) -> $scope.hoveringOverListSelectors = false $scope.loadMore = () => ide.historyManager.fetchNextBatchOfUpdates() $scope.handleEntrySelect = (entry) -> - # $scope.$applyAsync () -> ide.historyManager.selectUpdate(entry) $scope.recalculateSelectedUpdates() + $scope.handleLabelDelete = (labelDetails) -> + $modal.open( + templateUrl: "historyV2DeleteLabelModalTemplate" + controller: "HistoryV2DeleteLabelModalController" + resolve: + labelDetails: () -> labelDetails + ) + $scope.recalculateSelectedUpdates = () -> beforeSelection = true afterSelection = false diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ToolbarController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ToolbarController.coffee index ed8dee63ec..3feb9f8a7d 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2ToolbarController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ToolbarController.coffee @@ -6,5 +6,7 @@ define [ $modal.open( templateUrl: "historyV2AddLabelModalTemplate" controller: "HistoryV2AddLabelModalController" + resolve: + update: () -> $scope.history.selection.updates[0] ) ] \ No newline at end of file diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index 3e0d5f7ebc..9e099422c5 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -56,12 +56,38 @@ .history-entry-label { color: @history-entry-label-color; - font-weight: bold; + font-size: @font-size-small; margin-bottom: 3px; + margin-right: 10px; + &:last-of-type { + margin-right: 0; + } .history-entry-selected & { - color: #FFF; + color: @history-entry-selected-label-color; } } + .history-entry-label-comment, + .history-entry-label-delete-btn { + display: inline-block; + padding: 0 (@padding-xs-horizontal / 2) 1px @padding-xs-horizontal; + border: 0; + background-color: @history-entry-label-bg-color; + border-radius: 9999px 0 0 9999px; + .history-entry-selected & { + background-color: @history-entry-selected-label-bg-color; + } + } + .history-entry-label-delete-btn { + padding-left: (@padding-xs-horizontal / 2); + padding-right: @padding-xs-horizontal; + border-radius: 0 9999px 9999px 0; + &:hover { + background-color: darken(@history-entry-label-bg-color, 8%); + .history-entry-selected & { + background-color: darken(@history-entry-selected-label-bg-color, 8%); + } + } + } .history-entry-changes { .list-unstyled; diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index ac2afdca45..aef1f2b5a2 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -986,15 +986,18 @@ @sys-msg-border : 1px solid @common-border-color; // v2 History -@history-base-font-size : @font-size-small; -@history-base-bg : @gray-lightest; -@history-entry-label-color : @red; -@history-entry-day-bg : @gray; -@history-entry-selected-bg : @red; -@history-base-color : @gray-light; -@history-highlight-color : @gray; -@history-toolbar-bg-color : @toolbar-alt-bg-color; -@history-toolbar-color : @text-color; +@history-base-font-size : @font-size-small; +@history-base-bg : @gray-lightest; +@history-entry-label-bg-color : @red; +@history-entry-label-color : #FFF; +@history-entry-selected-label-bg-color : #FFF; +@history-entry-selected-label-color : @red; +@history-entry-day-bg : @gray; +@history-entry-selected-bg : @red; +@history-base-color : @gray-light; +@history-highlight-color : @gray; +@history-toolbar-bg-color : @toolbar-alt-bg-color; +@history-toolbar-color : @text-color; // Input suggestions @input-suggestion-v-offset : 6px; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 40e53a0c71..b75de6086d 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -275,15 +275,18 @@ // v2 History -@history-base-font-size : @font-size-small; -@history-base-bg : @ol-blue-gray-1; -@history-entry-label-color : @ol-blue; -@history-entry-day-bg : @ol-blue-gray-2; -@history-entry-selected-bg : @ol-green; -@history-base-color : @ol-blue-gray-2; -@history-highlight-color : @ol-type-color; -@history-toolbar-bg-color : @editor-toolbar-bg; -@history-toolbar-color : #FFF; +@history-base-font-size : @font-size-small; +@history-base-bg : @ol-blue-gray-1; +@history-entry-label-bg-color : @ol-blue; +@history-entry-label-color : #FFF; +@history-entry-selected-label-bg-color: #FFF; +@history-entry-selected-label-color : @ol-blue; +@history-entry-day-bg : @ol-blue-gray-2; +@history-entry-selected-bg : @ol-green; +@history-base-color : @ol-blue-gray-2; +@history-highlight-color : @ol-type-color; +@history-toolbar-bg-color : @editor-toolbar-bg; +@history-toolbar-color : #FFF; // System messages @sys-msg-background : @ol-blue; From d21f6ef62452ce560758834d4818774c091d129d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 1 Aug 2018 16:03:45 +0100 Subject: [PATCH 118/188] Label handling in compare mode. --- .../project/editor/history/entriesListV2.pug | 15 ++++++++++++++- .../coffee/ide/history/HistoryV2Manager.coffee | 18 +++++++++++++----- .../controllers/HistoryListController.coffee | 10 +++++++++- .../stylesheets/app/editor/history-v2.less | 1 + 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index ba7492663c..4cbed71fda 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -66,6 +66,18 @@ aside.change-list( ) div.description(ng-click="select()") + div + .history-entry-label( + ng-if="update.labels.length > 0" + ng-repeat="label in update.labels" + ) + span.history-entry-label-comment + i.fa.fa-tag + |  {{ label.comment }} + button.history-entry-label-delete-btn( + stop-propagation="click" + ng-click="deleteLabel(label)" + ) × div.time {{ update.meta.end_ts | formatDate:'h:mm a' }} div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0") | #{translate("file_action_edited")} @@ -133,7 +145,7 @@ script(type="text/ng-template", id="historyEntryTpl") .history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })") .history-entry-label( ng-if="$ctrl.entry.labels.length > 0" - ng-repeat="label in ::$ctrl.entry.labels" + ng-repeat="label in $ctrl.entry.labels" ) span.history-entry-label-comment i.fa.fa-tag @@ -162,6 +174,7 @@ script(type="text/ng-template", id="historyEntryTpl") ng-if="::project_op.remove" ) #{translate("file_action_deleted")} span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }} + .history-entry-metadata time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }} span diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index 3a8640ac9a..a921cacb08 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -306,11 +306,19 @@ define [ _labelVersion: (comment, version) -> url = "/project/#{@$scope.project_id}/labels" - @ide.$http.post url, { - comment, - version, - _csrf: window.csrfToken - } + @ide.$http + .post url, { + comment, + version, + _csrf: window.csrfToken + } + .then (response) => + @_addLabelToLocalUpdate response.data + + _addLabelToLocalUpdate: (label) -> + localUpdate = _.find @$scope.history.updates, (update) -> update.toV == label.version + if localUpdate? + localUpdate.labels.push label _perDocSummaryOfUpdates: (updates) -> # Track current_pathname -> original_pathname diff --git a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index 4b0786d259..e4bc2b541d 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -3,9 +3,17 @@ define [ "ide/history/util/displayNameForUser" ], (App, displayNameForUser) -> - App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) -> + App.controller "HistoryListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) -> $scope.hoveringOverListSelectors = false + $scope.deleteLabel = (labelDetails) -> + $modal.open( + templateUrl: "historyV2DeleteLabelModalTemplate" + controller: "HistoryV2DeleteLabelModalController" + resolve: + labelDetails: () -> labelDetails + ) + $scope.loadMore = () => ide.historyManager.fetchNextBatchOfUpdates() diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index 9e099422c5..7ed37d49b1 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -55,6 +55,7 @@ } .history-entry-label { + display: inline-block; color: @history-entry-label-color; font-size: @font-size-small; margin-bottom: 3px; From 8c50e4e9aec4fbe76d145cd14d4ecfe95d8047f7 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 2 Aug 2018 15:28:59 +0100 Subject: [PATCH 119/188] Add history label component. --- .../ide/history/components/historyLabel.coffee | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 services/web/public/coffee/ide/history/components/historyLabel.coffee diff --git a/services/web/public/coffee/ide/history/components/historyLabel.coffee b/services/web/public/coffee/ide/history/components/historyLabel.coffee new file mode 100644 index 0000000000..2394b76f8b --- /dev/null +++ b/services/web/public/coffee/ide/history/components/historyLabel.coffee @@ -0,0 +1,17 @@ +define [ + "base" +], (App) -> + historyLabelController = ($scope, $element, $attrs, $filter, _) -> + ctrl = @ + return + + App.component "historyLabel", { + bindings: + labelText: "<" + labelOwnerName: "<" + labelCreationDateTime: "<" + isOwnedByCurrentUser: "<" + onLabelDelete: "&" + controller: historyLabelController + templateUrl: "historyLabelTpl" + } \ No newline at end of file From a8ee87974653e9f679f8034573f9d0288c290b5e Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 2 Aug 2018 15:29:12 +0100 Subject: [PATCH 120/188] Use history label component; restrict label deletion to label owners. --- .../project/editor/history/entriesListV2.pug | 46 ++++++++++++++----- .../ide/history/HistoryV2Manager.coffee | 1 + .../components/historyEntriesList.coffee | 4 ++ .../history/components/historyEntry.coffee | 9 +++- .../HistoryV2ListController.coffee | 3 ++ .../stylesheets/app/editor/history-v2.less | 37 ++++++++++++--- 6 files changed, 82 insertions(+), 18 deletions(-) diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index 4cbed71fda..b96c74947d 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -5,10 +5,12 @@ aside.change-list( history-entries-list( entries="history.updates" current-user="user" + users="projectUsers" load-entries="loadMore()" load-disabled="history.loading || history.atEnd" load-initialize="ui.view == 'history'" is-loading="history.loading" + show-only-labelled="listConfig.showOnlyLabelled" on-entry-select="handleEntrySelect(selectedEntry)" on-label-delete="handleLabelDelete(label)" ) @@ -116,9 +118,10 @@ script(type="text/ng-template", id="historyEntriesListTpl") ) .infinite-scroll-inner history-entry( - ng-repeat="entry in $ctrl.entries" + ng-repeat="entry in $ctrl.entries | filter:$ctrl.shouldShowEntry" entry="entry" current-user="$ctrl.currentUser" + users="$ctrl.users" on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })" on-label-delete="$ctrl.onLabelDelete({ label: label })" ng-show="!$ctrl.isLoading" @@ -143,17 +146,14 @@ script(type="text/ng-template", id="historyEntryTpl") time.history-entry-day(ng-if="::$ctrl.entry.meta.first_in_day") {{ ::$ctrl.entry.meta.end_ts | relativeDate }} .history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })") - .history-entry-label( - ng-if="$ctrl.entry.labels.length > 0" + history-label( ng-repeat="label in $ctrl.entry.labels" + label-text="label.comment" + label-owner-name="$ctrl.displayNameById(label.user_id)" + label-creation-date-time="label.created_at" + is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" + on-label-delete="$ctrl.onLabelDelete({ label: label })" ) - span.history-entry-label-comment - i.fa.fa-tag - |  {{ label.comment }} - button.history-entry-label-delete-btn( - stop-propagation="click" - ng-click="$ctrl.onLabelDelete({ label: label })" - ) × ol.history-entry-changes li.history-entry-change( @@ -198,4 +198,28 @@ script(type="text/ng-template", id="historyEntryTpl") li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0") span.name( ng-style="$ctrl.getUserCSSStyle();" - ) #{translate("anonymous")} \ No newline at end of file + ) #{translate("anonymous")} + +script(type="text/ng-template", id="historyLabelTpl") + .history-entry-label( + ng-class="{ 'history-entry-label-own' : $ctrl.isOwnedByCurrentUser }" + ) + span.history-entry-label-comment( + tooltip-template="'historyLabelTooltipTpl'" + tooltip-placement="left" + ) + i.fa.fa-tag + |  {{ $ctrl.labelText }} + button.history-entry-label-delete-btn( + ng-if="$ctrl.isOwnedByCurrentUser" + stop-propagation="click" + ng-click="$ctrl.onLabelDelete()" + ) × + +script(type="text/ng-template", id="historyLabelTooltipTpl") + .history-label-tooltip + p.history-label-tooltip-title + i.fa.fa-tag + |  {{ $ctrl.labelText }} + p.history-label-tooltip-owner Created by {{ $ctrl.labelOwnerName }} + time.history-label-tooltip-datetime {{ labelCreationDateTime | formatDate }} \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index a921cacb08..7ff41985f4 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -12,6 +12,7 @@ define [ "ide/history/directives/infiniteScroll" "ide/history/components/historyEntriesList" "ide/history/components/historyEntry" + "ide/history/components/historyLabel" "ide/history/components/historyFileTree" "ide/history/components/historyFileEntity" ], (moment, ColorManager, displayNameForUser, HistoryViewModes) -> diff --git a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee index 82331258ec..f872b5d72a 100644 --- a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee @@ -3,11 +3,14 @@ define [ ], (App) -> historyEntriesListController = ($scope, $element, $attrs) -> ctrl = @ + ctrl.shouldShowEntry = (entry) -> + !(ctrl.showOnlyLabelled and entry.labels.length == 0) return App.component "historyEntriesList", { bindings: entries: "<" + users: "<" loadEntries: "&" loadDisabled: "<" loadInitialize: "<" @@ -15,6 +18,7 @@ define [ currentUser: "<" onEntrySelect: "&" onLabelDelete: "&" + showOnlyLabelled: "<" controller: historyEntriesListController templateUrl: "historyEntriesListTpl" } diff --git a/services/web/public/coffee/ide/history/components/historyEntry.coffee b/services/web/public/coffee/ide/history/components/historyEntry.coffee index 46018d3a32..2995c5f481 100644 --- a/services/web/public/coffee/ide/history/components/historyEntry.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntry.coffee @@ -2,9 +2,15 @@ define [ "base" "ide/history/util/displayNameForUser" ], (App, displayNameForUser) -> - historyEntryController = ($scope, $element, $attrs) -> + historyEntryController = ($scope, $element, $attrs, $filter, _) -> ctrl = @ + _getUserById = (id) -> + _.find ctrl.users, (user) -> + curUserId = user?._id or user?.id + curUserId == id ctrl.displayName = displayNameForUser + ctrl.displayNameById = (id) -> + displayNameForUser(_getUserById(id)) ctrl.getProjectOpDoc = (projectOp) -> if projectOp.rename? then "#{ projectOp.rename.pathname} → #{ projectOp.rename.newPathname }" else if projectOp.add? then "#{ projectOp.add.pathname}" @@ -21,6 +27,7 @@ define [ bindings: entry: "<" currentUser: "<" + users: "<" onSelect: "&" onLabelDelete: "&" controller: historyEntryController diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee index d63f9329ae..f7c602733f 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee @@ -5,6 +5,9 @@ define [ App.controller "HistoryV2ListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) -> $scope.hoveringOverListSelectors = false + $scope.listConfig = + showOnlyLabelled: false + $scope.projectUsers = $scope.project.members.concat $scope.project.owner $scope.loadMore = () => ide.historyManager.fetchNextBatchOfUpdates() diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index 7ed37d49b1..404609917a 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -60,24 +60,32 @@ font-size: @font-size-small; margin-bottom: 3px; margin-right: 10px; - &:last-of-type { - margin-right: 0; - } + white-space: nowrap; .history-entry-selected & { color: @history-entry-selected-label-color; } } .history-entry-label-comment, .history-entry-label-delete-btn { - display: inline-block; - padding: 0 (@padding-xs-horizontal / 2) 1px @padding-xs-horizontal; + padding: 0 @padding-xs-horizontal 1px @padding-xs-horizontal; border: 0; background-color: @history-entry-label-bg-color; - border-radius: 9999px 0 0 9999px; .history-entry-selected & { background-color: @history-entry-selected-label-bg-color; } } + .history-entry-label-comment { + display: block; + float: left; + border-radius: 9999px; + max-width: 190px; + overflow: hidden; + text-overflow: ellipsis; + .history-entry-label-own & { + padding-right: (@padding-xs-horizontal / 2); + border-radius: 9999px 0 0 9999px; + } + } .history-entry-label-delete-btn { padding-left: (@padding-xs-horizontal / 2); padding-right: @padding-xs-horizontal; @@ -90,6 +98,23 @@ } } + .history-label-tooltip { + white-space: normal; + padding: (@line-height-computed / 4); + text-align: left; + } + .history-label-tooltip-title, + .history-label-tooltip-owner, + .history-label-tooltip-datetime { + margin: 0 0 (@line-height-computed / 4) 0; + } + .history-label-tooltip-title { + font-weight: bold; + } + .history-label-tooltip-datetime { + margin-bottom: 0; + } + .history-entry-changes { .list-unstyled; margin-bottom: 3px; From dc235b91d98ed23eb90c72c116e15b85a10169aa Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 2 Aug 2018 15:46:42 +0100 Subject: [PATCH 121/188] Fix some issues with the label tooltip in compare mode. --- .../web/app/views/project/editor/history.pug | 26 ++++++++++ .../project/editor/history/entriesListV2.pug | 52 +++++++------------ .../controllers/HistoryListController.coffee | 12 ++++- 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index a7a52d2927..f0d22136b5 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -67,3 +67,29 @@ script(type="text/ng-template", id="historyRestoreDiffModalTemplate") ) span(ng-show="!state.inflight") #{translate("restore")} span(ng-show="state.inflight") #{translate("restoring")} ... + + +script(type="text/ng-template", id="historyLabelTpl") + .history-entry-label( + ng-class="{ 'history-entry-label-own' : $ctrl.isOwnedByCurrentUser }" + ) + span.history-entry-label-comment( + tooltip-append-to-body="true" + tooltip-template="'historyLabelTooltipTpl'" + tooltip-placement="left" + ) + i.fa.fa-tag + |  {{ $ctrl.labelText }} + button.history-entry-label-delete-btn( + ng-if="$ctrl.isOwnedByCurrentUser" + stop-propagation="click" + ng-click="$ctrl.onLabelDelete()" + ) × + +script(type="text/ng-template", id="historyLabelTooltipTpl") + .history-label-tooltip + p.history-label-tooltip-title + i.fa.fa-tag + |  {{ $ctrl.labelText }} + p.history-label-tooltip-owner Created by {{ $ctrl.labelOwnerName }} + time.history-label-tooltip-datetime {{ labelCreationDateTime | formatDate }} diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index b96c74947d..52da25c3b5 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -69,17 +69,25 @@ aside.change-list( div.description(ng-click="select()") div - .history-entry-label( - ng-if="update.labels.length > 0" + history-label( ng-repeat="label in update.labels" + label-text="label.comment" + label-owner-name="getDisplayNameById(label.user_id)" + label-creation-date-time="label.created_at" + is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" + on-label-delete="deleteLabel(label)" ) - span.history-entry-label-comment - i.fa.fa-tag - |  {{ label.comment }} - button.history-entry-label-delete-btn( - stop-propagation="click" - ng-click="deleteLabel(label)" - ) × + //- .history-entry-label( + //- ng-if="update.labels.length > 0" + //- ng-repeat="label in update.labels" + //- ) + //- span.history-entry-label-comment + //- i.fa.fa-tag + //- |  {{ label.comment }} + //- button.history-entry-label-delete-btn( + //- stop-propagation="click" + //- ng-click="deleteLabel(label)" + //- ) × div.time {{ update.meta.end_ts | formatDate:'h:mm a' }} div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0") | #{translate("file_action_edited")} @@ -198,28 +206,4 @@ script(type="text/ng-template", id="historyEntryTpl") li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0") span.name( ng-style="$ctrl.getUserCSSStyle();" - ) #{translate("anonymous")} - -script(type="text/ng-template", id="historyLabelTpl") - .history-entry-label( - ng-class="{ 'history-entry-label-own' : $ctrl.isOwnedByCurrentUser }" - ) - span.history-entry-label-comment( - tooltip-template="'historyLabelTooltipTpl'" - tooltip-placement="left" - ) - i.fa.fa-tag - |  {{ $ctrl.labelText }} - button.history-entry-label-delete-btn( - ng-if="$ctrl.isOwnedByCurrentUser" - stop-propagation="click" - ng-click="$ctrl.onLabelDelete()" - ) × - -script(type="text/ng-template", id="historyLabelTooltipTpl") - .history-label-tooltip - p.history-label-tooltip-title - i.fa.fa-tag - |  {{ $ctrl.labelText }} - p.history-label-tooltip-owner Created by {{ $ctrl.labelOwnerName }} - time.history-label-tooltip-datetime {{ labelCreationDateTime | formatDate }} \ No newline at end of file + ) #{translate("anonymous")} \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index e4bc2b541d..8257eebd8c 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -5,7 +5,17 @@ define [ App.controller "HistoryListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) -> $scope.hoveringOverListSelectors = false - + + projectUsers = $scope.project.members.concat $scope.project.owner + console.log projectUsers + _getUserById = (id) -> + _.find projectUsers, (user) -> + curUserId = user?._id or user?.id + curUserId == id + + $scope.getDisplayNameById = (id) -> + displayNameForUser(_getUserById(id)) + $scope.deleteLabel = (labelDetails) -> $modal.open( templateUrl: "historyV2DeleteLabelModalTemplate" From 2c2bb4c130da809fdfe84833c941ae8d1386248d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 2 Aug 2018 16:20:01 +0100 Subject: [PATCH 122/188] Add i18n; avoid injecting unneeded dependencies. --- .../web/app/views/project/editor/history.pug | 2 +- .../project/editor/history/entriesListV2.pug | 11 ----------- .../views/project/editor/history/toolbarV2.pug | 16 ++++++++-------- .../ide/history/components/historyEntry.coffee | 2 +- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index f0d22136b5..6281bbee49 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -91,5 +91,5 @@ script(type="text/ng-template", id="historyLabelTooltipTpl") p.history-label-tooltip-title i.fa.fa-tag |  {{ $ctrl.labelText }} - p.history-label-tooltip-owner Created by {{ $ctrl.labelOwnerName }} + p.history-label-tooltip-owner #{translate("history_label_created_by")} {{ $ctrl.labelOwnerName }} time.history-label-tooltip-datetime {{ labelCreationDateTime | formatDate }} diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index 52da25c3b5..e4ad48caba 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -77,17 +77,6 @@ aside.change-list( is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" on-label-delete="deleteLabel(label)" ) - //- .history-entry-label( - //- ng-if="update.labels.length > 0" - //- ng-repeat="label in update.labels" - //- ) - //- span.history-entry-label-comment - //- i.fa.fa-tag - //- |  {{ label.comment }} - //- button.history-entry-label-delete-btn( - //- stop-propagation="click" - //- ng-click="deleteLabel(label)" - //- ) × div.time {{ update.meta.end_ts | formatDate:'h:mm a' }} div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0") | #{translate("file_action_edited")} diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index 1b8b733154..76d4ed1494 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -12,7 +12,7 @@ ng-disabled="history.loadingFileTree" ) i.fa.fa-tag - |  Label this version + |  #{translate("history_label_this_version")} button.history-toolbar-btn( ng-click="toggleHistoryViewMode();" ng-disabled="history.loadingFileTree" @@ -27,20 +27,20 @@ script(type="text/ng-template", id="historyV2AddLabelModalTemplate") novalidate ) .modal-header - h3 Add label + h3 #{translate("history_add_label")} .modal-body .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} .form-group input.form-control( type="text" - placeholder="New label name" + placeholder=translate("history_new_label_name") ng-model="inputs.labelName" focus-on="open" required ) p.help-block(ng-if="update") - | A new label will be added as of + | #{translate("history_new_label_added_at")} strong {{ update.meta.end_ts | formatDate:'ddd Do MMM YYYY, h:mm a' }} .modal-footer button.btn.btn-default( @@ -50,18 +50,18 @@ script(type="text/ng-template", id="historyV2AddLabelModalTemplate") ) #{translate("cancel")} input.btn.btn-primary( ng-disabled="addLabelModalForm.$invalid || state.inflight" - ng-value="state.inflight ? 'Adding label' : 'Add label'" + ng-value="state.inflight ? '" + translate("history_adding_label") + "' : '" + translate("history_add_label") + "'" type="submit" ) script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate") .modal-header - h3 Delete label + h3 #{translate("history_delete_label")} .modal-body .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} p.help-block(ng-if="labelDetails") - | Are you sure you want to delete the following label: + | #{translate("history_are_you_sure_delete_label")} strong "{{ labelDetails.comment }}" | ? .modal-footer @@ -74,4 +74,4 @@ script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate") type="button" ng-click="deleteLabel()" ng-disabled="state.inflight" - ) {{ state.inflight ? 'Deleting label' : 'Delete label' }} \ No newline at end of file + ) {{ state.inflight ? '#{translate("history_deleting_label")}' : '#{translate("history_delete_label")}' }} diff --git a/services/web/public/coffee/ide/history/components/historyEntry.coffee b/services/web/public/coffee/ide/history/components/historyEntry.coffee index 2995c5f481..eb7f1e7266 100644 --- a/services/web/public/coffee/ide/history/components/historyEntry.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntry.coffee @@ -2,7 +2,7 @@ define [ "base" "ide/history/util/displayNameForUser" ], (App, displayNameForUser) -> - historyEntryController = ($scope, $element, $attrs, $filter, _) -> + historyEntryController = ($scope, $element, $attrs, _) -> ctrl = @ _getUserById = (id) -> _.find ctrl.users, (user) -> From f418929dcfe58c034e3a7d8436cb3b117d2182c9 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 2 Aug 2018 16:23:47 +0100 Subject: [PATCH 123/188] Adjust i18n. --- services/web/app/views/project/editor/history/toolbarV2.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index 76d4ed1494..b4afe439b3 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -61,7 +61,7 @@ script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate") .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} p.help-block(ng-if="labelDetails") - | #{translate("history_are_you_sure_delete_label")} + | #{translate("history_are_you_sure_delete_label")}: strong "{{ labelDetails.comment }}" | ? .modal-footer From f605e79fa5ff0b9a2bfc3f21890bc85b5134330b Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 2 Aug 2018 16:33:45 +0100 Subject: [PATCH 124/188] Remove changes to robots.txt. --- services/web/public/robots.txt | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/services/web/public/robots.txt b/services/web/public/robots.txt index e241feebcc..77470cb39f 100644 --- a/services/web/public/robots.txt +++ b/services/web/public/robots.txt @@ -1,21 +1,2 @@ -# robots.txt for https://www.sharelatex.com/ - User-agent: * -Disallow: /project/* -Disallow: /github/repos/* -Disallow: /recurly.com -Disallow: /user/password/set -Disallow: /learn-scripts/* -Allow: / - -User-Agent: AhrefsBot -Disallow: / - -User-Agent: XoviBot -Disallow: / - -User-Agent: RankSonicBot -Disallow: / - -User-Agent: SMTBot -Disallow: / +Disallow: / \ No newline at end of file From 80eeaaaaea9e5b5b44a83b0501087f037741dbb0 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 2 Aug 2018 17:19:16 +0100 Subject: [PATCH 125/188] Update web acceptance tests. --- .../coffee/ide/history/HistoryV2ManagerTests.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee b/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee index 5d5ba13609..725befb721 100644 --- a/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee +++ b/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee @@ -23,6 +23,7 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) -> toV: null } } + labels: null diff: null files: [] selectedFile: null From d8c9a966197ac88f0caac7656c300af54bb96cbc Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 31 Jul 2018 15:53:05 +0100 Subject: [PATCH 126/188] If we're creating v1 accounts, don't allow login for users already linked up --- .../AuthenticationController.coffee | 44 ++++++++++++------- .../AuthenticationControllerTests.coffee | 2 +- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 832f01043f..951506891f 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -1,6 +1,5 @@ AuthenticationManager = require ("./AuthenticationManager") LoginRateLimiter = require("../Security/LoginRateLimiter") -UserGetter = require "../User/UserGetter" UserUpdater = require "../User/UserUpdater" Metrics = require('metrics-sharelatex') logger = require("logger-sharelatex") @@ -64,7 +63,10 @@ module.exports = AuthenticationController = if user # `user` is either a user object or false AuthenticationController.finishLogin(user, req, res, next) else - res.json message: info + if info.redir? + res.json {redir: info.redir} + else + res.json message: info )(req, res, next) finishLogin: (user, req, res, next) -> @@ -81,20 +83,30 @@ module.exports = AuthenticationController = doPassportLogin: (req, username, password, done) -> email = username.toLowerCase() - LoginRateLimiter.processLoginRequest email, (err, isAllowed)-> - return done(err) if err? - if !isAllowed - logger.log email:email, "too many login requests" - return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'}) - AuthenticationManager.authenticate email: email, password, (error, user) -> - return done(error) if error? - if user? - # async actions - return done(null, user) - else - AuthenticationController._recordFailedLogin() - logger.log email: email, "failed log in" - return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'}) + Modules = require "../../infrastructure/Modules" + Modules.hooks.fire 'preDoPassportLogin', email, (err, infoList) -> + return next(err) if err? + info = infoList.find((i) => i?) + if info? + return done(null, false, info) + LoginRateLimiter.processLoginRequest email, (err, isAllowed)-> + return done(err) if err? + if !isAllowed + logger.log email:email, "too many login requests" + return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'}) + AuthenticationManager.authenticate email: email, password, (error, user) -> + return done(error) if error? + if user? + # async actions + return done(null, user) + else + AuthenticationController._recordFailedLogin() + logger.log email: email, "failed log in" + return done( + null, + false, + {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'} + ) _loginAsyncHandlers: (req, user) -> UserHandler.setupLoginData(user, ()->) diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index 8482c28e04..030c1cd0df 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -15,7 +15,6 @@ describe "AuthenticationController", -> tk.freeze(Date.now()) @AuthenticationController = SandboxedModule.require modulePath, requires: "./AuthenticationManager": @AuthenticationManager = {} - "../User/UserGetter" : @UserGetter = {} "../User/UserUpdater" : @UserUpdater = {} "metrics-sharelatex": @Metrics = { inc: sinon.stub() } "../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() } @@ -29,6 +28,7 @@ describe "AuthenticationController", -> trackSession: sinon.stub() untrackSession: sinon.stub() revokeAllUserSessions: sinon.stub().callsArgWith(1, null) + "../../infrastructure/Modules": {hooks: {fire: sinon.stub().callsArgWith(2, null, [])}} @user = _id: ObjectId() email: @email = "USER@example.com" From 05df55c89e183baffa736fb4fd41210bf63b4690 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 3 Aug 2018 16:10:50 +0100 Subject: [PATCH 127/188] Add a unit test for the `preDoPassportLogin` module hook. --- .../AuthenticationControllerTests.coffee | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index 030c1cd0df..ae8fedc9c8 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -28,7 +28,7 @@ describe "AuthenticationController", -> trackSession: sinon.stub() untrackSession: sinon.stub() revokeAllUserSessions: sinon.stub().callsArgWith(1, null) - "../../infrastructure/Modules": {hooks: {fire: sinon.stub().callsArgWith(2, null, [])}} + "../../infrastructure/Modules": @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, [])}} @user = _id: ObjectId() email: @email = "USER@example.com" @@ -214,6 +214,7 @@ describe "AuthenticationController", -> beforeEach -> @AuthenticationController._recordFailedLogin = sinon.stub() @AuthenticationController._recordSuccessfulLogin = sinon.stub() + @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, []) # @AuthenticationController.establishUserSession = sinon.stub().callsArg(2) @req.body = email: @email @@ -222,6 +223,17 @@ describe "AuthenticationController", -> postLoginRedirect: "/path/to/redir/to" @cb = sinon.stub() + describe "when the preDoPassportLogin hooks produce an info object", -> + beforeEach -> + @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, [null, {redir: '/somewhere'}, null]) + + it "should stop early and call done with this info object", (done) -> + @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb) + @cb.callCount.should.equal 1 + @cb.calledWith(null, false, {redir: '/somewhere'}).should.equal true + @LoginRateLimiter.processLoginRequest.callCount.should.equal 0 + done() + describe "when the users rate limit", -> beforeEach -> From 17a63258c6ee4ea24e5d10bad4c3bf85d91fdf01 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Sat, 4 Aug 2018 17:30:24 +0100 Subject: [PATCH 128/188] changed newsletter to use mailchimp --- .../Newsletter/NewsletterManager.coffee | 51 ++++++++++++------- services/web/config/settings.defaults.coffee | 6 +-- services/web/package.json | 1 + 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 8d7eee43bf..7baa317632 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -1,37 +1,52 @@ async = require('async') -Request = require('request') logger = require 'logger-sharelatex' Settings = require 'settings-sharelatex' +crypto = require('crypto') +Mailchimp = require('mailchimp-api-v3') +mailchimp = new Mailchimp(Settings.mailchimp?.api_key) if Settings.mailchimp? module.exports = subscribe: (user, callback = () ->)-> - if !Settings.markdownmail? + if !Settings.mailchimp? logger.warn "No newsletter provider configured so not subscribing user" return callback() - logger.log user:user, email:user.email, "trying to subscribe user to the mailing list" options = buildOptions(user, true) - Request.post options, (err, response, body)-> - logger.log body:body, user:user, "finished attempting to subscribe the user to the news letter" + logger.log options:options, user:user, email:user.email, "trying to subscribe user to the mailing list" + mailchimp.request options, (err)-> + if err? + logger.err err:err, "error subscribing person to newsletter" + else + logger.log user:user, "finished subscribing user to the newsletter" callback(err) unsubscribe: (user, callback = () ->)-> - if !Settings.markdownmail? + if !Settings.mailchimp? logger.warn "No newsletter provider configured so not unsubscribing user" return callback() logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list" options = buildOptions(user, false) - Request.post options, (err, response, body)-> - logger.log err:err, body:body, email:user.email, "compled newsletter unsubscribe attempt" + mailchimp.request options, (err)-> + if err? + logger.err err:err, "error unsubscribing person to newsletter" + else + logger.log user:user, "finished unsubscribing user to the newsletter" callback(err) +hashEmail = (email)-> + crypto.createHash('md5').update(email.toLowerCase()).digest("hex") + buildOptions = (user, is_subscribed)-> - options = - json: - secret_token: Settings.markdownmail.secret - name: "#{user.first_name} #{user.last_name}" - email: user.email - subscriber_list_id: Settings.markdownmail.list_id - is_subscribed: is_subscribed - url: "https://www.markdownmail.io/lists/subscribe" - timeout: 30 * 1000 - return options \ No newline at end of file + status = if is_subscribed then "subscribed" else "unsubscribed" + subscriber_hash = hashEmail(user.email) + opts = + method: "PUT" + path: "/lists/#{Settings.mailchimp?.list_id}/members/#{subscriber_hash}" + body: + status_if_new: status + status: status + email_address:user.email + merge_fields: + FNAME: user.first_name + LNAME: user.last_name + return opts + diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 1306ce91c2..827f4f2959 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -277,10 +277,10 @@ module.exports = settings = # Third party services # -------------------- # - # ShareLaTeX's regular newsletter is managed by Markdown mail. Add your + # ShareLaTeX's regular newsletter is managed by mailchimp. Add your # credentials here to integrate with this. - # markdownmail: - # secret: "" + # mailchimp: + # api_key: "" # list_id: "" # # Fill in your unique token from various analytics services to enable diff --git a/services/web/package.json b/services/web/package.json index ed0eb06455..1dce008612 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -59,6 +59,7 @@ "lodash": "^4.13.1", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master", "lynx": "0.1.1", + "mailchimp-api-v3": "^1.12.0", "marked": "^0.3.5", "method-override": "^2.3.3", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.7.1", From 7da8a926bbacdf6c04b0aca106fad640f18256fc Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 6 Aug 2018 10:52:10 +0100 Subject: [PATCH 129/188] increase bodyparser limit to 64kb 16kb is not enough for bibtex files with more escaping. --- services/web/app/coffee/infrastructure/Server.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index cf2757c80e..bf9d6652f4 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -68,7 +68,7 @@ Modules.loadViewIncludes app app.use bodyParser.urlencoded({ extended: true, limit: "2mb"}) # Make sure we can process the max doc length plus some overhead for JSON encoding -app.use bodyParser.json({limit: Settings.max_doc_length + 16 * 1024}) # 16kb overhead +app.use bodyParser.json({limit: Settings.max_doc_length + 64 * 1024}) # 64kb overhead app.use multer(dest: Settings.path.uploadFolder) app.use methodOverride() From bd6dcc007b7c416ab68db08848f0d42ab1fcff28 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 6 Aug 2018 11:00:45 +0100 Subject: [PATCH 130/188] Remove prototype code. --- .../web/app/views/project/editor/history/entriesListV2.pug | 3 +-- .../coffee/ide/history/components/historyEntriesList.coffee | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index e4ad48caba..55d86c434f 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -10,7 +10,6 @@ aside.change-list( load-disabled="history.loading || history.atEnd" load-initialize="ui.view == 'history'" is-loading="history.loading" - show-only-labelled="listConfig.showOnlyLabelled" on-entry-select="handleEntrySelect(selectedEntry)" on-label-delete="handleLabelDelete(label)" ) @@ -115,7 +114,7 @@ script(type="text/ng-template", id="historyEntriesListTpl") ) .infinite-scroll-inner history-entry( - ng-repeat="entry in $ctrl.entries | filter:$ctrl.shouldShowEntry" + ng-repeat="entry in $ctrl.entries" entry="entry" current-user="$ctrl.currentUser" users="$ctrl.users" diff --git a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee index f872b5d72a..934ca304fe 100644 --- a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee @@ -3,8 +3,6 @@ define [ ], (App) -> historyEntriesListController = ($scope, $element, $attrs) -> ctrl = @ - ctrl.shouldShowEntry = (entry) -> - !(ctrl.showOnlyLabelled and entry.labels.length == 0) return App.component "historyEntriesList", { @@ -18,7 +16,6 @@ define [ currentUser: "<" onEntrySelect: "&" onLabelDelete: "&" - showOnlyLabelled: "<" controller: historyEntriesListController templateUrl: "historyEntriesListTpl" } From e0707253a773146df8738b3fde56acd811bdfd81 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 6 Aug 2018 11:03:15 +0100 Subject: [PATCH 131/188] Remove debug lines and unneeded HTML. --- .../project/editor/history/entriesListV2.pug | 17 ++++++++--------- .../controllers/HistoryListController.coffee | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index 55d86c434f..337b5e33ce 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -67,15 +67,14 @@ aside.change-list( ) div.description(ng-click="select()") - div - history-label( - ng-repeat="label in update.labels" - label-text="label.comment" - label-owner-name="getDisplayNameById(label.user_id)" - label-creation-date-time="label.created_at" - is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" - on-label-delete="deleteLabel(label)" - ) + history-label( + ng-repeat="label in update.labels" + label-text="label.comment" + label-owner-name="getDisplayNameById(label.user_id)" + label-creation-date-time="label.created_at" + is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" + on-label-delete="deleteLabel(label)" + ) div.time {{ update.meta.end_ts | formatDate:'h:mm a' }} div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0") | #{translate("file_action_edited")} diff --git a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index 8257eebd8c..90c8142c6c 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -7,7 +7,7 @@ define [ $scope.hoveringOverListSelectors = false projectUsers = $scope.project.members.concat $scope.project.owner - console.log projectUsers + _getUserById = (id) -> _.find projectUsers, (user) -> curUserId = user?._id or user?.id From b0261970fc3d88abe9314d4bf1dcd6cc68806712 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 6 Aug 2018 11:26:22 +0100 Subject: [PATCH 132/188] Make label removal also work in compare mode. --- .../web/app/views/project/editor/history.pug | 23 ++++++++++++++++++ .../project/editor/history/entriesListV2.pug | 2 +- .../project/editor/history/toolbarV2.pug | 24 +------------------ .../history/components/historyEntry.coffee | 3 +++ .../controllers/HistoryListController.coffee | 3 +++ 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index 6281bbee49..2f3f317561 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -93,3 +93,26 @@ script(type="text/ng-template", id="historyLabelTooltipTpl") |  {{ $ctrl.labelText }} p.history-label-tooltip-owner #{translate("history_label_created_by")} {{ $ctrl.labelOwnerName }} time.history-label-tooltip-datetime {{ labelCreationDateTime | formatDate }} + + +script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate") + .modal-header + h3 #{translate("history_delete_label")} + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + p.help-block(ng-if="labelDetails") + | #{translate("history_are_you_sure_delete_label")} + strong "{{ labelDetails.comment }}" + | ? + .modal-footer + button.btn.btn-default( + type="button" + ng-disabled="state.inflight" + ng-click="$dismiss()" + ) #{translate("cancel")} + button.btn.btn-primary( + type="button" + ng-click="deleteLabel()" + ng-disabled="state.inflight" + ) {{ state.inflight ? '#{translate("history_deleting_label")}' : '#{translate("history_delete_label")}' }} diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index 337b5e33ce..e08de0effd 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -72,7 +72,7 @@ aside.change-list( label-text="label.comment" label-owner-name="getDisplayNameById(label.user_id)" label-creation-date-time="label.created_at" - is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" + is-owned-by-current-user="label.user_id === user.id" on-label-delete="deleteLabel(label)" ) div.time {{ update.meta.end_ts | formatDate:'h:mm a' }} diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index b4afe439b3..2a4c222bad 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -52,26 +52,4 @@ script(type="text/ng-template", id="historyV2AddLabelModalTemplate") ng-disabled="addLabelModalForm.$invalid || state.inflight" ng-value="state.inflight ? '" + translate("history_adding_label") + "' : '" + translate("history_add_label") + "'" type="submit" - ) - -script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate") - .modal-header - h3 #{translate("history_delete_label")} - .modal-body - .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} - .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} - p.help-block(ng-if="labelDetails") - | #{translate("history_are_you_sure_delete_label")}: - strong "{{ labelDetails.comment }}" - | ? - .modal-footer - button.btn.btn-default( - type="button" - ng-disabled="state.inflight" - ng-click="$dismiss()" - ) #{translate("cancel")} - button.btn.btn-primary( - type="button" - ng-click="deleteLabel()" - ng-disabled="state.inflight" - ) {{ state.inflight ? '#{translate("history_deleting_label")}' : '#{translate("history_delete_label")}' }} + ) \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/components/historyEntry.coffee b/services/web/public/coffee/ide/history/components/historyEntry.coffee index eb7f1e7266..0942613ef2 100644 --- a/services/web/public/coffee/ide/history/components/historyEntry.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntry.coffee @@ -4,6 +4,9 @@ define [ ], (App, displayNameForUser) -> historyEntryController = ($scope, $element, $attrs, _) -> ctrl = @ + # This method (and maybe the one below) will be removed soon. User details data will be + # injected into the history API responses, so we won't need to fetch user data from other + # local data structures. _getUserById = (id) -> _.find ctrl.users, (user) -> curUserId = user?._id or user?.id diff --git a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index 90c8142c6c..cfd5bb75c5 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -8,6 +8,9 @@ define [ projectUsers = $scope.project.members.concat $scope.project.owner + # This method (and maybe the one below) will be removed soon. User details data will be + # injected into the history API responses, so we won't need to fetch user data from other + # local data structures. _getUserById = (id) -> _.find projectUsers, (user) -> curUserId = user?._id or user?.id From fa37caef58d84d645b40aa57e8d2dc1c6b2fabb5 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 6 Aug 2018 12:37:18 +0100 Subject: [PATCH 133/188] remove null check on mailchimp at top of file I didn't like the if statment being after the require, mailchimp could also be null and called elsewhere by acident --- .../app/coffee/Features/Newsletter/NewsletterManager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 7baa317632..4850554303 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -3,7 +3,6 @@ logger = require 'logger-sharelatex' Settings = require 'settings-sharelatex' crypto = require('crypto') Mailchimp = require('mailchimp-api-v3') -mailchimp = new Mailchimp(Settings.mailchimp?.api_key) if Settings.mailchimp? module.exports = subscribe: (user, callback = () ->)-> @@ -12,6 +11,7 @@ module.exports = return callback() options = buildOptions(user, true) logger.log options:options, user:user, email:user.email, "trying to subscribe user to the mailing list" + mailchimp = new Mailchimp(Settings.mailchimp?.api_key) mailchimp.request options, (err)-> if err? logger.err err:err, "error subscribing person to newsletter" @@ -25,6 +25,7 @@ module.exports = return callback() logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list" options = buildOptions(user, false) + mailchimp = new Mailchimp(Settings.mailchimp?.api_key) mailchimp.request options, (err)-> if err? logger.err err:err, "error unsubscribing person to newsletter" From efcd3577ce758c7705b069931d3ba9949ad0e40e Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 6 Aug 2018 16:56:44 +0100 Subject: [PATCH 134/188] avoid clobbering imported image names --- .../Features/Project/ProjectCreationHandler.coffee | 3 ++- .../coffee/Project/ProjectCreationHandlerTests.coffee | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee index 529164a49e..f83d6e7d21 100644 --- a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee @@ -55,7 +55,8 @@ module.exports = ProjectCreationHandler = if Settings.apis?.project_history?.displayHistoryForNewProjects project.overleaf.history.display = true if Settings.currentImageName? - project.imageName = Settings.currentImageName + # avoid clobbering any imageName already set in attributes (e.g. importedImageName) + project.imageName ?= Settings.currentImageName project.rootFolder[0] = rootFolder User.findById owner_id, "ace.spellCheckLanguage", (err, user)-> project.spellCheckLanguage = user.ace.spellCheckLanguage diff --git a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee index 6d86545622..15499e6f71 100644 --- a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee @@ -111,7 +111,7 @@ describe 'ProjectCreationHandler', -> project.spellCheckLanguage.should.equal "de" done() - it "should set the imageName to currentImageName if set", (done) -> + it "should set the imageName to currentImageName if set and no imageName attribute", (done) -> @Settings.currentImageName = "mock-image-name" @handler.createBlankProject ownerId, projectName, (err, project)=> project.imageName.should.equal @Settings.currentImageName @@ -123,6 +123,14 @@ describe 'ProjectCreationHandler', -> expect(project.imageName).to.not.exist done() + it "should set the imageName to the attribute value if set and not overwrite it with the currentImageName", (done) -> + @Settings.currentImageName = "mock-image-name" + attributes = + imageName: "attribute-image-name" + @handler.createBlankProject ownerId, projectName, attributes, (err, project)=> + project.imageName.should.equal attributes.imageName + done() + it "should not set the overleaf.history.display if not configured in settings", (done) -> @Settings.apis.project_history.displayHistoryForNewProjects = false @handler.createBlankProject ownerId, projectName, (err, project)=> From 6cf1f71604923bd042c505e248be845f6c724fff Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 6 Aug 2018 17:43:03 +0100 Subject: [PATCH 135/188] add newsletter checkbox to user register forms --- .../coffee/Features/User/UserRegistrationHandler.coffee | 5 +++-- services/web/public/stylesheets/app/homepage.less | 4 ++++ .../unit/coffee/User/UserRegistrationHandlerTests.coffee | 8 +++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee index df7fe93218..848434271f 100644 --- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee @@ -1,4 +1,4 @@ -sanitize = require('sanitizer') +resanitize = require('sanitizer') User = require("../../models/User").User UserCreator = require("./UserCreator") UserGetter = require("./UserGetter") @@ -54,7 +54,8 @@ module.exports = UserRegistrationHandler = (cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb (cb)-> AuthenticationManager.setUserPassword user._id, userDetails.password, cb (cb)-> - NewsLetterManager.subscribe user, -> + if userDetails.subscribeToNewsletter == "true" + NewsLetterManager.subscribe user, -> cb() #this can be slow, just fire it off ], (err)-> logger.log user: user, "registered" diff --git a/services/web/public/stylesheets/app/homepage.less b/services/web/public/stylesheets/app/homepage.less index d502855b72..22efbc847d 100644 --- a/services/web/public/stylesheets/app/homepage.less +++ b/services/web/public/stylesheets/app/homepage.less @@ -47,7 +47,11 @@ } .form-group { margin-left: @line-height-computed / 2; + input[type="checkbox"] { + margin-right: 5px; + } } + } .screenshot { height: 550px; diff --git a/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee b/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee index e9a6c568dd..f8bcce30ce 100644 --- a/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee +++ b/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee @@ -132,11 +132,17 @@ describe "UserRegistrationHandler", -> @AuthenticationManager.setUserPassword.calledWith(@user._id, @passingRequest.password).should.equal true done() - it "should add the user to the news letter manager", (done)-> + it "should add the user to the newsletter if accepted terms", (done)-> + @passingRequest.subscribeToNewsletter = "true" @handler.registerNewUser @passingRequest, (err)=> @NewsLetterManager.subscribe.calledWith(@user).should.equal true done() + it "should not add the user to the newsletter if not accepted terms", (done)-> + @handler.registerNewUser @passingRequest, (err)=> + @NewsLetterManager.subscribe.calledWith(@user).should.equal false + done() + it "should track the registration event", (done)-> @handler.registerNewUser @passingRequest, (err)=> @AnalyticsManager.recordEvent From c26a85c02c40df5e28b879aabae14637ed79ce9f Mon Sep 17 00:00:00 2001 From: Nate Stemen Date: Tue, 7 Aug 2018 09:49:50 -0400 Subject: [PATCH 136/188] get rid of confusing regex --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index e91c92f643..3be4369213 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -162,7 +162,7 @@ define [ cursorPosition = @editor.getCursorPosition() end = change.end {lineUpToCursor, commandFragment} = Helpers.getContext(@editor, end) - if lineUpToCursor.match(/.*((?![\\]).|^)%.*/) + if (i = lineUpToCursor.indexOf('%') > -1 and lineUpToCursor[i-1] != '\\') return lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\" lastTwoChars = lineUpToCursor.slice(-2) From 88f2b3670bbe65f602a2b7b1c93be262a2c07b33 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 7 Aug 2018 16:48:35 +0100 Subject: [PATCH 137/188] remove unneed style --- services/web/public/stylesheets/app/homepage.less | 4 ---- 1 file changed, 4 deletions(-) diff --git a/services/web/public/stylesheets/app/homepage.less b/services/web/public/stylesheets/app/homepage.less index 22efbc847d..d502855b72 100644 --- a/services/web/public/stylesheets/app/homepage.less +++ b/services/web/public/stylesheets/app/homepage.less @@ -47,11 +47,7 @@ } .form-group { margin-left: @line-height-computed / 2; - input[type="checkbox"] { - margin-right: 5px; - } } - } .screenshot { height: 550px; From 6208e9f2d0cbf673ab1b69c0e2a48b915c2a9e4f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 7 Aug 2018 21:38:31 +0100 Subject: [PATCH 138/188] add changeEmail function to newsletter manager not actually called --- .../Newsletter/NewsletterManager.coffee | 30 ++++++++++++++----- .../coffee/Features/User/UserUpdater.coffee | 1 + 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 4850554303..0c1a98a6b1 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -4,14 +4,19 @@ Settings = require 'settings-sharelatex' crypto = require('crypto') Mailchimp = require('mailchimp-api-v3') + +if !Settings.mailchimp?.api_key? + logger.warn "No newsletter provider configured so not chaning email on user" + mailchimp = + request: (opts, cb)-> cb() +else + mailchimp = new Mailchimp(Settings.mailchimp?.api_key) + module.exports = + subscribe: (user, callback = () ->)-> - if !Settings.mailchimp? - logger.warn "No newsletter provider configured so not subscribing user" - return callback() options = buildOptions(user, true) logger.log options:options, user:user, email:user.email, "trying to subscribe user to the mailing list" - mailchimp = new Mailchimp(Settings.mailchimp?.api_key) mailchimp.request options, (err)-> if err? logger.err err:err, "error subscribing person to newsletter" @@ -20,12 +25,8 @@ module.exports = callback(err) unsubscribe: (user, callback = () ->)-> - if !Settings.mailchimp? - logger.warn "No newsletter provider configured so not unsubscribing user" - return callback() logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list" options = buildOptions(user, false) - mailchimp = new Mailchimp(Settings.mailchimp?.api_key) mailchimp.request options, (err)-> if err? logger.err err:err, "error unsubscribing person to newsletter" @@ -33,6 +34,19 @@ module.exports = logger.log user:user, "finished unsubscribing user to the newsletter" callback(err) + changeEmail: (oldEmail, newEmail, callback = ()->)-> + options = buildOptions({email:oldEmail}) + delete options.body.status + options.body.email_address = newEmail + mailchimp.request options, (err)-> + # if the user has unsubscribed mailchimp will error on email address change + if err? and err?.message.indexOf("could not be validated") == -1 + logger.err err:err, "error changing email in newsletter" + return callback(err) + else + logger.log "finished changing email in the newsletter" + return callback() + hashEmail = (email)-> crypto.createHash('md5').update(email.toLowerCase()).digest("hex") diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 8d5e3658f9..7d828366e7 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -11,6 +11,7 @@ EmailHelper = require "../Helpers/EmailHelper" Errors = require "../Errors/Errors" Settings = require "settings-sharelatex" request = require 'request' +NewsletterManager = require "../Newsletter/NewsletterManager" module.exports = UserUpdater = updateUser: (query, update, callback = (error) ->) -> From 9a27a39f55731cadac6b3244cb255e01fc86c1c0 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 8 Aug 2018 11:01:20 +0100 Subject: [PATCH 139/188] Make tooltip optional in the label component. --- services/web/app/views/project/editor/history.pug | 1 + .../public/coffee/ide/history/components/historyLabel.coffee | 3 +++ 2 files changed, 4 insertions(+) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index 2f3f317561..bfb7df256b 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -77,6 +77,7 @@ script(type="text/ng-template", id="historyLabelTpl") tooltip-append-to-body="true" tooltip-template="'historyLabelTooltipTpl'" tooltip-placement="left" + tooltip-enable="$ctrl.showTooltip" ) i.fa.fa-tag |  {{ $ctrl.labelText }} diff --git a/services/web/public/coffee/ide/history/components/historyLabel.coffee b/services/web/public/coffee/ide/history/components/historyLabel.coffee index 2394b76f8b..03253663dd 100644 --- a/services/web/public/coffee/ide/history/components/historyLabel.coffee +++ b/services/web/public/coffee/ide/history/components/historyLabel.coffee @@ -3,6 +3,8 @@ define [ ], (App) -> historyLabelController = ($scope, $element, $attrs, $filter, _) -> ctrl = @ + ctrl.$onInit = () -> + ctrl.showTooltip ?= true return App.component "historyLabel", { @@ -12,6 +14,7 @@ define [ labelCreationDateTime: "<" isOwnedByCurrentUser: "<" onLabelDelete: "&" + showTooltip: " Date: Wed, 8 Aug 2018 11:01:51 +0100 Subject: [PATCH 140/188] Create history labels list component. --- .../project/editor/history/entriesListV2.pug | 18 +++++++++++- .../components/historyLabelsList.coffee | 29 +++++++++++++++++++ .../stylesheets/app/editor/history-v2.less | 8 +++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 services/web/public/coffee/ide/history/components/historyLabelsList.coffee diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index e08de0effd..fa1ece2fdf 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -3,6 +3,7 @@ aside.change-list( ng-controller="HistoryV2ListController" ) history-entries-list( + ng-if="!history.showOnlyLabels" entries="history.updates" current-user="user" users="projectUsers" @@ -13,6 +14,10 @@ aside.change-list( on-entry-select="handleEntrySelect(selectedEntry)" on-label-delete="handleLabelDelete(label)" ) + history-labels-list( + ng-if="history.showOnlyLabels" + labels="history.labels" + ) aside.change-list( ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE" @@ -193,4 +198,15 @@ script(type="text/ng-template", id="historyEntryTpl") li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0") span.name( ng-style="$ctrl.getUserCSSStyle();" - ) #{translate("anonymous")} \ No newline at end of file + ) #{translate("anonymous")} + +script(type="text/ng-template", id="historyLabelsListTpl") + history-label( + ng-repeat="label in $ctrl.labels" + show-tooltip="false" + label-text="label.comment" + label-owner-name="$ctrl.displayNameById(label.user_id)" + label-creation-date-time="label.created_at" + is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" + on-label-delete="$ctrl.onLabelDelete({ label: label })" + ) \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/components/historyLabelsList.coffee b/services/web/public/coffee/ide/history/components/historyLabelsList.coffee new file mode 100644 index 0000000000..b692233a7e --- /dev/null +++ b/services/web/public/coffee/ide/history/components/historyLabelsList.coffee @@ -0,0 +1,29 @@ +define [ + "base" + "ide/history/util/displayNameForUser" +], (App, displayNameForUser) -> + historyLabelsListController = ($scope, $element, $attrs) -> + ctrl = @ + # This method (and maybe the one below) will be removed soon. User details data will be + # injected into the history API responses, so we won't need to fetch user data from other + # local data structures. + _getUserById = (id) -> + _.find ctrl.users, (user) -> + curUserId = user?._id or user?.id + curUserId == id + ctrl.displayName = displayNameForUser + ctrl.displayNameById = (id) -> + displayNameForUser(_getUserById(id)) + return + + App.component "historyLabelsList", { + bindings: + labels: "<" + users: "<" + isLoading: "<" + currentUser: "<" + onLabelSelect: "&" + onLabelDelete: "&" + controller: historyLabelsListController + templateUrl: "historyLabelsListTpl" + } diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index 404609917a..abbcef147d 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -27,6 +27,14 @@ margin-left: (@line-height-computed / 2); } + .history-toolbar-entries-list { + position: absolute; + right: 0; + width: @changesListWidth; + padding: 0 10px; + border-left: 1px solid @editor-border-color; + } + .history-entries { font-size: @history-base-font-size; color: @history-base-color; From cd853abb3c039a9ee0fa912e34502fb90ea30c43 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 8 Aug 2018 11:02:14 +0100 Subject: [PATCH 141/188] Load labels list from the backend. --- .../views/project/editor/history/toolbarV2.pug | 8 ++++++++ .../coffee/ide/history/HistoryV2Manager.coffee | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index 2a4c222bad..8774754de2 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -20,6 +20,14 @@ i.fa.fa-exchange |  #{translate("compare_to_another_version")} + .history-toolbar-entries-list + toggle-switch( + ng-model="history.showOnlyLabels" + label-true="Labels" + label-false="All history" + description="Show all of the project history or only labelled versions." + ) + script(type="text/ng-template", id="historyV2AddLabelModalTemplate") form( name="addLabelModalForm" diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index 7ff41985f4..519a94a54a 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -12,6 +12,7 @@ define [ "ide/history/directives/infiniteScroll" "ide/history/components/historyEntriesList" "ide/history/components/historyEntry" + "ide/history/components/historyLabelsList" "ide/history/components/historyLabel" "ide/history/components/historyFileTree" "ide/history/components/historyFileEntity" @@ -72,6 +73,7 @@ define [ toV: null } } + showOnlyLabels: false labels: null files: [] diff: null # When history.viewMode == HistoryViewModes.COMPARE @@ -134,17 +136,26 @@ define [ updatesURL = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}" if @$scope.history.nextBeforeTimestamp? updatesURL += "&before=#{@$scope.history.nextBeforeTimestamp}" - + labelsURL = "/project/#{@ide.project_id}/labels" + @$scope.history.loading = true @$scope.history.loadingFileTree = true - @ide.$http.get updatesURL + requests = + updates: @ide.$http.get updatesURL + + if !@$scope.history.labels? + requests.labels = @ide.$http.get labelsURL + + @ide.$q.all requests .then (response) => - updatesData = response.data + updatesData = response.updates.data @_loadUpdates(updatesData.updates) @$scope.history.nextBeforeTimestamp = updatesData.nextBeforeTimestamp if !updatesData.nextBeforeTimestamp? @$scope.history.atEnd = true + if response.labels? + @$scope.history.labels = response.labels.data @$scope.history.loading = false loadFileAtPointInTime: () -> From f2b957e5b33fed3931f3197c87208e88de06bc2d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 8 Aug 2018 13:46:43 +0100 Subject: [PATCH 142/188] Add history labels view. --- .../web/app/views/project/editor/history.pug | 8 +- .../project/editor/history/entriesListV2.pug | 44 ++- .../history/components/historyEntry.coffee | 6 +- .../history/components/historyLabel.coffee | 4 +- .../components/historyLabelsList.coffee | 16 +- .../HistoryV2ListController.coffee | 3 + .../stylesheets/app/editor/history-v2.less | 349 +----------------- .../stylesheets/app/editor/toolbar.less | 57 +-- 8 files changed, 103 insertions(+), 384 deletions(-) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index bfb7df256b..4782a5fa3d 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -70,10 +70,10 @@ script(type="text/ng-template", id="historyRestoreDiffModalTemplate") script(type="text/ng-template", id="historyLabelTpl") - .history-entry-label( - ng-class="{ 'history-entry-label-own' : $ctrl.isOwnedByCurrentUser }" + .history-label( + ng-class="{ 'history-label-own' : $ctrl.isOwnedByCurrentUser }" ) - span.history-entry-label-comment( + span.history-label-comment( tooltip-append-to-body="true" tooltip-template="'historyLabelTooltipTpl'" tooltip-placement="left" @@ -81,7 +81,7 @@ script(type="text/ng-template", id="historyLabelTpl") ) i.fa.fa-tag |  {{ $ctrl.labelText }} - button.history-entry-label-delete-btn( + button.history-label-delete-btn( ng-if="$ctrl.isOwnedByCurrentUser" stop-propagation="click" ng-click="$ctrl.onLabelDelete()" diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index fa1ece2fdf..e8bebabe69 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -17,6 +17,11 @@ aside.change-list( history-labels-list( ng-if="history.showOnlyLabels" labels="history.labels" + current-user="user" + users="projectUsers" + is-loading="history.loading" + on-label-select="handleLabelSelect(label)" + on-label-delete="handleLabelDelete(label)" ) aside.change-list( @@ -201,12 +206,33 @@ script(type="text/ng-template", id="historyEntryTpl") ) #{translate("anonymous")} script(type="text/ng-template", id="historyLabelsListTpl") - history-label( - ng-repeat="label in $ctrl.labels" - show-tooltip="false" - label-text="label.comment" - label-owner-name="$ctrl.displayNameById(label.user_id)" - label-creation-date-time="label.created_at" - is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" - on-label-delete="$ctrl.onLabelDelete({ label: label })" - ) \ No newline at end of file + .history-labels-list + .history-entry-label( + ng-repeat="label in $ctrl.labels | orderBy : [ '-version', '-created_at' ] track by label.id" + ng-click="$ctrl.onLabelSelect({ label: label })" + ) + history-label( + show-tooltip="false" + label-text="label.comment" + is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" + on-label-delete="$ctrl.onLabelDelete({ label: label })" + ) + .history-entry-label-metadata + .history-entry-label-metadata-user(ng-init="user = $ctrl.getUserById(label.user_id)") + | Saved by + span.name( + ng-if="user && user._id !== $ctrl.currentUser.id" + ng-style="$ctrl.getUserCSSStyle(user);" + ) {{ ::$ctrl.displayName(user) }} + span.name( + ng-if="user && user._id == $ctrl.currentUser.id" + ng-style="$ctrl.getUserCSSStyle(user);" + ) You + span.name( + ng-if="user == null" + ng-style="$ctrl.getUserCSSStyle(user);" + ) #{translate("anonymous")} + time.history-entry-label-metadata-time {{ ::label.created_at | formatDate }} + .loading(ng-show="$ctrl.isLoading") + i.fa.fa-spin.fa-refresh + |    #{translate("loading")}... \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/components/historyEntry.coffee b/services/web/public/coffee/ide/history/components/historyEntry.coffee index 0942613ef2..8af726b509 100644 --- a/services/web/public/coffee/ide/history/components/historyEntry.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntry.coffee @@ -1,7 +1,8 @@ define [ "base" + "ide/colors/ColorManager" "ide/history/util/displayNameForUser" -], (App, displayNameForUser) -> +], (App, ColorManager, displayNameForUser) -> historyEntryController = ($scope, $element, $attrs, _) -> ctrl = @ # This method (and maybe the one below) will be removed soon. User details data will be @@ -19,7 +20,8 @@ define [ else if projectOp.add? then "#{ projectOp.add.pathname}" else if projectOp.remove? then "#{ projectOp.remove.pathname}" ctrl.getUserCSSStyle = (user) -> - hue = user?.hue or 100 + curUserId = user?._id or user?.id + hue = ColorManager.getHueForUserId(curUserId) or 100 if ctrl.entry.inSelection color : "#FFF" else diff --git a/services/web/public/coffee/ide/history/components/historyLabel.coffee b/services/web/public/coffee/ide/history/components/historyLabel.coffee index 03253663dd..680d610d99 100644 --- a/services/web/public/coffee/ide/history/components/historyLabel.coffee +++ b/services/web/public/coffee/ide/history/components/historyLabel.coffee @@ -10,8 +10,8 @@ define [ App.component "historyLabel", { bindings: labelText: "<" - labelOwnerName: "<" - labelCreationDateTime: "<" + labelOwnerName: " +], (App, ColorManager, displayNameForUser) -> historyLabelsListController = ($scope, $element, $attrs) -> ctrl = @ # This method (and maybe the one below) will be removed soon. User details data will be # injected into the history API responses, so we won't need to fetch user data from other # local data structures. - _getUserById = (id) -> + ctrl.getUserById = (id) -> _.find ctrl.users, (user) -> curUserId = user?._id or user?.id curUserId == id ctrl.displayName = displayNameForUser - ctrl.displayNameById = (id) -> - displayNameForUser(_getUserById(id)) + ctrl.getUserCSSStyle = (user) -> + curUserId = user?._id or user?.id + hue = ColorManager.getHueForUserId(curUserId) or 100 + if false #ctrl.entry.inSelection + color : "#FFF" + else + color: "hsl(#{ hue }, 70%, 50%)" return App.component "historyLabelsList", { bindings: labels: "<" users: "<" - isLoading: "<" currentUser: "<" + isLoading: "<" onLabelSelect: "&" onLabelDelete: "&" controller: historyLabelsListController diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee index f7c602733f..a2873d47d8 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee @@ -16,6 +16,9 @@ define [ ide.historyManager.selectUpdate(entry) $scope.recalculateSelectedUpdates() + $scope.handleLabelSelect = (label) -> + console.log label + $scope.handleLabelDelete = (labelDetails) -> $modal.open( templateUrl: "historyV2DeleteLabelModalTemplate" diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index abbcef147d..c2e7ce6b26 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -61,8 +61,7 @@ color: #FFF; } } - - .history-entry-label { + .history-label { display: inline-block; color: @history-entry-label-color; font-size: @font-size-small; @@ -73,8 +72,8 @@ color: @history-entry-selected-label-color; } } - .history-entry-label-comment, - .history-entry-label-delete-btn { + .history-label-comment, + .history-label-delete-btn { padding: 0 @padding-xs-horizontal 1px @padding-xs-horizontal; border: 0; background-color: @history-entry-label-bg-color; @@ -82,19 +81,19 @@ background-color: @history-entry-selected-label-bg-color; } } - .history-entry-label-comment { + .history-label-comment { display: block; float: left; border-radius: 9999px; max-width: 190px; overflow: hidden; text-overflow: ellipsis; - .history-entry-label-own & { + .history-label-own & { padding-right: (@padding-xs-horizontal / 2); border-radius: 9999px 0 0 9999px; } } - .history-entry-label-delete-btn { + .history-label-delete-btn { padding-left: (@padding-xs-horizontal / 2); padding-right: @padding-xs-horizontal; border-radius: 0 9999px 9999px 0; @@ -163,6 +162,15 @@ } } +.history-labels-list { + .history-entries; + overflow-y: auto; +} + .history-entry-label { + .history-entry-details; + padding: 7px 10px; + } + .history-file-tree-inner { .full-size; overflow-y: auto; @@ -239,330 +247,3 @@ color: @brand-primary; } } -// @changesListWidth: 250px; -// @changesListPadding: @line-height-computed / 2; - -// @selector-padding-vertical: 10px; -// @selector-padding-horizontal: @line-height-computed / 2; -// @day-header-height: 24px; - -// @range-bar-color: @link-color; -// @range-bar-selected-offset: 14px; - -// #history { -// .upgrade-prompt { -// position: absolute; -// top: 0; -// bottom: 0; -// left: 0; -// right: 0; -// z-index: 100; -// background-color: rgba(128,128,128,0.4); -// .message { -// margin: auto; -// margin-top: 100px; -// padding: (@line-height-computed / 2) @line-height-computed; -// width: 400px; -// background-color: white; -// border-radius: 8px; -// } -// .message-wider { -// width: 650px; -// margin-top: 60px; -// padding: 0; -// } - -// .message-header { -// .modal-header; -// } - -// .message-body { -// .modal-body; -// } -// } - -// .diff-panel { -// .full-size; -// margin-right: @changesListWidth; -// } - -// .diff { -// .full-size; -// .toolbar { -// padding: 3px; -// .name { -// float: left; -// padding: 3px @line-height-computed / 4; -// display: inline-block; -// } -// } -// .diff-editor { -// .full-size; -// top: 40px; -// } -// .hide-ace-cursor { -// .ace_active-line, .ace_cursor-layer, .ace_gutter-active-line { -// display: none; -// } -// } -// .diff-deleted { -// padding: @line-height-computed; -// } -// .deleted-warning { -// background-color: @brand-danger; -// color: white; -// padding: @line-height-computed / 2; -// margin-right: @line-height-computed / 4; -// } -// &-binary { -// .alert { -// margin: @line-height-computed / 2; -// } -// } -// } - -// aside.change-list { -// border-left: 1px solid @editor-border-color; -// height: 100%; -// width: @changesListWidth; -// position: absolute; -// right: 0; - -// .loading { -// text-align: center; -// font-family: @font-family-serif; -// } - -// ul { -// li.change { -// position: relative; -// user-select: none; -// -ms-user-select: none; -// -moz-user-select: none; -// -webkit-user-select: none; - -// .day { -// background-color: #fafafa; -// border-bottom: 1px solid @editor-border-color; -// padding: 4px; -// font-weight: bold; -// text-align: center; -// height: @day-header-height; -// font-size: 14px; -// line-height: 1; -// } -// .selectors { -// input { -// margin: 0; -// } -// position: absolute; -// left: @selector-padding-horizontal; -// top: 0; -// bottom: 0; -// width: 24px; -// .selector-from { -// position: absolute; -// bottom: @selector-padding-vertical; -// left: 0; -// opacity: 0.8; -// } -// .selector-to { -// position: absolute; -// top: @selector-padding-vertical; -// left: 0; -// opacity: 0.8; -// } -// .range { -// position: absolute; -// left: 5px; -// width: 4px; -// top: 0; -// bottom: 0; -// } -// } -// .description { -// padding: (@line-height-computed / 4); -// padding-left: 38px; -// min-height: 38px; -// border-bottom: 1px solid @editor-border-color; -// cursor: pointer; -// &:hover { -// background-color: @gray-lightest; -// } -// } -// .users { -// .user { -// font-size: 0.8rem; -// color: @gray; -// text-transform: capitalize; -// position: relative; -// padding-left: 16px; -// .color-square { -// height: 12px; -// width: 12px; -// border-radius: 3px; -// position: absolute; -// left: 0; -// bottom: 3px; -// } -// .name { -// width: 94%; -// white-space: nowrap; -// overflow: hidden; -// text-overflow: ellipsis; -// } -// } -// } -// .time { -// float: right; -// color: @gray; -// display: inline-block; -// padding-right: (@line-height-computed / 2); -// font-size: 0.8rem; -// line-height: @line-height-computed; -// } -// .doc { -// font-size: 0.9rem; -// font-weight: bold; -// } -// .action { -// color: @gray; -// text-transform: uppercase; -// font-size: 0.7em; -// margin-bottom: -2px; -// margin-top: 2px; -// &-edited { -// margin-top: 0; -// } -// } -// } -// li.loading-changes, li.empty-message { -// padding: 6px; -// cursor: default; -// &:hover { -// background-color: inherit; -// } -// } -// li.selected { -// border-left: 4px solid @range-bar-color; -// .day { -// padding-left: 0; -// } -// .description { -// padding-left: 34px; -// } -// .selectors { -// left: @selector-padding-horizontal - 4px; -// .range { -// background-color: @range-bar-color; -// } -// } -// } -// li.selected-to { -// .selectors { -// .range { -// top: @range-bar-selected-offset; -// } -// .selector-to { -// opacity: 1; -// } -// } -// } -// li.selected-from { -// .selectors { -// .range { -// bottom: @range-bar-selected-offset; -// } -// .selector-from { -// opacity: 1; -// } -// } -// } -// li.first-in-day { -// .selectors { -// .selector-to { -// top: @day-header-height + @selector-padding-vertical; -// } -// } -// } -// li.first-in-day.selected-to { -// .selectors { -// .range { -// top: @day-header-height + @range-bar-selected-offset; -// } -// } -// } -// } -// ul.hover-state { -// li { -// .selectors { -// .range { -// background-color: transparent; -// top: 0; -// bottom: 0; -// } -// } -// } -// li.hover-selected { -// .selectors { -// .range { -// top: 0; -// background-color: @gray-light; -// } -// } -// } -// li.hover-selected-to { -// .selectors { -// .range { -// top: @range-bar-selected-offset; -// } -// .selector-to { -// opacity: 1; -// } -// } -// } -// li.hover-selected-from { -// .selectors { -// .range { -// bottom: @range-bar-selected-offset; -// } -// .selector-from { -// opacity: 1; -// } -// } -// } -// li.first-in-day.hover-selected-to { -// .selectors { -// .range { -// top: @day-header-height + @range-bar-selected-offset; -// } -// } -// } -// } -// } -// } - -// .diff-deleted { -// padding-top: 15px; -// } - -// .editor-dark { -// #history { -// aside.change-list { -// border-color: @editor-dark-toolbar-border-color; - -// ul li.change { -// .day { -// background-color: darken(@editor-dark-background-color, 10%); -// border-bottom: 1px solid @editor-dark-toolbar-border-color; -// } -// .description { -// border-bottom: 1px solid @editor-dark-toolbar-border-color; -// &:hover { -// background-color: black; -// } -// } -// } -// } -// } -// } diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less index 4006b56a7b..c5499180e0 100644 --- a/services/web/public/stylesheets/app/editor/toolbar.less +++ b/services/web/public/stylesheets/app/editor/toolbar.less @@ -194,56 +194,57 @@ } .toggle-switch { - position: relative; + position: relative; height: 100%; width: 100%; - background-color: @toggle-switch-bg; - border-radius: @btn-border-radius-base; + background-color: @toggle-switch-bg; + border-radius: @btn-border-radius-base; } .toggle-switch-label { position: relative; display: block; font-weight: normal; - z-index: 2; - float: left; - width: 50%; - height: 100%; - line-height: 24px; + z-index: 2; + float: left; + width: 50%; + height: 100%; + line-height: 24px; text-align: center; margin-bottom: 0; - cursor: pointer; - user-select: none; - transition: color 0.12s ease-out; + cursor: pointer; + user-select: none; + color: @text-color; + transition: color 0.12s ease-out; } .toggle-switch-input { - position: absolute; - opacity: 0; + position: absolute; + opacity: 0; } .toggle-switch-input:checked + .toggle-switch-label { - color: #fff; - font-weight: bold; + color: #fff; + font-weight: bold; } .toggle-switch-selection { - display: block; - position: absolute; - z-index: 1; - top: 2px; - left: 2px; - right: 2px; - width: calc(~"50% - 2px"); - height: calc(~"100% - 4px"); - background: @toggle-switch-highlight-color; - border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base; - transition: transform 0.12s ease-out, border-radius 0.12s ease-out; + display: block; + position: absolute; + z-index: 1; + top: 2px; + left: 2px; + right: 2px; + width: calc(~"50% - 2px"); + height: calc(~"100% - 4px"); + background: @toggle-switch-highlight-color; + border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base; + transition: transform 0.12s ease-out, border-radius 0.12s ease-out; } .toggle-switch-input:checked:nth-child(4) ~ .toggle-switch-selection { - transform: translate(100%); - border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0; + transform: translate(100%); + border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0; } /************************************** From 687637eec71bacd2aa8b8a92ced6330aa2d9c728 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Aug 2018 13:15:51 +0100 Subject: [PATCH 143/188] change email address in newsletter when changing default email put mongo_id into mailchimp merge fields --- .../Newsletter/NewsletterManager.coffee | 2 +- .../coffee/Features/User/UserUpdater.coffee | 22 ++++++++++------ .../unit/coffee/User/UserUpdaterTests.coffee | 26 ++++++++++++++++--- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 0c1a98a6b1..431eb214c3 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -4,7 +4,6 @@ Settings = require 'settings-sharelatex' crypto = require('crypto') Mailchimp = require('mailchimp-api-v3') - if !Settings.mailchimp?.api_key? logger.warn "No newsletter provider configured so not chaning email on user" mailchimp = @@ -63,5 +62,6 @@ buildOptions = (user, is_subscribed)-> merge_fields: FNAME: user.first_name LNAME: user.last_name + MONGO_ID:user._id return opts diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 7d828366e7..5770c7b834 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -100,15 +100,21 @@ module.exports = UserUpdater = setDefaultEmailAddress: (userId, email, callback) -> email = EmailHelper.parseEmail(email) return callback(new Error('invalid email')) if !email? - query = _id: userId, 'emails.email': email - update = $set: email: email - @updateUser query, update, (error, res) -> - if error? - logger.err error:error, 'problem setting default emails' + UserGetter.getUserEmail userId, (error, oldEmail) => + if err? return callback(error) - if res.n == 0 # TODO: Check n or nMatched? - return callback(new Error('Default email does not belong to user')) - callback() + query = _id: userId, 'emails.email': email + update = $set: email: email + @updateUser query, update, (error, res) -> + if error? + logger.err error:error, 'problem setting default emails' + return callback(error) + else if res.n == 0 # TODO: Check n or nMatched? + return callback(new Error('Default email does not belong to user')) + else + NewsletterManager.changeEmail oldEmail, email, callback + + updateV1AndSetDefaultEmailAddress: (userId, email, callback) -> @updateEmailAddressInV1 userId, email, (error) => diff --git a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee index f2ac951727..17f691edba 100644 --- a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee +++ b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee @@ -18,21 +18,27 @@ describe "UserUpdater", -> getUserEmail: sinon.stub() getUserByAnyEmail: sinon.stub() ensureUniqueEmailAddress: sinon.stub() - @logger = err: sinon.stub(), log: -> + @logger = + err: sinon.stub() + log: -> + warn: -> @addAffiliation = sinon.stub().yields() @removeAffiliation = sinon.stub().callsArgWith(2, null) @refreshFeatures = sinon.stub().yields() + @NewsletterManager = + changeEmail:sinon.stub() @UserUpdater = SandboxedModule.require modulePath, requires: "logger-sharelatex": @logger + "../../infrastructure/mongojs":@mongojs + "metrics-sharelatex": timeAsyncMethod: sinon.stub() "./UserGetter": @UserGetter '../Institutions/InstitutionsAPI': addAffiliation: @addAffiliation removeAffiliation: @removeAffiliation '../Subscription/FeaturesUpdater': refreshFeatures: @refreshFeatures - "../../infrastructure/mongojs":@mongojs - "metrics-sharelatex": timeAsyncMethod: sinon.stub() "settings-sharelatex": @settings = {} "request": @request = {} + "../Newsletter/NewsletterManager": @NewsletterManager @stubbedUser = _id: "3131231" @@ -174,6 +180,10 @@ describe "UserUpdater", -> done() describe 'setDefaultEmailAddress', -> + beforeEach -> + @UserGetter.getUserEmail.callsArgWith(1, null, @stubbedUser.email) + @NewsletterManager.changeEmail.callsArgWith(2, null) + it 'set default', (done)-> @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1) @@ -185,6 +195,16 @@ describe "UserUpdater", -> ).should.equal true done() + it 'set changed the email in newsletter', (done)-> + @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1) + + @UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=> + should.not.exist(err) + @NewsletterManager.changeEmail.calledWith( + @stubbedUser.email, @newEmail + ).should.equal true + done() + it 'handle error', (done)-> @UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope')) From c68366155e0e2e56c47ba9c0c9492b4aa0d440f3 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Aug 2018 14:14:56 +0100 Subject: [PATCH 144/188] remove sanitize = require('sanitizer') not used anywhere --- .../web/app/coffee/Features/User/UserRegistrationHandler.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee index 848434271f..1291142dab 100644 --- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee @@ -1,4 +1,3 @@ -resanitize = require('sanitizer') User = require("../../models/User").User UserCreator = require("./UserCreator") UserGetter = require("./UserGetter") From df161d3ece9f11673e7cdc04fd6ea6ec36e02528 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Aug 2018 14:32:36 +0100 Subject: [PATCH 145/188] change newsletter log to info on process boot --- .../app/coffee/Features/Newsletter/NewsletterManager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 431eb214c3..5fb2e00eb7 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -5,10 +5,11 @@ crypto = require('crypto') Mailchimp = require('mailchimp-api-v3') if !Settings.mailchimp?.api_key? - logger.warn "No newsletter provider configured so not chaning email on user" + logger.info "Using newsletter provider: none" mailchimp = request: (opts, cb)-> cb() else + logger.info "Using newsletter provider: mailchimp" mailchimp = new Mailchimp(Settings.mailchimp?.api_key) module.exports = From d59fe61a83feb9775066012378968e4df14199f7 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 9 Aug 2018 11:03:00 +0100 Subject: [PATCH 146/188] Handle sorting of labels; keep selection between view changes. --- .../project/editor/history/entriesListV2.pug | 11 +-- .../project/editor/history/toolbarV2.pug | 1 + .../ide/history/HistoryV2Manager.coffee | 69 +++++++++++++++---- .../components/historyLabelsList.coffee | 4 +- ...HistoryV2DeleteLabelModalController.coffee | 2 +- .../HistoryV2ListController.coffee | 5 +- .../web/public/coffee/ide/services/ide.coffee | 3 +- .../stylesheets/app/editor/history-v2.less | 19 +++-- 8 files changed, 85 insertions(+), 29 deletions(-) diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index e8bebabe69..3f936a8d11 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -152,7 +152,7 @@ script(type="text/ng-template", id="historyEntryTpl") .history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })") history-label( - ng-repeat="label in $ctrl.entry.labels" + ng-repeat="label in $ctrl.entry.labels | orderBy : '-created_at'" label-text="label.comment" label-owner-name="$ctrl.displayNameById(label.user_id)" label-creation-date-time="label.created_at" @@ -208,8 +208,9 @@ script(type="text/ng-template", id="historyEntryTpl") script(type="text/ng-template", id="historyLabelsListTpl") .history-labels-list .history-entry-label( - ng-repeat="label in $ctrl.labels | orderBy : [ '-version', '-created_at' ] track by label.id" + ng-repeat="label in $ctrl.labels track by label.id" ng-click="$ctrl.onLabelSelect({ label: label })" + ng-class="{ 'history-entry-label-selected': label.selected }" ) history-label( show-tooltip="false" @@ -222,15 +223,15 @@ script(type="text/ng-template", id="historyLabelsListTpl") | Saved by span.name( ng-if="user && user._id !== $ctrl.currentUser.id" - ng-style="$ctrl.getUserCSSStyle(user);" + ng-style="$ctrl.getUserCSSStyle(user, label);" ) {{ ::$ctrl.displayName(user) }} span.name( ng-if="user && user._id == $ctrl.currentUser.id" - ng-style="$ctrl.getUserCSSStyle(user);" + ng-style="$ctrl.getUserCSSStyle(user, label);" ) You span.name( ng-if="user == null" - ng-style="$ctrl.getUserCSSStyle(user);" + ng-style="$ctrl.getUserCSSStyle(user, label);" ) #{translate("anonymous")} time.history-entry-label-metadata-time {{ ::label.created_at | formatDate }} .loading(ng-show="$ctrl.isLoading") diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index 8774754de2..6abe167f42 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -9,6 +9,7 @@ time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }} button.history-toolbar-btn( ng-click="showAddLabelDialog();" + ng-if="!history.showOnlyLabels" ng-disabled="history.loadingFileTree" ) i.fa.fa-tag diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index 519a94a54a..2e56d955ce 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -49,6 +49,10 @@ define [ else @reloadDiff() + @$scope.$watch "history.showOnlyLabels", (showOnlyLabels, prevVal) => + if showOnlyLabels? and showOnlyLabels != prevVal and showOnlyLabels + @selectedLabelFromUpdatesSelection() + show: () -> @$scope.ui.view = "history" @reset() @@ -131,6 +135,36 @@ define [ @$scope.history.updates[selectedUpdateIndex].selectedFrom = true @loadFileTreeForUpdate @$scope.history.updates[selectedUpdateIndex] + selectLastLabel = () -> + return if @$scope.history.labels.length == 0 + # TODO Select last label + + selectedLabelFromUpdatesSelection: () -> + nLabels = @$scope.history.selection.updates?[0]?.labels?.length + if nLabels == 1 + @selectLabel @$scope.history.selection.updates[0].labels[0] + else if nLabels > 1 + sortedLabels = @ide.$filter("orderBy")(@$scope.history.selection.updates[0].labels, '-created_at') + lastLabelFromUpdate = sortedLabels[0] + @selectLabel lastLabelFromUpdate + + selectLabel: (labelToSelect) -> + labelToSelectIndex = -1 + for update, i in @$scope.history.updates + if update.toV == labelToSelect.version + labelToSelectIndex = i + break + if labelToSelectIndex == -1 + labelToSelectIndex = 0 + for update in @$scope.history.updates + update.selectedTo = false + update.selectedFrom = false + for label in @$scope.history.labels + label.selected = (labelToSelect.id == label.id) + @$scope.history.updates[labelToSelectIndex].selectedTo = true + @$scope.history.updates[labelToSelectIndex].selectedFrom = true + @loadFileTreeForUpdate @$scope.history.updates[labelToSelectIndex] + BATCH_SIZE: 10 fetchNextBatchOfUpdates: () -> updatesURL = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}" @@ -155,9 +189,12 @@ define [ if !updatesData.nextBeforeTimestamp? @$scope.history.atEnd = true if response.labels? - @$scope.history.labels = response.labels.data + @$scope.history.labels = @_sortLabelsByVersionAndDate response.labels.data @$scope.history.loading = false + _sortLabelsByVersionAndDate: (labels) -> + @ide.$filter("orderBy")(labels, [ '-version', '-created_at' ]) + loadFileAtPointInTime: () -> pathname = @$scope.history.selection.pathname toV = @$scope.history.selection.updates[0].toV @@ -219,8 +256,8 @@ define [ labelCurrentVersion: (labelComment) => @_labelVersion labelComment, @$scope.history.selection.updates[0].toV - deleteLabel: (labelId) => - url = "/project/#{@$scope.project_id}/labels/#{labelId}" + deleteLabel: (label) => + url = "/project/#{@$scope.project_id}/labels/#{label.id}" @ide.$http({ url, @@ -228,14 +265,16 @@ define [ headers: "X-CSRF-Token": window.csrfToken }).then (response) => - @_deleteLabelFromLocalCollection @$scope.history.updates, labelId - @_deleteLabelFromLocalCollection @$scope.history.selection, labelId + @_deleteLabelLocally label - - _deleteLabelFromLocalCollection: (collection, labelId) -> - for update in collection - update.labels = _.filter update.labels, (label) -> - label.id != labelId + _deleteLabelLocally: (labelToDelete) -> + for update, i in @$scope.history.updates + if update.toV == labelToDelete.version + update.labels = _.filter update.labels, (label) -> + label.id != labelToDelete.id + break + @$scope.history.labels = _.filter @$scope.history.labels, (label) -> + label.id != labelToDelete.id _parseDiff: (diff) -> if diff.binary @@ -314,7 +353,10 @@ define [ if @$scope.history.viewMode == HistoryViewModes.COMPARE @autoSelectRecentUpdates() else - @autoSelectLastUpdate() + if @$scope.history.showOnlyLabels + @selectLastLabel() + else + @autoSelectLastUpdate() _labelVersion: (comment, version) -> url = "/project/#{@$scope.project_id}/labels" @@ -327,10 +369,11 @@ define [ .then (response) => @_addLabelToLocalUpdate response.data - _addLabelToLocalUpdate: (label) -> + _addLabelToLocalUpdate: (label) => localUpdate = _.find @$scope.history.updates, (update) -> update.toV == label.version if localUpdate? - localUpdate.labels.push label + localUpdate.labels = @_sortLabelsByVersionAndDate localUpdate.labels.concat label + @$scope.history.labels = @_sortLabelsByVersionAndDate @$scope.history.labels.concat label _perDocSummaryOfUpdates: (updates) -> # Track current_pathname -> original_pathname diff --git a/services/web/public/coffee/ide/history/components/historyLabelsList.coffee b/services/web/public/coffee/ide/history/components/historyLabelsList.coffee index b95700250f..44c9a54742 100644 --- a/services/web/public/coffee/ide/history/components/historyLabelsList.coffee +++ b/services/web/public/coffee/ide/history/components/historyLabelsList.coffee @@ -13,10 +13,10 @@ define [ curUserId = user?._id or user?.id curUserId == id ctrl.displayName = displayNameForUser - ctrl.getUserCSSStyle = (user) -> + ctrl.getUserCSSStyle = (user, label) -> curUserId = user?._id or user?.id hue = ColorManager.getHueForUserId(curUserId) or 100 - if false #ctrl.entry.inSelection + if label.selected color : "#FFF" else color: "hsl(#{ hue }, 70%, 50%)" diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee index d03786c2c6..6001c33762 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee @@ -9,7 +9,7 @@ define [ $scope.deleteLabel = () -> $scope.state.inflight = true - ide.historyManager.deleteLabel labelDetails.id + ide.historyManager.deleteLabel labelDetails .then (response) -> $scope.state.inflight = false $modalInstance.close() diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee index a2873d47d8..7129e0374c 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee @@ -17,8 +17,9 @@ define [ $scope.recalculateSelectedUpdates() $scope.handleLabelSelect = (label) -> - console.log label - + ide.historyManager.selectLabel(label) + $scope.recalculateSelectedUpdates() + $scope.handleLabelDelete = (labelDetails) -> $modal.open( templateUrl: "historyV2DeleteLabelModalTemplate" diff --git a/services/web/public/coffee/ide/services/ide.coffee b/services/web/public/coffee/ide/services/ide.coffee index 6462859df2..f9c61b4114 100644 --- a/services/web/public/coffee/ide/services/ide.coffee +++ b/services/web/public/coffee/ide/services/ide.coffee @@ -3,11 +3,12 @@ define [ ], (App) -> # We create and provide this as service so that we can access the global ide # from within other parts of the angular app. - App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", ($http, queuedHttp, $modal, $q) -> + App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", "$filter", ($http, queuedHttp, $modal, $q, $filter) -> ide = {} ide.$http = $http ide.queuedHttp = queuedHttp ide.$q = $q + ide.$filter = $filter @recentEvents = [] ide.pushEvent = (type, meta = {}) => diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index c2e7ce6b26..7bb10f88f2 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -56,7 +56,8 @@ padding: 5px 10px; cursor: pointer; - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { background-color: @history-entry-selected-bg; color: #FFF; } @@ -68,7 +69,8 @@ margin-bottom: 3px; margin-right: 10px; white-space: nowrap; - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { color: @history-entry-selected-label-color; } } @@ -77,7 +79,8 @@ padding: 0 @padding-xs-horizontal 1px @padding-xs-horizontal; border: 0; background-color: @history-entry-label-bg-color; - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { background-color: @history-entry-selected-label-bg-color; } } @@ -99,7 +102,8 @@ border-radius: 0 9999px 9999px 0; &:hover { background-color: darken(@history-entry-label-bg-color, 8%); - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { background-color: darken(@history-entry-selected-label-bg-color, 8%); } } @@ -137,7 +141,8 @@ color: @history-highlight-color; font-weight: bold; word-break: break-all; - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { color: #FFF; } } @@ -169,6 +174,10 @@ .history-entry-label { .history-entry-details; padding: 7px 10px; + &.history-entry-label-selected { + background-color: @history-entry-selected-bg; + color: #FFF; + } } .history-file-tree-inner { From d6ab993519bff56a3c5082738e20aa9535fd269f Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 9 Aug 2018 12:00:53 +0100 Subject: [PATCH 147/188] Dont hide history entries while loading. --- services/web/app/views/project/editor/history/entriesListV2.pug | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index e08de0effd..ca0c48b722 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -119,7 +119,6 @@ script(type="text/ng-template", id="historyEntriesListTpl") users="$ctrl.users" on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })" on-label-delete="$ctrl.onLabelDelete({ label: label })" - ng-show="!$ctrl.isLoading" ) .loading(ng-show="$ctrl.isLoading") i.fa.fa-spin.fa-refresh From 4f4ab579583f2c0b237a68e4dc5aed2fd4eefadc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 9 Aug 2018 12:05:00 +0100 Subject: [PATCH 148/188] Use controller binding in the label tooltip. --- services/web/app/views/project/editor/history.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index 2f3f317561..fec7327d6f 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -92,7 +92,7 @@ script(type="text/ng-template", id="historyLabelTooltipTpl") i.fa.fa-tag |  {{ $ctrl.labelText }} p.history-label-tooltip-owner #{translate("history_label_created_by")} {{ $ctrl.labelOwnerName }} - time.history-label-tooltip-datetime {{ labelCreationDateTime | formatDate }} + time.history-label-tooltip-datetime {{ $ctrl.labelCreationDateTime | formatDate }} script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate") From 64a66aafd5452ddd9728511423186d974678360b Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 9 Aug 2018 12:16:36 +0100 Subject: [PATCH 149/188] Protect against reading the project members list too soon. --- .../ide/history/controllers/HistoryListController.coffee | 8 ++++++-- .../history/controllers/HistoryV2ListController.coffee | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index cfd5bb75c5..6c9890c6e5 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -6,13 +6,17 @@ define [ App.controller "HistoryListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) -> $scope.hoveringOverListSelectors = false - projectUsers = $scope.project.members.concat $scope.project.owner + $scope.projectUsers = [] + + $scope.$watch "project.members", (newVal) -> + if newVal? + $scope.projectUsers = newVal.concat $scope.project.owner # This method (and maybe the one below) will be removed soon. User details data will be # injected into the history API responses, so we won't need to fetch user data from other # local data structures. _getUserById = (id) -> - _.find projectUsers, (user) -> + _.find $scope.projectUsers, (user) -> curUserId = user?._id or user?.id curUserId == id diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee index f7c602733f..1635ec9f0a 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee @@ -7,8 +7,13 @@ define [ $scope.hoveringOverListSelectors = false $scope.listConfig = showOnlyLabelled: false - $scope.projectUsers = $scope.project.members.concat $scope.project.owner + $scope.projectUsers = [] + + $scope.$watch "project.members", (newVal) -> + if newVal? + $scope.projectUsers = newVal.concat $scope.project.owner + $scope.loadMore = () => ide.historyManager.fetchNextBatchOfUpdates() From 7830a5f69de7bd197c943e03b60acff8bd7ba143 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 9 Aug 2018 13:16:42 +0100 Subject: [PATCH 150/188] Update copy in v1 import modal now that more features are supported --- services/web/app/views/project/list/modals.pug | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 2bbde60e7b..aaacf660a6 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -345,13 +345,11 @@ script(type="text/ng-template", id="v1ImportModalTemplate") i.fa.fa-flask .v1-import-col h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental - p We are still working hard to bring some Overleaf v1 features to the v2 editor. In v2 there is: + p We are still working hard to bring some Overleaf v1 features to the v2 editor. In v2: ul - li No Journals and Services menu to submit directly to our partners yet - li No Rich Text (WYSIWYG) mode yet - li No linked files (to URLs or to files in other Overleaf projects) yet - li No Zotero and CiteULike integrations yet - li No labelled versions yet + li You may not be able to access all of your Labelled versions yet + li There are no Zotero and CiteULike integrations yet + li Some Journals and Services in the Submit menu don't support direct submissions yet p.row-spaced-small | If you currently use the Overleaf Git bridge with your v1 project, you can migrate your project to the Overleaf v2 GitHub integration. | From e80b52509b8899e4f5f5539f5a979abcd498cc49 Mon Sep 17 00:00:00 2001 From: Nate Stemen Date: Thu, 9 Aug 2018 08:53:49 -0400 Subject: [PATCH 151/188] fix silly paren issue for index var --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 3be4369213..8fde8595cd 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -162,7 +162,8 @@ define [ cursorPosition = @editor.getCursorPosition() end = change.end {lineUpToCursor, commandFragment} = Helpers.getContext(@editor, end) - if (i = lineUpToCursor.indexOf('%') > -1 and lineUpToCursor[i-1] != '\\') + if ((i = lineUpToCursor.indexOf('%')) > -1 and lineUpToCursor[i-1] != '\\') + console.log lineUpToCursor, i return lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\" lastTwoChars = lineUpToCursor.slice(-2) From 7e86218b212f4ca06f1793c5e0c0573a35636166 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 10 Aug 2018 11:28:17 +0100 Subject: [PATCH 152/188] Allow multiple events to trigger layout external resizes. --- services/web/app/views/project/editor.pug | 2 +- .../public/coffee/ide/directives/layout.coffee | 3 ++- .../coffee/ide/history/HistoryV2Manager.coffee | 17 ++++++++++------- .../web/public/coffee/ide/services/ide.coffee | 3 ++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 6d7d18687d..08124c334c 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -65,7 +65,7 @@ block content ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME) }", layout="main", ng-hide="state.loading", - resize-on="layout:chat:resize", + resize-on="layout:chat:resize,history:toggle", minimum-restore-size-west="130" custom-toggler-pane=hasFeature('custom-togglers') ? "'west'" : "false" custom-toggler-msg-when-open=hasFeature('custom-togglers') ? "'" + translate("tooltip_hide_filetree") + "'" : "false" diff --git a/services/web/public/coffee/ide/directives/layout.coffee b/services/web/public/coffee/ide/directives/layout.coffee index b4e2e7f83d..8c30943f39 100644 --- a/services/web/public/coffee/ide/directives/layout.coffee +++ b/services/web/public/coffee/ide/directives/layout.coffee @@ -112,7 +112,8 @@ define [ element.layout().resizeAll() if attrs.resizeOn? - scope.$on attrs.resizeOn, () -> onExternalResize() + for event in attrs.resizeOn.split "," + scope.$on event, () -> onExternalResize() if hasCustomToggler state = element.layout().readState() diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index 2e56d955ce..e7a2c071e3 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -27,6 +27,9 @@ define [ @hide() else @show() + @ide.$timeout () => + @$scope.$broadcast "history:toggle" + , 0 @$scope.toggleHistoryViewMode = () => if @$scope.history.viewMode == HistoryViewModes.COMPARE @@ -124,6 +127,10 @@ define [ return if @$scope.history.updates.length == 0 @selectUpdate @$scope.history.updates[0] + autoSelectLastLabel: () -> + return if @$scope.history.labels.length == 0 + @selectLabel @$scope.history.labels[0] + selectUpdate: (update) -> selectedUpdateIndex = @$scope.history.updates.indexOf update if selectedUpdateIndex == -1 @@ -135,10 +142,6 @@ define [ @$scope.history.updates[selectedUpdateIndex].selectedFrom = true @loadFileTreeForUpdate @$scope.history.updates[selectedUpdateIndex] - selectLastLabel = () -> - return if @$scope.history.labels.length == 0 - # TODO Select last label - selectedLabelFromUpdatesSelection: () -> nLabels = @$scope.history.selection.updates?[0]?.labels?.length if nLabels == 1 @@ -184,12 +187,12 @@ define [ @ide.$q.all requests .then (response) => updatesData = response.updates.data + if response.labels? + @$scope.history.labels = @_sortLabelsByVersionAndDate response.labels.data @_loadUpdates(updatesData.updates) @$scope.history.nextBeforeTimestamp = updatesData.nextBeforeTimestamp if !updatesData.nextBeforeTimestamp? @$scope.history.atEnd = true - if response.labels? - @$scope.history.labels = @_sortLabelsByVersionAndDate response.labels.data @$scope.history.loading = false _sortLabelsByVersionAndDate: (labels) -> @@ -354,7 +357,7 @@ define [ @autoSelectRecentUpdates() else if @$scope.history.showOnlyLabels - @selectLastLabel() + @autoSelectLastLabel() else @autoSelectLastUpdate() diff --git a/services/web/public/coffee/ide/services/ide.coffee b/services/web/public/coffee/ide/services/ide.coffee index f9c61b4114..24805b6270 100644 --- a/services/web/public/coffee/ide/services/ide.coffee +++ b/services/web/public/coffee/ide/services/ide.coffee @@ -3,12 +3,13 @@ define [ ], (App) -> # We create and provide this as service so that we can access the global ide # from within other parts of the angular app. - App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", "$filter", ($http, queuedHttp, $modal, $q, $filter) -> + App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", "$filter", "$timeout", ($http, queuedHttp, $modal, $q, $filter, $timeout) -> ide = {} ide.$http = $http ide.queuedHttp = queuedHttp ide.$q = $q ide.$filter = $filter + ide.$timeout = $timeout @recentEvents = [] ide.pushEvent = (type, meta = {}) => From 197e7ce8abfd522cd647a0619e0e543252306dd3 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Fri, 10 Aug 2018 13:40:25 +0200 Subject: [PATCH 153/188] refactor institutions getter --- .../Institutions/InstitutionsFeatures.coffee | 11 +++-- .../Institutions/InstitutionsGetter.coffee | 14 ++++++ .../InstitutionsFeaturesTests.coffee | 38 ++++++---------- .../InstitutionsGetterTests.coffee | 44 +++++++++++++++++++ 4 files changed, 77 insertions(+), 30 deletions(-) create mode 100644 services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee create mode 100644 services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee index 5c91058146..4ce233406b 100644 --- a/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee +++ b/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee @@ -1,4 +1,4 @@ -UserGetter = require '../User/UserGetter' +InstitutionsGetter = require './InstitutionsGetter' PlansLocator = require '../Subscription/PlansLocator' Settings = require 'settings-sharelatex' logger = require 'logger-sharelatex' @@ -13,11 +13,10 @@ module.exports = InstitutionsFeatures = hasLicence: (userId, callback = (error, hasLicence) ->) -> - UserGetter.getUserFullEmails userId, (error, emailsData) -> + InstitutionsGetter.getConfirmedInstitutions userId, (error, institutions) -> return callback error if error? - affiliation = emailsData.find (emailData) -> - licence = emailData.affiliation?.institution?.licence - emailData.confirmedAt? and licence? and licence != 'free' + hasLicence = institutions.some (institution) -> + institution.licence and institution.licence != 'free' - callback(null, !!affiliation) + callback(null, hasLicence) diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee new file mode 100644 index 0000000000..e5941dc777 --- /dev/null +++ b/services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee @@ -0,0 +1,14 @@ +UserGetter = require '../User/UserGetter' +logger = require 'logger-sharelatex' + +module.exports = InstitutionsGetter = + getConfirmedInstitutions: (userId, callback = (error, institutions) ->) -> + UserGetter.getUserFullEmails userId, (error, emailsData) -> + return callback error if error? + + confirmedInstitutions = emailsData.filter (emailData) -> + emailData.confirmedAt? and emailData.affiliation?.institution? + .map (emailData) -> + emailData.affiliation?.institution + + callback(null, confirmedInstitutions) diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee index 2304f2e5b7..8811f72c35 100644 --- a/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee +++ b/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee @@ -8,11 +8,11 @@ modulePath = require('path').join __dirname, '../../../../app/js/Features/Instit describe 'InstitutionsFeatures', -> beforeEach -> - @UserGetter = getUserFullEmails: sinon.stub() + @InstitutionsGetter = getConfirmedInstitutions: sinon.stub() @PlansLocator = findLocalPlanInSettings: sinon.stub() @institutionPlanCode = 'institution_plan_code' @InstitutionsFeatures = SandboxedModule.require modulePath, requires: - '../User/UserGetter': @UserGetter + './InstitutionsGetter': @InstitutionsGetter '../Subscription/PlansLocator': @PlansLocator 'settings-sharelatex': institutionPlanCode: @institutionPlanCode 'logger-sharelatex': @@ -23,47 +23,37 @@ describe 'InstitutionsFeatures', -> describe "hasLicence", -> it 'should handle error', (done)-> - @UserGetter.getUserFullEmails.yields(new Error('Nope')) + @InstitutionsGetter.getConfirmedInstitutions.yields(new Error('Nope')) @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> expect(error).to.exist done() - it 'should return false if user has no affiliations', (done) -> - @UserGetter.getUserFullEmails.yields(null, []) - @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> - expect(error).to.not.exist - expect(hasLicence).to.be.false - done() - it 'should return false if user has no confirmed affiliations', (done) -> - affiliations = [ - { confirmedAt: null, affiliation: institution: { licence: 'pro_plus' } } - ] - @UserGetter.getUserFullEmails.yields(null, affiliations) + institutions = [] + @InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions) @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> expect(error).to.not.exist expect(hasLicence).to.be.false done() it 'should return false if user has no paid affiliations', (done) -> - affiliations = [ - { confirmedAt: new Date(), affiliation: institution: { licence: 'free' } } + institutions = [ + { licence: 'free' } ] - @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions) @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> expect(error).to.not.exist expect(hasLicence).to.be.false done() it 'should return true if user has confirmed paid affiliation', (done)-> - affiliations = [ - { confirmedAt: new Date(), affiliation: institution: { licence: 'pro_plus' } } - { confirmedAt: new Date(), affiliation: institution: { licence: 'free' } } - { confirmedAt: null, affiliation: institution: { licence: 'pro' } } - { confirmedAt: null, affiliation: institution: { licence: null } } - { confirmedAt: new Date(), affiliation: institution: {} } + institutions = [ + { licence: 'pro_plus' } + { licence: 'free' } + { licence: 'pro' } + { licence: null } ] - @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions) @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> expect(error).to.not.exist expect(hasLicence).to.be.true diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee new file mode 100644 index 0000000000..62c57cdaa9 --- /dev/null +++ b/services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee @@ -0,0 +1,44 @@ +SandboxedModule = require('sandboxed-module') +require('chai').should() +expect = require('chai').expect +sinon = require('sinon') +modulePath = require('path').join __dirname, '../../../../app/js/Features/Institutions/InstitutionsGetter.js' + +describe 'InstitutionsGetter', -> + beforeEach -> + @UserGetter = getUserFullEmails: sinon.stub() + @InstitutionsGetter = SandboxedModule.require modulePath, requires: + '../User/UserGetter': @UserGetter + 'logger-sharelatex': + log:-> console.log(arguments) + err:-> + + @userId = '12345abcde' + + describe "getConfirmedInstitutions", -> + it 'filters unconfirmed emails', (done) -> + @userEmails = [ + { confirmedAt: null, affiliation: institution: { id: 123 } } + { confirmedAt: new Date(), affiliation: institution: { id: 456 } } + { confirmedAt: new Date(), affiliation: null } + { confirmedAt: new Date(), affiliation: institution: null } + ] + @UserGetter.getUserFullEmails.yields(null, @userEmails) + @InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) -> + expect(error).to.not.exist + institutions.length.should.equal 1 + institutions[0].id.should.equal 456 + done() + + it 'should handle empty response', (done) -> + @UserGetter.getUserFullEmails.yields(null, []) + @InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) -> + expect(error).to.not.exist + institutions.length.should.equal 0 + done() + + it 'should handle error', (done) -> + @UserGetter.getUserFullEmails.yields(new Error('Nope')) + @InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) -> + expect(error).to.exist + done() From c13e0264e555ec7bb190eb2bd4221cf85c6ee04b Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 10 Aug 2018 14:07:35 +0100 Subject: [PATCH 154/188] Fix a few bugs related to keeping the selection when moving from labels to full history. --- .../ide/history/HistoryV2Manager.coffee | 84 ++++++++++++++----- .../HistoryV2ListController.coffee | 62 -------------- 2 files changed, 62 insertions(+), 84 deletions(-) diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index e7a2c071e3..e8a3f46f24 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -53,8 +53,15 @@ define [ @reloadDiff() @$scope.$watch "history.showOnlyLabels", (showOnlyLabels, prevVal) => - if showOnlyLabels? and showOnlyLabels != prevVal and showOnlyLabels - @selectedLabelFromUpdatesSelection() + if showOnlyLabels? and showOnlyLabels != prevVal + if showOnlyLabels + @selectedLabelFromUpdatesSelection() + else + if @$scope.history.selection.updates.length == 0 + @autoSelectLastUpdate() + + @$scope.$watch "history.updates.length", () => + @recalculateSelectedUpdates() show: () -> @$scope.ui.view = "history" @@ -95,10 +102,9 @@ define [ _csrf: window.csrfToken }) - loadFileTreeForUpdate: (update) -> - {fromV, toV} = update + loadFileTreeForVersion: (version) -> url = "/project/#{@$scope.project_id}/filetree/diff" - query = [ "from=#{toV}", "to=#{toV}" ] + query = [ "from=#{version}", "to=#{version}" ] url += "?" + query.join("&") @$scope.history.loadingFileTree = true @$scope.history.selectedFile = null @@ -140,33 +146,66 @@ define [ update.selectedFrom = false @$scope.history.updates[selectedUpdateIndex].selectedTo = true @$scope.history.updates[selectedUpdateIndex].selectedFrom = true - @loadFileTreeForUpdate @$scope.history.updates[selectedUpdateIndex] + @recalculateSelectedUpdates() + @loadFileTreeForVersion @$scope.history.updates[selectedUpdateIndex].toV selectedLabelFromUpdatesSelection: () -> - nLabels = @$scope.history.selection.updates?[0]?.labels?.length - if nLabels == 1 + # Get the number of labels associated with the currently selected update + nSelectedLabels = @$scope.history.selection.updates?[0]?.labels?.length + # If the currently selected update has no labels, select the last one (version-wise) + if nSelectedLabels == 0 + @autoSelectLastLabel() + # If the update has one label, select it + else if nSelectedLabels == 1 @selectLabel @$scope.history.selection.updates[0].labels[0] - else if nLabels > 1 + # If there are multiple labels for the update, select the latest + else if nSelectedLabels > 1 sortedLabels = @ide.$filter("orderBy")(@$scope.history.selection.updates[0].labels, '-created_at') lastLabelFromUpdate = sortedLabels[0] @selectLabel lastLabelFromUpdate selectLabel: (labelToSelect) -> - labelToSelectIndex = -1 - for update, i in @$scope.history.updates - if update.toV == labelToSelect.version - labelToSelectIndex = i - break - if labelToSelectIndex == -1 - labelToSelectIndex = 0 + updateToSelect = null + alreadySelected = false for update in @$scope.history.updates - update.selectedTo = false - update.selectedFrom = false + if update.toV == labelToSelect.version + updateToSelect = update + break + for label in @$scope.history.labels - label.selected = (labelToSelect.id == label.id) - @$scope.history.updates[labelToSelectIndex].selectedTo = true - @$scope.history.updates[labelToSelectIndex].selectedFrom = true - @loadFileTreeForUpdate @$scope.history.updates[labelToSelectIndex] + matchingLabel = (labelToSelect.id == label.id) + if matchingLabel and label.selected + alreadySelected = true + label.selected = matchingLabel + + if alreadySelected + return + + if updateToSelect? + @selectUpdate updateToSelect + else + @$scope.history.selection.updates = [] + @loadFileTreeForVersion labelToSelect.version + + recalculateSelectedUpdates: () -> + beforeSelection = true + afterSelection = false + @$scope.history.selection.updates = [] + for update in @$scope.history.updates + if update.selectedTo + inSelection = true + beforeSelection = false + + update.beforeSelection = beforeSelection + update.inSelection = inSelection + update.afterSelection = afterSelection + + if inSelection + @$scope.history.selection.updates.push update + + if update.selectedFrom + inSelection = false + afterSelection = true BATCH_SIZE: 10 fetchNextBatchOfUpdates: () -> @@ -199,6 +238,7 @@ define [ @ide.$filter("orderBy")(labels, [ '-version', '-created_at' ]) loadFileAtPointInTime: () -> + console.log @$scope.history.selection.pathname, @$scope.history.selection.updates pathname = @$scope.history.selection.pathname toV = @$scope.history.selection.updates[0].toV url = "/project/#{@$scope.project_id}/diff" diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee index 7129e0374c..55c2d791a9 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee @@ -14,11 +14,9 @@ define [ $scope.handleEntrySelect = (entry) -> ide.historyManager.selectUpdate(entry) - $scope.recalculateSelectedUpdates() $scope.handleLabelSelect = (label) -> ide.historyManager.selectLabel(label) - $scope.recalculateSelectedUpdates() $scope.handleLabelDelete = (labelDetails) -> $modal.open( @@ -27,64 +25,4 @@ define [ resolve: labelDetails: () -> labelDetails ) - - $scope.recalculateSelectedUpdates = () -> - beforeSelection = true - afterSelection = false - $scope.history.selection.updates = [] - for update in $scope.history.updates - if update.selectedTo - inSelection = true - beforeSelection = false - - update.beforeSelection = beforeSelection - update.inSelection = inSelection - update.afterSelection = afterSelection - - if inSelection - $scope.history.selection.updates.push update - - if update.selectedFrom - inSelection = false - afterSelection = true - - $scope.recalculateHoveredUpdates = () -> - hoverSelectedFrom = false - hoverSelectedTo = false - for update in $scope.history.updates - # Figure out whether the to or from selector is hovered over - if update.hoverSelectedFrom - hoverSelectedFrom = true - if update.hoverSelectedTo - hoverSelectedTo = true - - if hoverSelectedFrom - # We want to 'hover select' everything between hoverSelectedFrom and selectedTo - inHoverSelection = false - for update in $scope.history.updates - if update.selectedTo - update.hoverSelectedTo = true - inHoverSelection = true - update.inHoverSelection = inHoverSelection - if update.hoverSelectedFrom - inHoverSelection = false - if hoverSelectedTo - # We want to 'hover select' everything between hoverSelectedTo and selectedFrom - inHoverSelection = false - for update in $scope.history.updates - if update.hoverSelectedTo - inHoverSelection = true - update.inHoverSelection = inHoverSelection - if update.selectedFrom - update.hoverSelectedFrom = true - inHoverSelection = false - - $scope.resetHoverState = () -> - for update in $scope.history.updates - delete update.hoverSelectedFrom - delete update.hoverSelectedTo - delete update.inHoverSelection - - $scope.$watch "history.updates.length", () -> - $scope.recalculateSelectedUpdates() ] \ No newline at end of file From 035ff0a1f0b0984560d9589f289db86fa66d8dce Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 10 Aug 2018 15:57:10 +0100 Subject: [PATCH 155/188] More bugfixing. --- .../public/coffee/ide/history/HistoryV2Manager.coffee | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index e8a3f46f24..ddd098a5f5 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -238,9 +238,15 @@ define [ @ide.$filter("orderBy")(labels, [ '-version', '-created_at' ]) loadFileAtPointInTime: () -> - console.log @$scope.history.selection.pathname, @$scope.history.selection.updates pathname = @$scope.history.selection.pathname - toV = @$scope.history.selection.updates[0].toV + if @$scope.history.selection.updates?[0]? + toV = @$scope.history.selection.updates[0].toV + else + for label in @$scope.history.labels or [] + if label.selected + toV = label.version + if !toV? + return url = "/project/#{@$scope.project_id}/diff" query = ["pathname=#{encodeURIComponent(pathname)}", "from=#{toV}", "to=#{toV}"] url += "?" + query.join("&") From 0f8fe53bc90fccb4ff0ef1720aaf0c8e7a620b93 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 1 Aug 2018 09:37:14 +0100 Subject: [PATCH 156/188] Allow confirmed_at date to be specified --- services/web/app/coffee/Features/User/UserUpdater.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 8d5e3658f9..544f37a82e 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -152,7 +152,10 @@ module.exports = UserUpdater = else return callback new Error("non-success code from v1: #{response.statusCode}") - confirmEmail: (userId, email, callback) -> + confirmEmail: (userId, email, confirmedAt, callback) -> + if arguments.length == 3 + callback = confirmedAt + confirmedAt = new Date() email = EmailHelper.parseEmail(email) return callback(new Error('invalid email')) if !email? logger.log {userId, email}, 'confirming user email' @@ -166,7 +169,7 @@ module.exports = UserUpdater = 'emails.email': email update = $set: - 'emails.$.confirmedAt': new Date() + 'emails.$.confirmedAt': confirmedAt @updateUser query, update, (error, res) -> return callback(error) if error? logger.log {res, userId, email}, "tried to confirm email" From 19dfe5fc8bfe2778a253cfb41540e34ed3413cfc Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 13 Aug 2018 10:22:23 +0100 Subject: [PATCH 157/188] intial version of user setting for texlive imageName --- .../coffee/Features/Editor/EditorController.coffee | 7 +++++++ .../coffee/Features/Project/ProjectController.coffee | 5 +++++ .../Features/Project/ProjectEditorHandler.coffee | 3 ++- .../Features/Project/ProjectOptionsHandler.coffee | 10 ++++++++++ services/web/app/views/project/editor/left-menu.pug | 9 +++++++++ services/web/config/settings.defaults.coffee | 11 +++++++++++ .../settings/controllers/SettingsController.coffee | 11 +++++++++++ 7 files changed, 55 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index 846042312d..7dad02bde9 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -163,6 +163,13 @@ module.exports = EditorController = EditorRealTimeController.emitToRoom project_id, 'compilerUpdated', compiler callback() + setImageName : (project_id, imageName, callback = (err) ->) -> + ProjectOptionsHandler.setImageName project_id, imageName, (err) -> + return callback(err) if err? + logger.log imageName:imageName, project_id:project_id, "setting imageName" + EditorRealTimeController.emitToRoom project_id, 'imageNameUpdated', imageName + callback() + setSpellCheckLanguage : (project_id, languageCode, callback = (err) ->) -> ProjectOptionsHandler.setSpellCheckLanguage project_id, languageCode, (err) -> return callback(err) if err? diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index bb16855885..f3d12bf5c5 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -49,6 +49,10 @@ module.exports = ProjectController = jobs.push (callback) -> editorController.setCompiler project_id, req.body.compiler, callback + if req.body.imageName? + jobs.push (callback) -> + editorController.setImageName project_id, req.body.imageName, callback + if req.body.name? jobs.push (callback) -> editorController.renameProject project_id, req.body.name, callback @@ -347,6 +351,7 @@ module.exports = ProjectController = useV2History: !!project.overleaf?.history?.display richTextEnabled: Features.hasFeature('rich-text') showTestControls: req.query?.tc == 'true' || user.isAdmin + allowedImageNames: Settings.allowedImageNames || [] timer.done() _buildProjectList: (allProjects, v1Projects = [])-> diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 410dbf0351..f81c59c399 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -1,5 +1,5 @@ _ = require("underscore") - +Path = require 'path' module.exports = ProjectEditorHandler = trackChangesAvailable: false @@ -20,6 +20,7 @@ module.exports = ProjectEditorHandler = members: [] invites: invites tokens: project.tokens + imageName: if project.imageName? then Path.basename(project.imageName) else undefined if !result.invites? result.invites = [] diff --git a/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee index 0a0b02e40e..456b164683 100644 --- a/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee @@ -17,6 +17,16 @@ module.exports = if callback? callback() + setImageName : (project_id, imageName, callback = ()->)-> + logger.log project_id:project_id, imageName:imageName, "setting the imageName" + imageName = imageName.toLowerCase() + if ! _.some(settings.allowedImageNames, (allowed) -> imageName is allowed.imageName) + return callback() + conditions = {_id:project_id} + update = {imageName: settings.imageRoot + '/' + imageName} + Project.update conditions, update, {}, (err)-> + if callback? + callback() setSpellCheckLanguage: (project_id, languageCode, callback = ()->)-> logger.log project_id:project_id, languageCode:languageCode, "setting the spell check language" diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index 2cedc0b37e..bbfa4c480c 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -188,6 +188,15 @@ aside#left-menu.full-size( option(value="pdfjs") #{translate("built_in")} option(value="native") #{translate("native")} + if (getSessionUser() && getSessionUser().isAdmin && typeof(allowedImageNames) !== 'undefined' && allowedImageNames.length > 0) + .form-controls(ng-show="permissions.write") + label(for="imageName") #{translate("TeXLive")} + select( + name="imageName" + ng-model="project.imageName" + ) + each image in allowedImageNames + option(value=image.imageName) #{image.imageDesc} h4 #{translate("hotkeys")} ul.list-unstyled.nav diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 1306ce91c2..d6e6ecb690 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -472,3 +472,14 @@ module.exports = settings = autoCompile: everyone: 100 standard: 25 + + # currentImage: "texlive-full:2017.1" + # imageRoot: "" # without any trailing slash + + # allowedImageNames: [ + # {imageName: 'texlive-full:2017.1', imageDesc: 'TeXLive 2017'} + # {imageName: 'wl_texlive:2018.1', imageDesc: 'Legacy OL TeXLive 2015'} + # {imageName: 'texlive-full:2016.1', imageDesc: 'Legacy SL TeXLive 2016'} + # {imageName: 'texlive-full:2015.1', imageDesc: 'Legacy SL TeXLive 2015'} + # {imageName: 'texlive-full:2014.2', imageDesc: 'Legacy SL TeXLive 2014.2'} + # ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee index 9920372aab..2e8c09739e 100644 --- a/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee +++ b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee @@ -67,6 +67,11 @@ define [ if oldCompiler? and compiler != oldCompiler settings.saveProjectSettings({compiler: compiler}) + $scope.$watch "project.imageName", (imageName, oldImageName) => + return if @ignoreUpdates + if oldImageName? and imageName != oldImageName + settings.saveProjectSettings({imageName: imageName}) + $scope.$watch "project.rootDoc_id", (rootDoc_id, oldRootDoc_id) => return if @ignoreUpdates # don't save on initialisation, Angular passes oldRootDoc_id as @@ -83,6 +88,12 @@ define [ $scope.project.compiler = compiler delete @ignoreUpdates + ide.socket.on "imageNameUpdated", (imageName) => + @ignoreUpdates = true + $scope.$apply () => + $scope.project.imageName = imageName + delete @ignoreUpdates + ide.socket.on "spellCheckLanguageUpdated", (languageCode) => @ignoreUpdates = true $scope.$apply () => From c2828c8ca539031230306f405a1d5b5410917aed Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 13 Aug 2018 10:48:11 +0100 Subject: [PATCH 158/188] add unit tests --- .../coffee/Editor/EditorControllerTests.coffee | 14 ++++++++++++++ .../Project/ProjectControllerTests.coffee | 12 ++++++++++++ .../Project/ProjectOptionsHandlerTests.coffee | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee index 703a03d14f..f1bf3792a2 100644 --- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee @@ -32,6 +32,7 @@ describe "EditorController", -> '../Project/ProjectEntityUpdateHandler' : @ProjectEntityUpdateHandler = {} '../Project/ProjectOptionsHandler' : @ProjectOptionsHandler = setCompiler: sinon.stub().yields() + setImageName: sinon.stub().yields() setSpellCheckLanguage: sinon.stub().yields() '../Project/ProjectDetailsHandler': @ProjectDetailsHandler = setProjectDescription: sinon.stub().yields() @@ -377,6 +378,19 @@ describe "EditorController", -> .calledWith(@project_id, "compilerUpdated", @compiler) .should.equal true + describe "setImageName", -> + beforeEach -> + @imageName = "texlive-1234.5" + @EditorController.setImageName @project_id, @imageName, @callback + + it "should send the new imageName and project id to the project options handler", -> + @ProjectOptionsHandler.setImageName + .calledWith(@project_id, @imageName) + .should.equal true + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "imageNameUpdated", @imageName) + .should.equal true + describe "setSpellCheckLanguage", -> beforeEach -> @languageCode = "fr" diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index 1acd1c8128..46e54501c9 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -145,6 +145,18 @@ describe "ProjectController", -> done() @ProjectController.updateProjectSettings @req, @res + it "should update the imageName", (done) -> + @EditorController.setImageName = sinon.stub().callsArg(2) + @req.body = + imageName: @imageName = "texlive-1234.5" + @res.sendStatus = (code) => + @EditorController.setImageName + .calledWith(@project_id, @imageName) + .should.equal true + code.should.equal 204 + done() + @ProjectController.updateProjectSettings @req, @res + it "should update the spell check language", (done) -> @EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2) @req.body = diff --git a/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee index 825822c236..435dba9dbc 100644 --- a/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee @@ -19,6 +19,11 @@ describe 'creating a project', -> {name: "English", code: "en"} {name: "French", code: "fr"} ] + imageRoot: "docker-repo/subdir" + allowedImageNames: [ + {imageName: "texlive-0000.0", imageDesc: "test image 0"} + {imageName: "texlive-1234.5", imageDesc: "test image 1"} + ] 'logger-sharelatex': log:-> err:-> @@ -37,6 +42,19 @@ describe 'creating a project', -> @projectModel.update.called.should.equal false done() + describe 'Setting the imageName', -> + it 'should perform and update on mongo', (done)-> + @handler.setImageName project_id, "texlive-1234.5", (err)=> + args = @projectModel.update.args[0] + args[0]._id.should.equal project_id + args[1].imageName.should.equal "docker-repo/subdir/texlive-1234.5" + done() + @projectModel.update.args[0][3]() + + it 'should not perform and update on mongo if it is not a reconised compiler', (done)-> + @handler.setImageName project_id, "something", (err)=> + @projectModel.update.called.should.equal false + done() describe "setting the spellCheckLanguage", -> From 2ee42af1de4fd6e0408840050041d8aaf2b8efa1 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 13 Aug 2018 13:39:25 +0100 Subject: [PATCH 159/188] Change strategy to store selected label; improve the history toolbar layout to accomodate larger labels; fix issues with layout not being updated when changing history view modes. --- .../project/editor/history/entriesListV2.pug | 3 +- .../project/editor/history/toolbarV2.pug | 37 +++++++++++-------- .../ide/history/HistoryV2Manager.coffee | 31 +++++++++------- .../components/historyLabelsList.coffee | 3 +- .../stylesheets/app/editor/history-v2.less | 32 +++++++++------- 5 files changed, 62 insertions(+), 44 deletions(-) diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index 345eab8dd6..21c2438742 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -20,6 +20,7 @@ aside.change-list( current-user="user" users="projectUsers" is-loading="history.loading" + selected-label="history.selection.label" on-label-select="handleLabelSelect(label)" on-label-delete="handleLabelDelete(label)" ) @@ -209,7 +210,7 @@ script(type="text/ng-template", id="historyLabelsListTpl") .history-entry-label( ng-repeat="label in $ctrl.labels track by label.id" ng-click="$ctrl.onLabelSelect({ label: label })" - ng-class="{ 'history-entry-label-selected': label.selected }" + ng-class="{ 'history-entry-label-selected': label.id === $ctrl.selectedLabel.id }" ) history-label( show-tooltip="false" diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index 6abe167f42..4e4b6f7ae3 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -2,24 +2,31 @@ ng-controller="HistoryV2ToolbarController" ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME" ) - span(ng-show="history.loadingFileTree") + span.history-toolbar-selected-version(ng-show="history.loadingFileTree") i.fa.fa-spin.fa-refresh |    #{translate("loading")}... - span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}  + span.history-toolbar-selected-version( + ng-show="!history.loadingFileTree && !history.showOnlyLabels" + ) #{translate("browsing_project_as_of")}  time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }} - button.history-toolbar-btn( - ng-click="showAddLabelDialog();" - ng-if="!history.showOnlyLabels" - ng-disabled="history.loadingFileTree" - ) - i.fa.fa-tag - |  #{translate("history_label_this_version")} - button.history-toolbar-btn( - ng-click="toggleHistoryViewMode();" - ng-disabled="history.loadingFileTree" - ) - i.fa.fa-exchange - |  #{translate("compare_to_another_version")} + span.history-toolbar-selected-version( + ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label" + ) Browsing project version labelled + span.history-toolbar-selected-label "{{ history.selection.label.comment }}" + div.history-toolbar-actions + button.history-toolbar-btn( + ng-click="showAddLabelDialog();" + ng-if="!history.showOnlyLabels" + ng-disabled="history.loadingFileTree" + ) + i.fa.fa-tag + |  #{translate("history_label_this_version")} + button.history-toolbar-btn( + ng-click="toggleHistoryViewMode();" + ng-disabled="history.loadingFileTree" + ) + i.fa.fa-exchange + |  #{translate("compare_to_another_version")} .history-toolbar-entries-list toggle-switch( diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index ddd098a5f5..aa4ba09d62 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -38,6 +38,9 @@ define [ else @reset() @$scope.history.viewMode = HistoryViewModes.COMPARE + @ide.$timeout () => + @$scope.$broadcast "history:toggle" + , 0 @$scope.$watch "history.selection.updates", (updates) => if @$scope.history.viewMode == HistoryViewModes.COMPARE @@ -57,6 +60,7 @@ define [ if showOnlyLabels @selectedLabelFromUpdatesSelection() else + @$scope.history.selection.label = null if @$scope.history.selection.updates.length == 0 @autoSelectLastUpdate() @@ -79,6 +83,7 @@ define [ nextBeforeTimestamp: null atEnd: false selection: { + label: null updates: [] docs: {} pathname: null @@ -166,21 +171,17 @@ define [ selectLabel: (labelToSelect) -> updateToSelect = null - alreadySelected = false + + if @_isLabelSelected labelToSelect + # Label already selected + return + for update in @$scope.history.updates if update.toV == labelToSelect.version updateToSelect = update break - for label in @$scope.history.labels - matchingLabel = (labelToSelect.id == label.id) - if matchingLabel and label.selected - alreadySelected = true - label.selected = matchingLabel - - if alreadySelected - return - + @$scope.history.selection.label = labelToSelect if updateToSelect? @selectUpdate updateToSelect else @@ -241,10 +242,9 @@ define [ pathname = @$scope.history.selection.pathname if @$scope.history.selection.updates?[0]? toV = @$scope.history.selection.updates[0].toV - else - for label in @$scope.history.labels or [] - if label.selected - toV = label.version + else if @$scope.history.selection.label? + toV = @$scope.history.selection.label.version + if !toV? return url = "/project/#{@$scope.project_id}/diff" @@ -316,6 +316,9 @@ define [ }).then (response) => @_deleteLabelLocally label + _isLabelSelected: (label) -> + label.id == @$scope.history.selection.label?.id + _deleteLabelLocally: (labelToDelete) -> for update, i in @$scope.history.updates if update.toV == labelToDelete.version diff --git a/services/web/public/coffee/ide/history/components/historyLabelsList.coffee b/services/web/public/coffee/ide/history/components/historyLabelsList.coffee index 44c9a54742..75486b9081 100644 --- a/services/web/public/coffee/ide/history/components/historyLabelsList.coffee +++ b/services/web/public/coffee/ide/history/components/historyLabelsList.coffee @@ -16,7 +16,7 @@ define [ ctrl.getUserCSSStyle = (user, label) -> curUserId = user?._id or user?.id hue = ColorManager.getHueForUserId(curUserId) or 100 - if label.selected + if label.id == ctrl.selectedLabel?.id color : "#FFF" else color: "hsl(#{ hue }, 70%, 50%)" @@ -28,6 +28,7 @@ define [ users: "<" currentUser: "<" isLoading: "<" + selectedLabel: "<" onLabelSelect: "&" onLabelDelete: "&" controller: historyLabelsListController diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index 7bb10f88f2..cfefbb462c 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -15,22 +15,28 @@ .history-toolbar when (@is-overleaf = false) { border-bottom: @toolbar-border-bottom; } - .history-toolbar-time { - font-weight: bold; + .history-toolbar-selected-version { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } - .history-toolbar-btn { - .btn; - .btn-info; - .btn-xs; - padding-left: @padding-small-horizontal; - padding-right: @padding-small-horizontal; - margin-left: (@line-height-computed / 2); + .history-toolbar-time, + .history-toolbar-selected-label { + font-weight: bold; + } + .history-toolbar-actions { + flex-grow: 1; } - + .history-toolbar-btn { + .btn; + .btn-info; + .btn-xs; + padding-left: @padding-small-horizontal; + padding-right: @padding-small-horizontal; + margin-left: (@line-height-computed / 2); + } .history-toolbar-entries-list { - position: absolute; - right: 0; - width: @changesListWidth; + flex: 0 0 @changesListWidth; padding: 0 10px; border-left: 1px solid @editor-border-color; } From bfb2f636cdf50f0b5cf91019b7c51892af1893aa Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Mon, 13 Aug 2018 14:59:39 +0200 Subject: [PATCH 160/188] enable affiliations UI --- .../web/app/coffee/Features/User/UserPagesController.coffee | 1 - services/web/app/views/user/settings.pug | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 2a7ed62d02..5e6ea7d62b 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -68,7 +68,6 @@ module.exports = shouldAllowEditingDetails: shouldAllowEditingDetails languages: Settings.languages, accountSettingsTabActive: true - showAffiliationsUI: (req.query?.aff == "true") or false sessionsPage: (req, res, next) -> user = AuthenticationController.getSessionUser(req) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index a43e81c12c..e4da83c1db 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -9,7 +9,7 @@ block content .page-header h1 #{translate("account_settings")} .account-settings(ng-controller="AccountSettingsController", ng-cloak) - if locals.showAffiliationsUI && hasFeature('affiliations') + if hasFeature('affiliations') include settings/user-affiliations form-messages(for="settingsForm") @@ -22,7 +22,7 @@ block content h3 #{translate("update_account_info")} form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate) input(type="hidden", name="_csrf", value=csrfToken) - if !(locals.showAffiliationsUI && hasFeature('affiliations')) + if !hasFeature('affiliations') if !externalAuthenticationSystemUsed() .form-group label(for='email') #{translate("email")} From 085c2529d979f7cd7987c6ef276e859065218e1b Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 13 Aug 2018 12:13:44 +0100 Subject: [PATCH 161/188] Inject isOverleaf setting globally --- services/web/app/coffee/infrastructure/ExpressLocals.coffee | 4 ++++ services/web/app/views/layout.pug | 1 + 2 files changed, 5 insertions(+) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 5c71afbe5d..f31d94b8e2 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -333,3 +333,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> defaultLineHeight : if isOl then 'normal' else 'compact' renderAnnouncements : !isOl next() + + webRouter.use (req, res, next) -> + res.locals.exposedSettings = + isOverleaf: Settings.overleaf? diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 9e633667eb..d820f2b4a1 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -70,6 +70,7 @@ html(itemscope, itemtype='http://schema.org/Product') window.systemMessages = !{JSON.stringify(systemMessages).replace(/\//g, '\\/')}; window.ab = {}; window.user_id = '#{getLoggedInUserId()}'; + window.exposedSettings = JSON.parse('!{JSON.stringify(exposedSettings).replace(/\//g, "\\/")}'); - if (typeof(settings.algolia) != "undefined") script. From db16fad6c9d8f56877a643df9f593697651087f4 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 13 Aug 2018 12:32:32 +0100 Subject: [PATCH 162/188] Fix --- services/web/app/coffee/infrastructure/ExpressLocals.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index f31d94b8e2..d4444b3542 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -335,5 +335,6 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> next() webRouter.use (req, res, next) -> - res.locals.exposedSettings = + res.locals.ExposedSettings = isOverleaf: Settings.overleaf? + next() From 274ac8b1ba48be1c19b8826f9366eb23307d5f89 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 13 Aug 2018 12:32:49 +0100 Subject: [PATCH 163/188] Another fix --- services/web/app/views/layout.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index d820f2b4a1..15d3150035 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -70,7 +70,7 @@ html(itemscope, itemtype='http://schema.org/Product') window.systemMessages = !{JSON.stringify(systemMessages).replace(/\//g, '\\/')}; window.ab = {}; window.user_id = '#{getLoggedInUserId()}'; - window.exposedSettings = JSON.parse('!{JSON.stringify(exposedSettings).replace(/\//g, "\\/")}'); + window.ExposedSettings = JSON.parse('!{JSON.stringify(ExposedSettings).replace(/\//g, "\\/")}'); - if (typeof(settings.algolia) != "undefined") script. From 5836c029c82e42eaea87f9cc43de8ba19e5ba03a Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 13 Aug 2018 12:33:31 +0100 Subject: [PATCH 164/188] Show archive heading if archiving and on OL --- services/web/app/views/project/list/modals.pug | 1 + .../coffee/main/project-list/modal-controllers.coffee | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index aaacf660a6..9ac969b518 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -192,6 +192,7 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate') ng-click="cancel()" ) × h3(ng-if="action == 'delete'") #{translate("delete_projects")} + h3(ng-if="action == 'archive'") #{translate("archive_projects")} h3(ng-if="action == 'leave'") #{translate("leave_projects")} h3(ng-if="action == 'delete-and-leave'") #{translate("delete_and_leave_projects")} .modal-body diff --git a/services/web/public/coffee/main/project-list/modal-controllers.coffee b/services/web/public/coffee/main/project-list/modal-controllers.coffee index 609a16cb77..a8e5d5ec8f 100644 --- a/services/web/public/coffee/main/project-list/modal-controllers.coffee +++ b/services/web/public/coffee/main/project-list/modal-controllers.coffee @@ -102,10 +102,15 @@ define [ $scope.projectsToDelete = projects.filter (project) -> project.accessLevel == "owner" $scope.projectsToLeave = projects.filter (project) -> project.accessLevel != "owner" + if $scope.projectsToLeave.length > 0 and $scope.projectsToDelete.length > 0 $scope.action = "delete-and-leave" else if $scope.projectsToLeave.length == 0 and $scope.projectsToDelete.length > 0 - $scope.action = "delete" + projectsToArchive = $scope.projectsToDelete.filter (project) -> !project.archived + if projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf + $scope.action = "archive" + else + $scope.action = "delete" else $scope.action = "leave" From 009c5c363b1b5cca57cf22b5f054384115fef425 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 13 Aug 2018 13:52:21 +0100 Subject: [PATCH 165/188] Also fix modal body for archiving projects --- services/web/app/views/project/list/modals.pug | 3 ++- .../coffee/main/project-list/modal-controllers.coffee | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 9ac969b518..3fd8b5b1f6 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -197,7 +197,8 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate') h3(ng-if="action == 'delete-and-leave'") #{translate("delete_and_leave_projects")} .modal-body div(ng-show="projectsToDelete.length > 0") - p #{translate("about_to_delete_projects")} + p(ng-if="action == 'delete'") #{translate("about_to_delete_projects")} + p(ng-if="action == 'archive'") #{translate("about_to_archive_projects")} ul li(ng-repeat="project in projectsToDelete | orderBy:'name'") strong {{project.name}} diff --git a/services/web/public/coffee/main/project-list/modal-controllers.coffee b/services/web/public/coffee/main/project-list/modal-controllers.coffee index a8e5d5ec8f..bec9a8efb4 100644 --- a/services/web/public/coffee/main/project-list/modal-controllers.coffee +++ b/services/web/public/coffee/main/project-list/modal-controllers.coffee @@ -101,13 +101,13 @@ define [ App.controller 'DeleteProjectsModalController', ($scope, $modalInstance, $timeout, projects) -> $scope.projectsToDelete = projects.filter (project) -> project.accessLevel == "owner" $scope.projectsToLeave = projects.filter (project) -> project.accessLevel != "owner" - + $scope.projectsToArchive = projects.filter (project) -> + project.accessLevel == "owner" and !project.archived if $scope.projectsToLeave.length > 0 and $scope.projectsToDelete.length > 0 $scope.action = "delete-and-leave" else if $scope.projectsToLeave.length == 0 and $scope.projectsToDelete.length > 0 - projectsToArchive = $scope.projectsToDelete.filter (project) -> !project.archived - if projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf + if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf $scope.action = "archive" else $scope.action = "delete" From bd94e55ce912b773e483ec4ef8fcc2286df61e99 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 13 Aug 2018 13:59:35 +0100 Subject: [PATCH 166/188] Handle archive and leave action --- services/web/app/views/project/list/modals.pug | 5 +++-- .../public/coffee/main/project-list/modal-controllers.coffee | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 3fd8b5b1f6..d3d8e25f23 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -195,10 +195,11 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate') h3(ng-if="action == 'archive'") #{translate("archive_projects")} h3(ng-if="action == 'leave'") #{translate("leave_projects")} h3(ng-if="action == 'delete-and-leave'") #{translate("delete_and_leave_projects")} + h3(ng-if="action == 'archive-and-leave'") #{translate("archive_and_leave_projects")} .modal-body div(ng-show="projectsToDelete.length > 0") - p(ng-if="action == 'delete'") #{translate("about_to_delete_projects")} - p(ng-if="action == 'archive'") #{translate("about_to_archive_projects")} + p(ng-if="action == 'delete' || action == 'delete-and-leave'") #{translate("about_to_delete_projects")} + p(ng-if="action == 'archive' || action == 'archive-and-leave'") #{translate("about_to_archive_projects")} ul li(ng-repeat="project in projectsToDelete | orderBy:'name'") strong {{project.name}} diff --git a/services/web/public/coffee/main/project-list/modal-controllers.coffee b/services/web/public/coffee/main/project-list/modal-controllers.coffee index bec9a8efb4..42a11500cf 100644 --- a/services/web/public/coffee/main/project-list/modal-controllers.coffee +++ b/services/web/public/coffee/main/project-list/modal-controllers.coffee @@ -105,7 +105,10 @@ define [ project.accessLevel == "owner" and !project.archived if $scope.projectsToLeave.length > 0 and $scope.projectsToDelete.length > 0 - $scope.action = "delete-and-leave" + if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf + $scope.action = "archive-and-leave" + else + $scope.action = "delete-and-leave" else if $scope.projectsToLeave.length == 0 and $scope.projectsToDelete.length > 0 if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf $scope.action = "archive" From 4a65a526cec540290941165f1762dda51f196a57 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Mon, 13 Aug 2018 17:03:48 +0200 Subject: [PATCH 167/188] require minilist package --- services/web/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/package.json b/services/web/package.json index ed0eb06455..ae0a51a3c4 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -98,7 +98,8 @@ "v8-profiler": "^5.2.3", "valid-url": "^1.0.9", "xml2js": "0.2.0", - "yauzl": "^2.8.0" + "yauzl": "^2.8.0", + "minimist": "1.2.0" }, "devDependencies": { "autoprefixer": "^6.6.1", From 663ca275cb02760895ae0fa0aaca4b6fb316cef6 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 13 Aug 2018 17:09:18 +0100 Subject: [PATCH 168/188] Add i18n. --- .../app/views/project/editor/history/previewPanelV2.pug | 4 ++-- .../web/app/views/project/editor/history/toolbarV2.pug | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/editor/history/previewPanelV2.pug b/services/web/app/views/project/editor/history/previewPanelV2.pug index 3d7a1ac3df..773d79878f 100644 --- a/services/web/app/views/project/editor/history/previewPanelV2.pug +++ b/services/web/app/views/project/editor/history/previewPanelV2.pug @@ -45,7 +45,7 @@ text="history.diff.text", highlights="history.diff.highlights", read-only="true", - resize-on="layout:main:resize", + resize-on="layout:main:resize,history:toggle", navigate-highlights="true" ) .alert.alert-info(ng-if="history.diff.binary") @@ -70,7 +70,7 @@ font-size="settings.fontSize", text="history.selectedFile.text", read-only="true", - resize-on="layout:main:resize", + resize-on="layout:main:resize,history:toggle", ) .alert.alert-info(ng-if="history.selectedFile.binary") | We're still working on showing image and binary changes, sorry. Stay tuned! diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index 4e4b6f7ae3..aa459e0b32 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -11,7 +11,7 @@ time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }} span.history-toolbar-selected-version( ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label" - ) Browsing project version labelled + ) #{translate("browsing_project_labelled")}  span.history-toolbar-selected-label "{{ history.selection.label.comment }}" div.history-toolbar-actions button.history-toolbar-btn( @@ -31,9 +31,9 @@ .history-toolbar-entries-list toggle-switch( ng-model="history.showOnlyLabels" - label-true="Labels" - label-false="All history" - description="Show all of the project history or only labelled versions." + label-true=translate("history_view_labels") + label-false=translate("history_view_all") + description=translate("history_view_a11y_description") ) script(type="text/ng-template", id="historyV2AddLabelModalTemplate") From 6b31268bfa65096b82dd17bd0b28f9c6f451a4dd Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 14 Aug 2018 15:27:07 +0100 Subject: [PATCH 169/188] Scroll to selected history update when coming back from the labels view. --- .../history/components/historyEntriesList.coffee | 16 ++++++++++++++++ .../ide/history/components/historyEntry.coffee | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee index 934ca304fe..de4e4f1b92 100644 --- a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee @@ -3,6 +3,22 @@ define [ ], (App) -> historyEntriesListController = ($scope, $element, $attrs) -> ctrl = @ + ctrl.$entryListViewportEl = null + _isEntryElVisible = ($entryEl) -> + entryElTop = $entryEl.offset().top + entryElBottom = entryElTop + $entryEl.outerHeight() + entryListViewportElTop = ctrl.$entryListViewportEl.offset().top + entryListViewportElBottom = entryListViewportElTop + ctrl.$entryListViewportEl.height() + return entryElTop >= entryListViewportElTop and entryElBottom <= entryListViewportElBottom; + _getScrollTopPosForEntry = ($entryEl) -> + halfViewportElHeight = ctrl.$entryListViewportEl.height() / 2 + return $entryEl.offset().top - halfViewportElHeight + ctrl.onEntryLinked = (entry, $entryEl) -> + if entry.selectedTo and entry.selectedFrom and !_isEntryElVisible $entryEl + $scope.$applyAsync () -> + ctrl.$entryListViewportEl.scrollTop _getScrollTopPosForEntry $entryEl + ctrl.$onInit = () -> + ctrl.$entryListViewportEl = $element.find "> .history-entries" return App.component "historyEntriesList", { diff --git a/services/web/public/coffee/ide/history/components/historyEntry.coffee b/services/web/public/coffee/ide/history/components/historyEntry.coffee index 8af726b509..6d5eed53d5 100644 --- a/services/web/public/coffee/ide/history/components/historyEntry.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntry.coffee @@ -26,6 +26,8 @@ define [ color : "#FFF" else color: "hsl(#{ hue }, 70%, 50%)" + ctrl.$onInit = () -> + ctrl.historyEntriesList.onEntryLinked ctrl.entry, $element.find "> .history-entry" return App.component "historyEntry", { @@ -35,6 +37,8 @@ define [ users: "<" onSelect: "&" onLabelDelete: "&" + require: + historyEntriesList: '^historyEntriesList' controller: historyEntryController templateUrl: "historyEntryTpl" } \ No newline at end of file From 2c9977d26ed642453246da96e8ada574e30189ab Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Thu, 9 Aug 2018 11:02:48 -0500 Subject: [PATCH 170/188] Add analytics to link mixins --- services/web/app/views/_mixins_links.pug | 63 +++++++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/_mixins_links.pug b/services/web/app/views/_mixins_links.pug index 18b208270c..bad56ffc25 100644 --- a/services/web/app/views/_mixins_links.pug +++ b/services/web/app/views/_mixins_links.pug @@ -1,9 +1,19 @@ -mixin linkAdvisors(linkText, linkClass, tracked) +mixin linkAdvisors(linkText, linkClass, track) //- To Do: verify path - //- To Do: track + - var gaCategory = track && track.category ? track.category : 'All' + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null a(href="/advisors" class=linkClass ? linkClass : '' - eventTrackingGa=track ? 'advisors' : null + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation ) | #{linkText ? linkText : 'advisor programme'} @@ -32,8 +42,23 @@ mixin linkEmail(linkText, linkClass, email) a(href="mailto:#{email ? email : 'contact'}@#{emailDomain}" class=linkClass ? linkClass : '') | #{linkText ? linkText : 'email'} -mixin linkInvite(linkText, linkClass) - a(href="/user/bonus" class=linkClass ? linkClass : '') +mixin linkInvite(linkText, linkClass, track) + - var gaCategory = track && track.category ? track.category : 'All' + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null + + a(href="/user/bonus" + class=linkClass ? linkClass : '' + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation + ) | #{linkText ? linkText : 'invite your friends'} mixin linkPlansAndPricing(linkText, linkClass) @@ -42,10 +67,21 @@ mixin linkPlansAndPricing(linkText, linkClass) | #{linkText ? linkText : 'plans and pricing'} mixin linkPrintNewTab(linkText, linkClass, icon, track) - //- To Do: track + - var gaCategory = track && track.category ? track.category : null + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null + a(href='?media=print' class=linkClass ? linkClass : '' - eventTrackingGa=track ? 'print' : null + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation target="_BLANK" ) if icon @@ -63,8 +99,19 @@ mixin linkSignUp(linkText, linkClass) mixin linkTweet(linkText, linkClass, tweetText, track) //- twitter-share-button is required by twitter - //- To Do: track + - var gaCategory = track && track.category ? track.category : 'All' + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null a(class="twitter-share-button " + linkClass + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation href="https://twitter.com/intent/tweet?text=" + tweetText target="_BLANK" ) #{linkText ? linkText : 'tweet'} From fc0b9df14ccc30c7fb7ce243fbb1e63bb101d4c2 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 7 Aug 2018 14:54:23 +0100 Subject: [PATCH 171/188] Add feature flag for redirecting SL to v2 --- services/web/app/coffee/infrastructure/Features.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index ffc60604e6..496bbf1e39 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -25,5 +25,7 @@ module.exports = Features = when 'rich-text' isEnabled = true # Switch to false to disable Settings.overleaf? and isEnabled + when 'redirect-sl' + return Settings.createV1AccountOnLogin else throw new Error("unknown feature: #{feature}") From 985344c0d74edfb3c138525213d336e670048236 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 8 Aug 2018 15:24:20 +0100 Subject: [PATCH 172/188] Use redirect setting instead of re-using setting that coincidentally would be flipped on the same day --- services/web/app/coffee/infrastructure/Features.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index 496bbf1e39..43dadcda2d 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -26,6 +26,6 @@ module.exports = Features = isEnabled = true # Switch to false to disable Settings.overleaf? and isEnabled when 'redirect-sl' - return Settings.createV1AccountOnLogin + return Settings.redirectToV2? else throw new Error("unknown feature: #{feature}") From af6e7758d38b6edff684674075bbad519446d39b Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 15 Aug 2018 15:26:22 +0100 Subject: [PATCH 173/188] configure backend group name via config files --- services/web/app/coffee/Features/Compile/ClsiManager.coffee | 4 ++-- .../web/app/coffee/Features/Compile/CompileController.coffee | 2 +- services/web/app/coffee/router.coffee | 2 +- services/web/config/settings.defaults.coffee | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 8d5c5b2baa..b0f7a01fa1 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -7,8 +7,8 @@ ProjectGetter = require("../Project/ProjectGetter") ProjectEntityHandler = require("../Project/ProjectEntityHandler") logger = require "logger-sharelatex" Url = require("url") -ClsiCookieManager = require("./ClsiCookieManager")() -NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")("newBackendcloud") +ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName) +NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi_new?.backendGroupName) ClsiStateManager = require("./ClsiStateManager") _ = require("underscore") async = require("async") diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index d79eb5d389..8a3f92ac66 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -9,7 +9,7 @@ Settings = require "settings-sharelatex" AuthenticationController = require "../Authentication/AuthenticationController" UserGetter = require "../User/UserGetter" RateLimiter = require("../../infrastructure/RateLimiter") -ClsiCookieManager = require("./ClsiCookieManager")() +ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName) Path = require("path") module.exports = CompileController = diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index f48882a634..b590e58f5d 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -22,7 +22,7 @@ UserPagesController = require('./Features/User/UserPagesController') DocumentController = require('./Features/Documents/DocumentController') CompileManager = require("./Features/Compile/CompileManager") CompileController = require("./Features/Compile/CompileController") -ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")() +ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")(Settings.apis.clsi?.backendGroupName) HealthCheckController = require("./Features/HealthCheck/HealthCheckController") ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController" FileStoreController = require("./Features/FileStore/FileStoreController") diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 1306ce91c2..802183357c 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -135,6 +135,7 @@ module.exports = settings = url: "http://#{process.env['FILESTORE_HOST'] or 'localhost'}:3009" clsi: url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013" + backendGroupName: undefined templates: url: "http://#{process.env['TEMPLATES_HOST'] or 'localhost'}:3007" githubSync: From 23bb866988ac026c2c61595b8dfcffdcfe534b3c Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 16 Aug 2018 11:26:34 +0100 Subject: [PATCH 174/188] Show an error message when history fails to load updates. --- .../views/project/editor/history/entriesListV2.pug | 4 ++-- .../views/project/editor/history/previewPanelV2.pug | 11 +++++++++++ .../app/views/project/editor/history/toolbarV2.pug | 12 ++++++++---- .../coffee/ide/history/HistoryV2Manager.coffee | 6 ++++++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index 21c2438742..5e4c4b66c6 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -3,7 +3,7 @@ aside.change-list( ng-controller="HistoryV2ListController" ) history-entries-list( - ng-if="!history.showOnlyLabels" + ng-if="!history.showOnlyLabels && !history.error" entries="history.updates" current-user="user" users="projectUsers" @@ -15,7 +15,7 @@ aside.change-list( on-label-delete="handleLabelDelete(label)" ) history-labels-list( - ng-if="history.showOnlyLabels" + ng-if="history.showOnlyLabels && !history.error" labels="history.labels" current-user="user" users="projectUsers" diff --git a/services/web/app/views/project/editor/history/previewPanelV2.pug b/services/web/app/views/project/editor/history/previewPanelV2.pug index 773d79878f..bd814c2da4 100644 --- a/services/web/app/views/project/editor/history/previewPanelV2.pug +++ b/services/web/app/views/project/editor/history/previewPanelV2.pug @@ -77,5 +77,16 @@ .loading-panel(ng-show="history.selectedFile.loading") i.fa.fa-spin.fa-refresh |   #{translate("loading")}... + .error-panel(ng-show="history.error") + .alert.alert-danger + | #{translate("generic_history_error")} + a( + ng-href="mailto:#{settings.adminEmail}?Subject=Error%20loading%20history%20for%project%20{{ project_id }}" + ) #{settings.adminEmail} + .clearfix + a.alert-link-as-btn.pull-right( + href + ng-click="toggleHistory()" + ) #{translate("back_to_editor")} .error-panel(ng-show="history.selectedFile.error") .alert.alert-danger #{translate("generic_something_went_wrong")} diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index aa459e0b32..a683facba4 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -6,14 +6,16 @@ i.fa.fa-spin.fa-refresh |    #{translate("loading")}... span.history-toolbar-selected-version( - ng-show="!history.loadingFileTree && !history.showOnlyLabels" + ng-show="!history.loadingFileTree && !history.showOnlyLabels && !history.error" ) #{translate("browsing_project_as_of")}  time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }} span.history-toolbar-selected-version( - ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label" + ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label && !history.error" ) #{translate("browsing_project_labelled")}  span.history-toolbar-selected-label "{{ history.selection.label.comment }}" - div.history-toolbar-actions + div.history-toolbar-actions( + ng-if="!history.error" + ) button.history-toolbar-btn( ng-click="showAddLabelDialog();" ng-if="!history.showOnlyLabels" @@ -28,7 +30,9 @@ i.fa.fa-exchange |  #{translate("compare_to_another_version")} - .history-toolbar-entries-list + .history-toolbar-entries-list( + ng-if="!history.error" + ) toggle-switch( ng-model="history.showOnlyLabels" label-true=translate("history_view_labels") diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index aa4ba09d62..3f231c046b 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -92,6 +92,7 @@ define [ toV: null } } + error: null showOnlyLabels: false labels: null files: [] @@ -234,6 +235,11 @@ define [ if !updatesData.nextBeforeTimestamp? @$scope.history.atEnd = true @$scope.history.loading = false + .catch (error) => + { status, statusText } = error + @$scope.history.error = { status, statusText } + @$scope.history.loading = false + @$scope.history.loadingFileTree = false _sortLabelsByVersionAndDate: (labels) -> @ide.$filter("orderBy")(labels, [ '-version', '-created_at' ]) From 652be9efe693622851ca6adad47ec41c20f03c60 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 16 Aug 2018 11:41:19 +0100 Subject: [PATCH 175/188] Slightly adapt the markup for the generic history error. --- .../views/project/editor/history/previewPanelV2.pug | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/services/web/app/views/project/editor/history/previewPanelV2.pug b/services/web/app/views/project/editor/history/previewPanelV2.pug index bd814c2da4..e75af8db90 100644 --- a/services/web/app/views/project/editor/history/previewPanelV2.pug +++ b/services/web/app/views/project/editor/history/previewPanelV2.pug @@ -79,11 +79,12 @@ |   #{translate("loading")}... .error-panel(ng-show="history.error") .alert.alert-danger - | #{translate("generic_history_error")} - a( - ng-href="mailto:#{settings.adminEmail}?Subject=Error%20loading%20history%20for%project%20{{ project_id }}" - ) #{settings.adminEmail} - .clearfix + p + | #{translate("generic_history_error")} + a( + ng-href="mailto:#{settings.adminEmail}?Subject=Error%20loading%20history%20for%project%20{{ project_id }}" + ) #{settings.adminEmail} + p.clearfix a.alert-link-as-btn.pull-right( href ng-click="toggleHistory()" From 713a39171ae4334ec6128bb620cccc752f509edb Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 16 Aug 2018 13:44:48 +0100 Subject: [PATCH 176/188] Fix history frontend tests. --- .../coffee/ide/history/HistoryV2ManagerTests.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee b/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee index 725befb721..2fc5c5b3b6 100644 --- a/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee +++ b/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee @@ -15,14 +15,17 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) -> nextBeforeTimestamp: null atEnd: false selection: { + label: null updates: [] - pathname: null docs: {} + pathname: null range: { fromV: null toV: null } } + error: null + showOnlyLabels: false labels: null diff: null files: [] From 3b31125a62b4095016a574dd8dfade8da60a3a4b Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 16 Aug 2018 14:11:43 +0100 Subject: [PATCH 177/188] Add message about deleting account affecting v2 Only show on SL and if the user has linked accounts --- services/web/app/views/user/settings.pug | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index e4da83c1db..d231047d8d 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -167,7 +167,12 @@ block content ) i.fa.fa-check | #{translate("unsubscribed")} - + + if !settings.overleaf && user.overleaf + p + | Please note: If you have linked your account with Overleaf + | v2, then deleting your ShareLaTeX account will also delete + | account and all of it's associated projects and data. p #{translate("need_to_leave")} a(href, ng-click="deleteAccount()") #{translate("delete_your_account")} From c33192b852f24579dc60cd4fac27f4af2300aad2 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 16 Jul 2018 16:18:08 +0100 Subject: [PATCH 178/188] download log files using buildPdfDownloadUrl --- .../ide/pdf/controllers/PdfController.coffee | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 389631c0a7..c717ef6b82 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -224,6 +224,12 @@ define [ _csrf: window.csrfToken }, {params: params} + buildPdfDownloadUrl = (pdfDownloadDomain, path)-> + if pdfDownloadDomain? + return "#{pdfDownloadDomain}#{path}" + else + return path + parseCompileResponse = (response) -> # keep last url @@ -244,11 +250,7 @@ define [ $scope.pdf.compileInProgress = false $scope.pdf.autoCompileDisabled = false - buildPdfDownloadUrl = (path)-> - if pdfDownloadDomain? - return "#{pdfDownloadDomain}#{path}" - else - return path + # make a cache to look up files by name fileByPath = {} if response?.outputFiles? @@ -267,24 +269,24 @@ define [ if response.status == "timedout" $scope.pdf.view = 'errors' $scope.pdf.timedout = true - fetchLogs(fileByPath) + fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain}) else if response.status == "terminated" $scope.pdf.view = 'errors' $scope.pdf.compileTerminated = true - fetchLogs(fileByPath) + fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain}) else if response.status in ["validation-fail", "validation-pass"] $scope.pdf.view = 'pdf' - $scope.pdf.url = buildPdfDownloadUrl last_pdf_url + $scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, last_pdf_url $scope.shouldShowLogs = true $scope.pdf.failedCheck = true if response.status is "validation-fail" event_tracking.sendMB "syntax-check-#{response.status}" - fetchLogs(fileByPath, { validation: true }) + fetchLogs(fileByPath, { validation: true, pdfDownloadDomain:pdfDownloadDomain}) else if response.status == "exited" $scope.pdf.view = 'pdf' $scope.pdf.compileExited = true - $scope.pdf.url = buildPdfDownloadUrl last_pdf_url + $scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, last_pdf_url $scope.shouldShowLogs = true - fetchLogs(fileByPath) + fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain}) else if response.status == "autocompile-backoff" if $scope.pdf.isAutoCompileOnLoad # initial autocompile $scope.pdf.view = 'uncompiled' @@ -300,7 +302,7 @@ define [ $scope.pdf.view = 'errors' $scope.pdf.failure = true $scope.shouldShowLogs = true - fetchLogs(fileByPath) + fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain}) else if response.status == 'clsi-maintenance' $scope.pdf.view = 'errors' $scope.pdf.clsiMaintenance = true @@ -320,12 +322,12 @@ define [ # define the base url. if the pdf file has a build number, pass it to the clsi in the url if fileByPath['output.pdf']?.url? - $scope.pdf.url = buildPdfDownloadUrl fileByPath['output.pdf'].url + $scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, fileByPath['output.pdf'].url else if fileByPath['output.pdf']?.build? build = fileByPath['output.pdf'].build - $scope.pdf.url = buildPdfDownloadUrl "/project/#{$scope.project_id}/build/#{build}/output/output.pdf" + $scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, "/project/#{$scope.project_id}/build/#{build}/output/output.pdf" else - $scope.pdf.url = buildPdfDownloadUrl "/project/#{$scope.project_id}/output/output.pdf" + $scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, "/project/#{$scope.project_id}/output/output.pdf" # check if we need to bust cache (build id is unique so don't need it in that case) if not fileByPath['output.pdf']?.build? qs.cache_bust = "#{Date.now()}" @@ -335,7 +337,7 @@ define [ qs.popupDownload = true $scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs) - fetchLogs(fileByPath) + fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain}) IGNORE_FILES = ["output.fls", "output.fdb_latexmk"] $scope.pdf.outputFiles = [] @@ -376,11 +378,11 @@ define [ compileGroup:ide.compileGroup clsiserverid:ide.clsiServerId if file?.url? # FIXME clean this up when we have file.urls out consistently - opts.url = file.url + opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, file.url else if file?.build? - opts.url = "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}" + opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}" else - opts.url = "/project/#{$scope.project_id}/output/#{name}" + opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, "/project/#{$scope.project_id}/output/#{name}" # check if we need to bust cache (build id is unique so don't need it in that case) if not file?.build? opts.params.cache_bust = "#{Date.now()}" From dfc25a093b3959e3494908b49675b3bce6aeccfc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 16 Aug 2018 16:28:02 +0100 Subject: [PATCH 179/188] Fix review icon for Overleaf. --- .../web/public/img/review-icon-sprite-ol.png | Bin 0 -> 765 bytes .../public/img/review-icon-sprite-ol@2x.png | Bin 0 -> 1082 bytes .../stylesheets/app/editor/review-panel.less | 17 ++++++++++++----- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 services/web/public/img/review-icon-sprite-ol.png create mode 100644 services/web/public/img/review-icon-sprite-ol@2x.png diff --git a/services/web/public/img/review-icon-sprite-ol.png b/services/web/public/img/review-icon-sprite-ol.png new file mode 100644 index 0000000000000000000000000000000000000000..1e5c7192e4d649f6e1cff67cbc31dfffaca2c6ea GIT binary patch literal 765 zcmVeV^tcYf!5pY!|oebOAq2~0(q7MP-v3jE(nx{o%~QG!Lt zLkPooi&yBy`+pg@5IeCA%M-M-pdGl3n;7>KI4#C<%EDpPW47wL@mybMfijVgVw56R z?H=GLI+I4Wz%Dts>sstRmEw(6E(Jqi=`l>67d#3(8!@Re1r?yXQ=qps~eYvVl|oz)9b zgf6uEToKtg?Yf??<2Q`q6Fw)~Kyq~aVDrsqV4L|%ab4FdJogJwgR9t&V60hPf^qa} zk|pcn#TY>w!s^;q-D}XhtPCQebDOxC-Z4%`rrFbyzYgu1B2ME zy3Odo0>Q vFWH%0}WF zc!BXTe@Q0s)|&?(o1e)f-+XUo-n`kREX#t!WJiKSW}dJo>} z!7U{BruaKYEF@u0DnjsMX8GJ6HozD$ZG!Mp$yk~z*a7qeHLDs|d;}QOsy-)N0Bt~= z)o(GrD{ze>3ikm!7UezyKH>2@?nT1A3;4SRShuKa3$WX?!XaRyQDY8x2-pN1E7002 z_{=t7rm25|-{3@H8?ve5LtX){1C||q46p08;$5jL(q+hg#*YhX!ZmzHEu@Q*ec(`0 z9hUK3DY|=(4}kM^5S}Q=1`T6PaxHRAxYsaQMj*T2M&$R;xg?x6NOnikPidKM z+t|F6=d}!kzci?>NZ7P;8ZA-+pikWPatT-E!o+|CF|_W}h7o$DhAAkMjJl#Fno&AI zWSS7PU1VCKmAMWfY5h#n(5Z&7t+j2Zr7=0b=+Koz;;vhm^$19xwH>MXvT*~@73#?? zv32RTq_s93sXHs6BGcSU6%iR-Vn#L!&m}4k3e1fvqp6^-!LxJ&*M>yV{5pN7)hNA1 z&d&m#RwNt~a$`wD(*Q(fV@D2sR@0EEy?N zA~4i>i7uxlo7;FGvJH4CDSr|hhAMThghzbC_wVC3tP7Ff)G#n!#p7RqD}cAQIW4Bi zWj&FUzmy$3dsH-Q$qx8SqqIIhI6m)_Li^ zBnq1wo-wU+DPl$NFeDjFT^*?;iu((;TP?fTx{08|DZmN9cS(7gzwQbiBa3+ls7R0W z$GN1BhKhb15$_qf*mTB2!k(}v>wrg*!~g&Q07*qoM6N<$g7n$+ A0ssI2 literal 0 HcmV?d00001 diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 800a25618f..a7e7d3fdbd 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -922,7 +922,7 @@ } } -.review-icon { +.review-icon when (@is-overleaf = false) { display: inline-block; background: url('/img/review-icon-sprite.png') top/30px no-repeat; width: 30px; @@ -945,10 +945,17 @@ } } -.review-icon when (@is-overleaf) { - background-position-y: -60px; - .toolbar .btn-full-height:hover & { - background-position-y: -60px; +.review-icon when (@is-overleaf = true) { + display: inline-block; + background: url('/img/review-icon-sprite-ol.png') top/30px no-repeat; + width: 30px; + + &::before { + content: '\00a0'; // Non-breakable space. A non-breakable character here makes this icon work like font-awesome. + } + + @media (min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + background-image: url('/img/review-icon-sprite-ol@2x.png'); } } From 9bc3fa2df0538a143f320d176a8af354317fe1d0 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 17 Aug 2018 12:04:05 +0100 Subject: [PATCH 180/188] Pass req to preDoPassportLogin module hook --- .../Features/Authentication/AuthenticationController.coffee | 2 +- .../Authentication/AuthenticationControllerTests.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 951506891f..d7e9ed4bc6 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -84,7 +84,7 @@ module.exports = AuthenticationController = doPassportLogin: (req, username, password, done) -> email = username.toLowerCase() Modules = require "../../infrastructure/Modules" - Modules.hooks.fire 'preDoPassportLogin', email, (err, infoList) -> + Modules.hooks.fire 'preDoPassportLogin', req, email, (err, infoList) -> return next(err) if err? info = infoList.find((i) => i?) if info? diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index ae8fedc9c8..24af9971d2 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -214,7 +214,7 @@ describe "AuthenticationController", -> beforeEach -> @AuthenticationController._recordFailedLogin = sinon.stub() @AuthenticationController._recordSuccessfulLogin = sinon.stub() - @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, []) + @Modules.hooks.fire = sinon.stub().callsArgWith(3, null, []) # @AuthenticationController.establishUserSession = sinon.stub().callsArg(2) @req.body = email: @email @@ -225,7 +225,7 @@ describe "AuthenticationController", -> describe "when the preDoPassportLogin hooks produce an info object", -> beforeEach -> - @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, [null, {redir: '/somewhere'}, null]) + @Modules.hooks.fire = sinon.stub().callsArgWith(3, null, [null, {redir: '/somewhere'}, null]) it "should stop early and call done with this info object", (done) -> @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb) From 356e31caecd1173f1eba502d12f9007a0ef525d9 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 15 Aug 2018 13:32:26 -0500 Subject: [PATCH 181/188] Set variant when sixpack request successful --- services/web/public/coffee/main/plans.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/main/plans.coffee b/services/web/public/coffee/main/plans.coffee index 1637d79374..258f84b62c 100644 --- a/services/web/public/coffee/main/plans.coffee +++ b/services/web/public/coffee/main/plans.coffee @@ -152,8 +152,9 @@ define [ if $scope.shouldABTestPlans sixpack.participate 'plans-details', ['default', 'more-details'], (chosenVariation, rawResponse)-> - $scope.plansVariant = chosenVariation - event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation + if rawResponse?.status != 'failed' + $scope.plansVariant = chosenVariation + event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation $scope.showPlans = true From 5b5ece37a5fbb41be076373465f05d0f1a652c63 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 15 Aug 2018 14:11:58 -0500 Subject: [PATCH 182/188] Handle sixpack timeouts and include variant in metabase segmentation Render default when sixpack timesout Only convert when variant exists Use Angular cookies module to add variant cookie, which is used on the new subscription page. --- services/web/app/views/subscriptions/plans.pug | 2 +- .../public/coffee/main/new-subscription.coffee | 17 ++++++++--------- services/web/public/coffee/main/plans.coffee | 11 ++++++++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index 0a37b90dd8..51efcd5125 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -14,5 +14,5 @@ block content .content.plans(ng-controller="PlansController") .container(class="more-details" ng-cloak ng-if="plansVariant === 'more-details'") include _plans_page_details_more - .container(ng-cloak ng-if="plansVariant === 'default' || !shouldABTestPlans") + .container(ng-cloak ng-if="plansVariant === 'default' || !shouldABTestPlans || timeout") include _plans_page_details_less diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee index 7851171524..e8294a5885 100644 --- a/services/web/public/coffee/main/new-subscription.coffee +++ b/services/web/public/coffee/main/new-subscription.coffee @@ -4,20 +4,21 @@ define [ "libs/recurly-4.8.5" ], (App)-> - App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)-> + App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils, ipCookie)-> throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined" $scope.currencyCode = MultiCurrencyPricing.currencyCode $scope.plans = MultiCurrencyPricing.plans $scope.planCode = window.plan_code + $scope.plansVariant = ipCookie('plansVariant') $scope.switchToStudent = ()-> currentPlanCode = window.plan_code planCode = currentPlanCode.replace('collaborator', 'student') - event_tracking.sendMB 'subscription-form-switch-to-student', { plan: window.plan_code } + event_tracking.sendMB 'subscription-form-switch-to-student', { plan: window.plan_code, variant: $scope.plansVariant } window.location = "/user/subscription/new?planCode=#{planCode}¤cy=#{$scope.currencyCode}&cc=#{$scope.data.coupon}" - event_tracking.sendMB "subscription-form", { plan : window.plan_code } + event_tracking.sendMB "subscription-form", { plan : window.plan_code, variant: $scope.plansVariant } $scope.paymentMethod = value: "credit_card" @@ -143,13 +144,14 @@ define [ currencyCode : postData.subscriptionDetails.currencyCode, plan_code : postData.subscriptionDetails.plan_code, coupon_code : postData.subscriptionDetails.coupon_code, - isPaypal : postData.subscriptionDetails.isPaypal + isPaypal : postData.subscriptionDetails.isPaypal, + variant : $scope.plansVariant } $http.post("/user/subscription/create", postData) .then ()-> - event_tracking.sendMB "subscription-submission-success" + event_tracking.sendMB "subscription-submission-success", { variant: $scope.plansVariant } window.location.href = "/user/subscription/thank-you" .catch ()-> $scope.processing = false @@ -234,7 +236,4 @@ define [ {code:'VU',name:'Vanuatu'},{code:'VA',name:'Vatican City'},{code:'VE',name:'Venezuela'},{code:'VN',name:'Vietnam'}, {code:'WK',name:'Wake Island'},{code:'WF',name:'Wallis and Futuna'},{code:'EH',name:'Western Sahara'},{code:'YE',name:'Yemen'}, {code:'ZM',name:'Zambia'},{code:'AX',name:'Åland Islandscode:'} - ] - - sixpack.participate 'plans', ['default', 'more-details'], (chosenVariation, rawResponse)-> - $scope.plansVariant = chosenVariation \ No newline at end of file + ] \ No newline at end of file diff --git a/services/web/public/coffee/main/plans.coffee b/services/web/public/coffee/main/plans.coffee index 258f84b62c..f13dff689f 100644 --- a/services/web/public/coffee/main/plans.coffee +++ b/services/web/public/coffee/main/plans.coffee @@ -145,7 +145,7 @@ define [ } - App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack, $filter) -> + App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack, $filter, ipCookie) -> $scope.showPlans = false $scope.shouldABTestPlans = window.shouldABTestPlans @@ -154,7 +154,12 @@ define [ sixpack.participate 'plans-details', ['default', 'more-details'], (chosenVariation, rawResponse)-> if rawResponse?.status != 'failed' $scope.plansVariant = chosenVariation + expiration = new Date(); + expiration.setDate(expiration.getDate() + 5); + ipCookie('plansVariant', chosenVariation, {expires: expiration}) event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation + else + $scope.timeout = true $scope.showPlans = true @@ -185,9 +190,9 @@ define [ if $scope.ui.view == "annual" plan = "#{plan}_annual" plan = eventLabel(plan, location) - event_tracking.sendMB 'plans-page-start-trial', {plan} + event_tracking.sendMB 'plans-page-start-trial', {plan, variant: $scope.plansVariant} event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan - if $scope.shouldABTestPlans + if $scope.plansVariant sixpack.convert 'plans-details' $scope.switchToMonthly = (e, location) -> From f5513f36ca2222fb4f360cbbea420949c294e878 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Fri, 17 Aug 2018 15:08:59 +0200 Subject: [PATCH 183/188] add param to skip affiliation creation on user create --- .../app/coffee/Features/User/UserCreator.coffee | 16 ++++++++++------ .../unit/coffee/User/UserCreatorTests.coffee | 8 ++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserCreator.coffee b/services/web/app/coffee/Features/User/UserCreator.coffee index 2dfa7f5ac0..17248b38ae 100644 --- a/services/web/app/coffee/Features/User/UserCreator.coffee +++ b/services/web/app/coffee/Features/User/UserCreator.coffee @@ -6,15 +6,18 @@ metrics = require('metrics-sharelatex') module.exports = UserCreator = - createNewUser: (opts, callback)-> - logger.log opts:opts, "creating new user" + createNewUser: (attributes, options, callback = (error, user) ->)-> + if arguments.length == 2 + callback = options + options = {} + logger.log user: attributes, "creating new user" user = new User() - username = opts.email.match(/^[^@]*/) - if !opts.first_name? or opts.first_name == "" - opts.first_name = username[0] + username = attributes.email.match(/^[^@]*/) + if !attributes.first_name? or attributes.first_name == "" + attributes.first_name = username[0] - for key, value of opts + for key, value of attributes user[key] = value user.ace.syntaxValidation = true @@ -27,6 +30,7 @@ module.exports = UserCreator = user.save (err)-> callback(err, user) + return if options?.skip_affiliation # call addaffiliation after the main callback so it runs in the # background. There is no guaranty this will run so we must no rely on it addAffiliation user._id, user.email, (error) -> diff --git a/services/web/test/unit/coffee/User/UserCreatorTests.coffee b/services/web/test/unit/coffee/User/UserCreatorTests.coffee index f9d88e4cf8..5f3bf60e6a 100644 --- a/services/web/test/unit/coffee/User/UserCreatorTests.coffee +++ b/services/web/test/unit/coffee/User/UserCreatorTests.coffee @@ -88,3 +88,11 @@ describe "UserCreator", -> process.nextTick () => sinon.assert.calledWith(@addAffiliation, user._id, user.email) done() + + it "should not add affiliation if skipping", (done)-> + attributes = email: @email + options = skip_affiliation: true + @UserCreator.createNewUser attributes, options, (err, user) => + process.nextTick () => + sinon.assert.notCalled(@addAffiliation) + done() From 8d6505b518f74b8797eb673293a3e5b24128f77a Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Fri, 17 Aug 2018 15:13:24 +0200 Subject: [PATCH 184/188] log institutions API errors --- .../web/app/coffee/Features/Institutions/InstitutionsAPI.coffee | 2 ++ services/web/test/unit/coffee/User/UserCreatorTests.coffee | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee index 8ce39d68e3..c62a8a01bc 100644 --- a/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee +++ b/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee @@ -80,6 +80,8 @@ makeAffiliationRequest = (requestOptions, callback = (error) ->) -> errorMessage = "#{response.statusCode}: #{body.errors}" else errorMessage = "#{requestOptions.defaultErrorMessage}: #{response.statusCode}" + + logger.err path: requestOptions.path, body: requestOptions.body, errorMessage return callback(new Error(errorMessage)) callback(null, body) diff --git a/services/web/test/unit/coffee/User/UserCreatorTests.coffee b/services/web/test/unit/coffee/User/UserCreatorTests.coffee index 5f3bf60e6a..d9118e450f 100644 --- a/services/web/test/unit/coffee/User/UserCreatorTests.coffee +++ b/services/web/test/unit/coffee/User/UserCreatorTests.coffee @@ -20,7 +20,7 @@ describe "UserCreator", -> @addAffiliation = sinon.stub().yields() @UserCreator = SandboxedModule.require modulePath, requires: "../../models/User": User:@UserModel - "logger-sharelatex":{log:->} + "logger-sharelatex":{ log: sinon.stub(), err: sinon.stub() } 'metrics-sharelatex': {timeAsyncMethod: ()->} "../Institutions/InstitutionsAPI": addAffiliation: @addAffiliation From 47c51257bbbaff6a4f57f3dc9ef3b17f83d37f81 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 20 Aug 2018 11:51:12 +0100 Subject: [PATCH 185/188] Explicitly set the user default email for the delete account modal controller. --- .../coffee/main/account-settings.coffee | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/services/web/public/coffee/main/account-settings.coffee b/services/web/public/coffee/main/account-settings.coffee index 82d323110e..b12b8f3dde 100644 --- a/services/web/public/coffee/main/account-settings.coffee +++ b/services/web/public/coffee/main/account-settings.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.controller "AccountSettingsController", ["$scope", "$http", "$modal", "event_tracking", ($scope, $http, $modal, event_tracking) -> + App.controller "AccountSettingsController", ["$scope", "$http", "$modal", "event_tracking", "UserAffiliationsDataService", ($scope, $http, $modal, event_tracking, UserAffiliationsDataService) -> $scope.subscribed = true $scope.unsubscribe = () -> @@ -21,8 +21,15 @@ define [ $scope.deleteAccount = () -> modalInstance = $modal.open( templateUrl: "deleteAccountModalTemplate" - controller: "DeleteAccountModalController", - scope: $scope + controller: "DeleteAccountModalController" + resolve: + userDefaultEmail: () -> + UserAffiliationsDataService + .getUserEmails() + .then (userEmails) -> + defaultEmailDetails = _.find userEmails, (userEmail) -> userEmail.default + return defaultEmailDetails?.email or null + .catch () -> null ) $scope.upgradeIntegration = (service) -> @@ -30,8 +37,8 @@ define [ ] App.controller "DeleteAccountModalController", [ - "$scope", "$modalInstance", "$timeout", "$http", - ($scope, $modalInstance, $timeout, $http) -> + "$scope", "$modalInstance", "$timeout", "$http", "userDefaultEmail", + ($scope, $modalInstance, $timeout, $http, userDefaultEmail) -> $scope.state = isValid : false deleteText: "" @@ -46,7 +53,7 @@ define [ , 700 $scope.checkValidation = -> - $scope.state.isValid = $scope.state.deleteText == $scope.email and $scope.state.password.length > 0 + $scope.state.isValid = userDefaultEmail? and $scope.state.deleteText == userDefaultEmail and $scope.state.password.length > 0 $scope.delete = () -> $scope.state.inflight = true From 042c959d3acb5e701d03a2ff1b2ceff5306944cc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 20 Aug 2018 12:15:15 +0100 Subject: [PATCH 186/188] Move default email checking logic into the data service. --- services/web/public/coffee/main/account-settings.coffee | 5 ++--- .../factories/UserAffiliationsDataService.coffee | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/main/account-settings.coffee b/services/web/public/coffee/main/account-settings.coffee index b12b8f3dde..cac35394a1 100644 --- a/services/web/public/coffee/main/account-settings.coffee +++ b/services/web/public/coffee/main/account-settings.coffee @@ -25,9 +25,8 @@ define [ resolve: userDefaultEmail: () -> UserAffiliationsDataService - .getUserEmails() - .then (userEmails) -> - defaultEmailDetails = _.find userEmails, (userEmail) -> userEmail.default + .getUserDefaultEmail() + .then (defaultEmailDetails) -> return defaultEmailDetails?.email or null .catch () -> null ) diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index cbcadf7e67..ca1ffef168 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -31,6 +31,10 @@ define [ $http.get "/user/emails" .then (response) -> response.data + getUserDefaultEmail = () -> + getUserEmails().then (userEmails) -> + _.find userEmails, (userEmail) -> userEmail.default + getUniversitiesFromCountry = (country) -> if universities[country.code]? universitiesFromCountry = universities[country.code] @@ -118,6 +122,7 @@ define [ getDefaultRoleHints getDefaultDepartmentHints getUserEmails + getUserDefaultEmail getUniversitiesFromCountry getUniversityDomainFromPartialDomainInput getUniversityDetails From 7a253e56ef94b90175b037a4b1384de9fc4cf9d8 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 21 Aug 2018 09:24:48 +0100 Subject: [PATCH 187/188] Download all log files from compiles.sharelatex.env (#833) * add a couple of missing buildPdfDownloadUrl * only download pdfs from compile server when streaming * use compiles.env for resonse files * update config to use clsi_lb --- services/web/config/settings.defaults.coffee | 3 ++- .../coffee/ide/pdf/controllers/PdfController.coffee | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 832c3855b5..59d6e0aa89 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -135,6 +135,7 @@ module.exports = settings = url: "http://#{process.env['FILESTORE_HOST'] or 'localhost'}:3009" clsi: url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013" + # url: "http://#{process.env['CLSI_LB_HOST']}:3014" backendGroupName: undefined templates: url: "http://#{process.env['TEMPLATES_HOST'] or 'localhost'}:3007" @@ -340,7 +341,7 @@ module.exports = settings = # disablePerUserCompiles: true # Domain the client (pdfjs) should download the compiled pdf from - # pdfDownloadDomain: "http://compiles.sharelatex.test:3014" + # pdfDownloadDomain: "http://clsi-lb:3014" # Maximum size of text documents in the real-time editing system. max_doc_length: 2 * 1024 * 1024 # 2mb diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index c717ef6b82..40264e1be0 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -225,7 +225,8 @@ define [ }, {params: params} buildPdfDownloadUrl = (pdfDownloadDomain, path)-> - if pdfDownloadDomain? + #we only download builds from compiles server for security reasons + if pdfDownloadDomain? and path.indexOf("build") != -1 return "#{pdfDownloadDomain}#{path}" else return path @@ -378,14 +379,15 @@ define [ compileGroup:ide.compileGroup clsiserverid:ide.clsiServerId if file?.url? # FIXME clean this up when we have file.urls out consistently - opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, file.url + opts.url = file.url else if file?.build? - opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}" + opts.url = "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}" else - opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, "/project/#{$scope.project_id}/output/#{name}" + opts.url = "/project/#{$scope.project_id}/output/#{name}" # check if we need to bust cache (build id is unique so don't need it in that case) if not file?.build? opts.params.cache_bust = "#{Date.now()}" + opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, opts.url return $http(opts) # accumulate the log entries From 473063cf3d2006cad75947d3a0a6013a82531cc6 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 21 Aug 2018 10:36:18 +0100 Subject: [PATCH 188/188] set rclient.expire for server ttl even when there is no server id --- .../web/app/coffee/Features/Compile/ClsiCookieManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index 2bee2ba5e6..f2c982a077 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -49,7 +49,7 @@ module.exports = (backendGroup)-> return callback() serverId = @_parseServerIdFromResponse(response) if !serverId? # We don't get a cookie back if it hasn't changed - return callback() + return rclient.expire(@buildKey(project_id), Settings.clsiCookie.ttl, callback) if rclient_secondary? @_setServerIdInRedis rclient_secondary, project_id, serverId @_setServerIdInRedis rclient, project_id, serverId, (err) ->