From 7e86e21107b7118ce3d47ca1986e9971e4981c29 Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Wed, 29 Aug 2018 13:43:42 -0300 Subject: [PATCH 01/33] Stylesheet for publish modal --- services/web/public/stylesheets/components/publish-modal.less | 4 ++++ services/web/public/stylesheets/ol-style.less | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 services/web/public/stylesheets/components/publish-modal.less diff --git a/services/web/public/stylesheets/components/publish-modal.less b/services/web/public/stylesheets/components/publish-modal.less new file mode 100644 index 0000000000..63803113a6 --- /dev/null +++ b/services/web/public/stylesheets/components/publish-modal.less @@ -0,0 +1,4 @@ +.publish-guide .form-control, +.publish-guide .form-control-box { + width: '30%'; +} diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less index 774e70a2ab..3c5c654c48 100644 --- a/services/web/public/stylesheets/ol-style.less +++ b/services/web/public/stylesheets/ol-style.less @@ -8,9 +8,10 @@ @import "_ol_style_includes.less"; @import "components/embed-responsive.less"; @import "components/icons.less"; +@import "components/publish-modal.less"; // Pages @import "app/about.less"; @import "app/blog-posts.less"; @import "app/cms-page.less"; -@import "app/portals.less"; \ No newline at end of file +@import "app/portals.less"; From f38c637f31afdeb1efd1f2c9c5202d76da68655a Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Wed, 29 Aug 2018 13:43:42 -0300 Subject: [PATCH 02/33] Stylesheet for publish modal --- services/web/public/stylesheets/components/publish-modal.less | 4 ++++ services/web/public/stylesheets/ol-style.less | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 services/web/public/stylesheets/components/publish-modal.less diff --git a/services/web/public/stylesheets/components/publish-modal.less b/services/web/public/stylesheets/components/publish-modal.less new file mode 100644 index 0000000000..63803113a6 --- /dev/null +++ b/services/web/public/stylesheets/components/publish-modal.less @@ -0,0 +1,4 @@ +.publish-guide .form-control, +.publish-guide .form-control-box { + width: '30%'; +} diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less index 2be148a497..0f99dab3f0 100644 --- a/services/web/public/stylesheets/ol-style.less +++ b/services/web/public/stylesheets/ol-style.less @@ -9,9 +9,10 @@ @import "components/embed-responsive.less"; @import "components/icons.less"; @import "components/pagination.less"; +@import "components/publish-modal.less"; // Pages @import "app/about.less"; @import "app/blog-posts.less"; @import "app/cms-page.less"; -@import "app/portals.less"; \ No newline at end of file +@import "app/portals.less"; From 2b9e978379076c6891e0a9e42d4050292560ec0b Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Fri, 31 Aug 2018 13:59:47 -0300 Subject: [PATCH 03/33] Repair syntax and additional publish-modal style --- services/web/public/stylesheets/components/publish-modal.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/components/publish-modal.less b/services/web/public/stylesheets/components/publish-modal.less index 63803113a6..b7fcad11b5 100644 --- a/services/web/public/stylesheets/components/publish-modal.less +++ b/services/web/public/stylesheets/components/publish-modal.less @@ -1,4 +1,5 @@ .publish-guide .form-control, .publish-guide .form-control-box { - width: '30%'; + width: 70%; + margin_bottom: 1.0ex; } From 92548be04445fbdbac425e9a545efb4ddc37c640 Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Fri, 31 Aug 2018 14:16:09 -0300 Subject: [PATCH 04/33] Use hyphen for underscore in publish-modal style --- services/web/public/stylesheets/components/publish-modal.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/components/publish-modal.less b/services/web/public/stylesheets/components/publish-modal.less index b7fcad11b5..2e463d4f16 100644 --- a/services/web/public/stylesheets/components/publish-modal.less +++ b/services/web/public/stylesheets/components/publish-modal.less @@ -1,5 +1,5 @@ .publish-guide .form-control, .publish-guide .form-control-box { width: 70%; - margin_bottom: 1.0ex; + margin-bottom: 1.0ex; } From 1e31f5dc1c35030301ab0d37db6dacd768b1558d Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Fri, 31 Aug 2018 15:35:24 -0300 Subject: [PATCH 05/33] Move new publish-model styles into existing style file --- .../web/public/stylesheets/app/editor/publish-modal.less | 5 +++++ .../web/public/stylesheets/components/publish-modal.less | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 services/web/public/stylesheets/components/publish-modal.less diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less index 3f049fc75b..6a017b7a53 100644 --- a/services/web/public/stylesheets/app/editor/publish-modal.less +++ b/services/web/public/stylesheets/app/editor/publish-modal.less @@ -1,4 +1,9 @@ .modal-body-publish { + .form-control, + .form-control-box { + width: 70%; + margin-bottom: 1.0ex; + } #search-input-container { overflow: hidden; margin: 5px 0 10px; diff --git a/services/web/public/stylesheets/components/publish-modal.less b/services/web/public/stylesheets/components/publish-modal.less deleted file mode 100644 index 2e463d4f16..0000000000 --- a/services/web/public/stylesheets/components/publish-modal.less +++ /dev/null @@ -1,5 +0,0 @@ -.publish-guide .form-control, -.publish-guide .form-control-box { - width: 70%; - margin-bottom: 1.0ex; -} From c570daf03b209bde2bccde0309821536a0da2190 Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Fri, 31 Aug 2018 18:36:19 -0300 Subject: [PATCH 06/33] Remove unneeded include from ol-style --- services/web/public/stylesheets/ol-style.less | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less index 74cfe9ff22..be8a97ea98 100644 --- a/services/web/public/stylesheets/ol-style.less +++ b/services/web/public/stylesheets/ol-style.less @@ -10,11 +10,10 @@ @import "components/icons.less"; @import "components/navs-ol.less"; @import "components/pagination.less"; -@import "components/publish-modal.less"; // Pages @import "app/about.less"; @import "app/blog-posts.less"; @import "app/cms-page.less"; @import "app/plans-ol.less"; -@import "app/portals.less"; +@import "app/portals.less"; \ No newline at end of file From 91e653dafcd09880d21491ef6de76729db11a4d1 Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Fri, 31 Aug 2018 18:36:53 -0300 Subject: [PATCH 07/33] Limit width and margin publish model form controls to the enclosing box --- services/web/public/stylesheets/app/editor/publish-modal.less | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less index 6a017b7a53..61e5a84b80 100644 --- a/services/web/public/stylesheets/app/editor/publish-modal.less +++ b/services/web/public/stylesheets/app/editor/publish-modal.less @@ -1,5 +1,4 @@ .modal-body-publish { - .form-control, .form-control-box { width: 70%; margin-bottom: 1.0ex; From 1965967361f3a68cb43ecea772a807d5e2be1811 Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Mon, 3 Sep 2018 06:51:15 -0300 Subject: [PATCH 08/33] Use less vertical space in gallery publish modal --- .../stylesheets/app/editor/publish-modal.less | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less index 61e5a84b80..49ee22b1f3 100644 --- a/services/web/public/stylesheets/app/editor/publish-modal.less +++ b/services/web/public/stylesheets/app/editor/publish-modal.less @@ -1,7 +1,18 @@ .modal-body-publish { .form-control-box { - width: 70%; margin-bottom: 1.0ex; + margin-left: 1.0em; + label { + display: inline-block; + width: 10em; + } + .form-control { + display: inline-block; + width: 60%; + } + input[type="checkbox"] { + margin-right: 0.5em; + } } #search-input-container { overflow: hidden; From fa7f8ebfeddda6ecb848616670b47c3f582ed0f2 Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Tue, 4 Sep 2018 07:22:58 -0300 Subject: [PATCH 09/33] Styling tweaks to Gallery publish dialog --- .../public/stylesheets/app/editor/publish-modal.less | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less index 49ee22b1f3..b10ae8b58a 100644 --- a/services/web/public/stylesheets/app/editor/publish-modal.less +++ b/services/web/public/stylesheets/app/editor/publish-modal.less @@ -1,6 +1,6 @@ .modal-body-publish { .form-control-box { - margin-bottom: 1.0ex; + margin-bottom: 1.5ex; margin-left: 1.0em; label { display: inline-block; @@ -13,6 +13,15 @@ input[type="checkbox"] { margin-right: 0.5em; } + select { + padding-top: 1ex; + padding-bottom: 1ex; + padding-left: 1em; + padding-right: 1em; + } + option { + margin-left: -4px; + } } #search-input-container { overflow: hidden; From 15a54d8228b9c427db2615481ea2430457851c64 Mon Sep 17 00:00:00 2001 From: Douglas Lovell Date: Tue, 4 Sep 2018 08:53:29 -0300 Subject: [PATCH 10/33] Style for label alignment on text area --- services/web/public/stylesheets/app/editor/publish-modal.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less index b10ae8b58a..dbf49a945d 100644 --- a/services/web/public/stylesheets/app/editor/publish-modal.less +++ b/services/web/public/stylesheets/app/editor/publish-modal.less @@ -5,6 +5,7 @@ label { display: inline-block; width: 10em; + vertical-align: baseline; } .form-control { display: inline-block; @@ -13,6 +14,9 @@ input[type="checkbox"] { margin-right: 0.5em; } + textarea { + vertical-align: baseline; + } select { padding-top: 1ex; padding-bottom: 1ex; From fa23ea75b8080f09f198e26d81236e7006537c75 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Sun, 2 Sep 2018 13:47:16 +0100 Subject: [PATCH 11/33] Call university ip matcher api when checking notifications --- .../Notifications/NotificationsBuilder.coffee | 15 +++++++++++++++ .../Notifications/NotificationsController.coffee | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 941f4d4d4d..849995db6c 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -1,5 +1,7 @@ logger = require("logger-sharelatex") NotificationsHandler = require("./NotificationsHandler") +request = require "request" +settings = require "settings-sharelatex" module.exports = @@ -29,3 +31,16 @@ module.exports = NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback read: (callback=()->) -> NotificationsHandler.markAsReadByKeyOnly @key, callback + + ipMatcherAffiliation: (userId, ip) -> + return null unless settings?.apis?.v1?.url # service is not configured + request { + method: 'GET' + url: "#{settings.apis.v1.url}/api/v2/users/ip_matcher/#{userId}" + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass } + body: { ip: ip } + json: true + timeout: 20 * 1000 + }, (error, response, body) -> + return error if error? + return null if response.statusCode == 204 diff --git a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee index 5b83a60248..517f17d325 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee @@ -1,12 +1,22 @@ NotificationsHandler = require("./NotificationsHandler") +NotificationsBuilder = require("./NotificationsBuilder") AuthenticationController = require("../Authentication/AuthenticationController") +Settings = require 'settings-sharelatex' logger = require("logger-sharelatex") _ = require("underscore") module.exports = getAllUnreadNotifications: (req, res)-> + ip = req.headers['x-forwarded-for'] || + req.connection.remoteAddress || + req.socket.remoteAddress user_id = AuthenticationController.getLoggedInUserId(req) + + # in v2 add notifications for matching university IPs + if Settings.overleaf? + NotificationsBuilder.ipMatcherAffiliation(user_id, ip) + NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)-> unreadNotifications = _.map unreadNotifications, (notification)-> notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts) From 38faa5c25ea2275215269d6a548f50fde089c607 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Sun, 2 Sep 2018 15:22:45 +0100 Subject: [PATCH 12/33] correctly create and display ip matched affiliations --- .../Notifications/NotificationsBuilder.coffee | 35 +++++++++++++------ .../NotificationsController.coffee | 4 ++- .../app/views/project/list/notifications.pug | 10 ++++++ 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 849995db6c..229c97cb04 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -33,14 +33,27 @@ module.exports = NotificationsHandler.markAsReadByKeyOnly @key, callback ipMatcherAffiliation: (userId, ip) -> - return null unless settings?.apis?.v1?.url # service is not configured - request { - method: 'GET' - url: "#{settings.apis.v1.url}/api/v2/users/ip_matcher/#{userId}" - auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass } - body: { ip: ip } - json: true - timeout: 20 * 1000 - }, (error, response, body) -> - return error if error? - return null if response.statusCode == 204 + key: "ip-matched-affiliation-#{ip}" + create: (callback=()->) -> + return null unless settings?.apis?.v1?.url # service is not configured + _key = @key + request { + method: 'GET' + url: "#{settings.apis.v1.url}/api/v2/users/ip_matcher/#{userId}" + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass } + body: { ip: ip } + json: true + timeout: 20 * 1000 + }, (error, response, body) -> + return error if error? + return null if response.statusCode == 204 + + messageOpts = + university_id: body.university_id + university_name: body.university_name + content: body.ad_copy + logger.log user_id:userId, key:_key, "creating notification key for user" + NotificationsHandler.createNotification userId, _key, "notification_ip_matched_affiliation", messageOpts, null, callback + + read: (callback = ->)-> + NotificationsHandler.markAsReadWithKey userId, @key, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee index 517f17d325..5fde9bc955 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee @@ -15,7 +15,9 @@ module.exports = # in v2 add notifications for matching university IPs if Settings.overleaf? - NotificationsBuilder.ipMatcherAffiliation(user_id, ip) + NotificationsBuilder.ipMatcherAffiliation(user_id, ip).create((err) -> + return err + ) NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)-> unreadNotifications = _.map unreadNotifications, (notification)-> diff --git a/services/web/app/views/project/list/notifications.pug b/services/web/app/views/project/list/notifications.pug index 55798d6a2b..2b4466ebab 100644 --- a/services/web/app/views/project/list/notifications.pug +++ b/services/web/app/views/project/list/notifications.pug @@ -60,6 +60,16 @@ span(ng-controller="NotificationsController").userNotifications button(ng-click="dismiss(notification)").close.pull-right span(aria-hidden="true") × span.sr-only #{translate("close")} + .alert.alert-info(ng-switch-when="notification_ip_matched_affiliation") + div.notification_inner + .notification_body + | Add an email for: {{ notification.messageOpts.university_name }} + a.pull-right.btn.btn-sm.btn-info(href="/user/settings") + | Add Affiliation + span().notification_close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} .alert.alert-info(ng-switch-default) div.notification_inner span(ng-bind-html="notification.html").notification_body From de83df2703e9078d4f504e7859e76b1a098864bb Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Sun, 2 Sep 2018 17:28:51 +0100 Subject: [PATCH 13/33] adding tests for ip matching notifications --- .../NotificationsBuilderTests.coffee | 40 +++++++++++++++++++ .../NotificationsControllerTests.coffee | 15 +++++++ 2 files changed, 55 insertions(+) create mode 100644 services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee diff --git a/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee new file mode 100644 index 0000000000..8b984a8cee --- /dev/null +++ b/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee @@ -0,0 +1,40 @@ +SandboxedModule = require('sandboxed-module') +assert = require('chai').assert +require('chai').should() +sinon = require('sinon') +modulePath = require('path').join __dirname, '../../../../app/js/Features/Notifications/NotificationsBuilder.js' + +describe 'NotificationsBuilder', -> + user_id = "123nd3ijdks" + + beforeEach -> + @handler = + createNotification: sinon.stub().callsArgWith(5) + + @settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } } + @body = {university_id: 1, university_name: 'stanford', ad_copy: 'v1 ad content'} + response = {statusCode: 200} + @request = sinon.stub().returns(@stubResponse).callsArgWith(1, null, response, @body) + @controller = SandboxedModule.require modulePath, requires: + "./NotificationsHandler":@handler + "settings-sharelatex":@settings + 'request': @request + "logger-sharelatex": + log:-> + err:-> + + it 'should call v1 and create affiliation notifications', (done)-> + ip = '192.168.0.1' + @controller.ipMatcherAffiliation(user_id, ip).create (callback)=> + @request.calledOnce.should.equal true + expectedOpts = + university_id: @body.university_id + university_name: @body.university_name + content: @body.ad_copy + @handler.createNotification.calledWith( + user_id, + "ip-matched-affiliation-#{ip}", + "notification_ip_matched_affiliation", + expectedOpts + ).should.equal true + done() diff --git a/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee index 126b223f04..9fab8a96f5 100644 --- a/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee +++ b/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee @@ -13,7 +13,14 @@ describe 'NotificationsController', -> @handler = getUserNotifications: sinon.stub().callsArgWith(1) markAsRead: sinon.stub().callsArgWith(2) + @builder = + ipMatcherAffiliation: sinon.stub().returns({create: sinon.stub()}) + @settings = + apis: { v1: { url: 'v1.url', user: '', pass: '' } } @req = + headers: {} + connection: + remoteAddress: "192.170.18.1" params: notification_id:notification_id session: @@ -25,6 +32,8 @@ describe 'NotificationsController', -> getLoggedInUserId: sinon.stub().returns(@req.session.user._id) @controller = SandboxedModule.require modulePath, requires: "./NotificationsHandler":@handler + "./NotificationsBuilder":@builder + "settings-sharelatex":@settings "underscore":@underscore = map:(arr)-> return arr 'logger-sharelatex': @@ -44,3 +53,9 @@ describe 'NotificationsController', -> @controller.markNotificationAsRead @req, send:=> @handler.markAsRead.calledWith(user_id, notification_id).should.equal true done() + + it 'should call the builder with the user ip if v2', (done)-> + @settings.overleaf = true + @controller.getAllUnreadNotifications @req, send:(body)=> + @builder.ipMatcherAffiliation.calledWith(user_id, @req.connection.remoteAddress).should.equal true + done() From bf2ea4e7b3a9688cad95696921d476ec654faebe Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Tue, 4 Sep 2018 17:26:15 +0100 Subject: [PATCH 14/33] test against ip matcher for notification on login if different from previous ip --- .../Authentication/AuthenticationController.coffee | 11 +++++++++++ services/web/app/coffee/models/User.coffee | 1 + 2 files changed, 12 insertions(+) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 8a2c33536a..926d72a257 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -11,6 +11,7 @@ UserHandler = require("../User/UserHandler") UserSessionsManager = require("../User/UserSessionsManager") Analytics = require "../Analytics/AnalyticsManager" passport = require 'passport' +NotificationsBuilder = require("../Notifications/NotificationsBuilder") module.exports = AuthenticationController = @@ -72,6 +73,7 @@ module.exports = AuthenticationController = finishLogin: (user, req, res, next) -> redir = AuthenticationController._getRedirectFromSession(req) || "/project" AuthenticationController._loginAsyncHandlers(req, user) + AuthenticationController.ipMatchCheck(req, user) AuthenticationController.afterLoginSessionSetup req, user, (err) -> if err? return next(err) @@ -119,6 +121,15 @@ module.exports = AuthenticationController = # capture the request ip for use when creating the session user._login_req_ip = req.ip + ipMatchCheck: (req, user) -> + if req.ip != user.lastLoginIp + NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create((err) -> + return err + ) + UserUpdater.updateUser user._id.toString(), { + $set: { "lastLoginIp": req.ip } + } + setInSessionUser: (req, props) -> for key, value of props if req?.session?.passport?.user? diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index 95bedebbf4..23b59375f4 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -22,6 +22,7 @@ UserSchema = new Schema confirmed : {type : Boolean, default : false} signUpDate : {type : Date, default: () -> new Date() } lastLoggedIn : {type : Date} + lastLoginIp : {type : String, default : ''} loginCount : {type : Number, default: 0} holdingAccount : {type : Boolean, default: false} ace : { From d950e14b3fe5b5fb2ced2295381a894fd459e185 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Wed, 5 Sep 2018 09:44:45 +0100 Subject: [PATCH 15/33] use new routes and params from v1 ip matcher endpoint --- .../Notifications/NotificationsBuilder.coffee | 8 ++++---- .../Notifications/NotificationsController.coffee | 14 +++++++++++--- .../web/app/views/project/list/notifications.pug | 3 ++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 229c97cb04..419a2a21f1 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -39,7 +39,7 @@ module.exports = _key = @key request { method: 'GET' - url: "#{settings.apis.v1.url}/api/v2/users/ip_matcher/#{userId}" + url: "#{settings.apis.v1.url}/api/v2/users/#{userId}/ip_matcher" auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass } body: { ip: ip } json: true @@ -49,9 +49,9 @@ module.exports = return null if response.statusCode == 204 messageOpts = - university_id: body.university_id - university_name: body.university_name - content: body.ad_copy + university_id: body.id + university_name: body.name + content: body.enrolment_ad_html logger.log user_id:userId, key:_key, "creating notification key for user" NotificationsHandler.createNotification userId, _key, "notification_ip_matched_affiliation", messageOpts, null, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee index 5fde9bc955..53ae2af4ac 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee @@ -2,7 +2,9 @@ NotificationsHandler = require("./NotificationsHandler") NotificationsBuilder = require("./NotificationsBuilder") AuthenticationController = require("../Authentication/AuthenticationController") Settings = require 'settings-sharelatex' +UserGetter = require("../User/UserGetter") logger = require("logger-sharelatex") +async = require "async" _ = require("underscore") module.exports = @@ -15,9 +17,15 @@ module.exports = # in v2 add notifications for matching university IPs if Settings.overleaf? - NotificationsBuilder.ipMatcherAffiliation(user_id, ip).create((err) -> - return err - ) + UserGetter.getUser user_id, { 'lastLoginIp': 1 }, (error, user) -> + console.log(user.lastLoginIp) + if ip != user.lastLoginIp + async.series ([ + () -> + NotificationsBuilder.ipMatcherAffiliation(user_id, ip).create((err) -> + return err + ) + ]) NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)-> unreadNotifications = _.map unreadNotifications, (notification)-> diff --git a/services/web/app/views/project/list/notifications.pug b/services/web/app/views/project/list/notifications.pug index 2b4466ebab..ef2ac66440 100644 --- a/services/web/app/views/project/list/notifications.pug +++ b/services/web/app/views/project/list/notifications.pug @@ -63,7 +63,8 @@ span(ng-controller="NotificationsController").userNotifications .alert.alert-info(ng-switch-when="notification_ip_matched_affiliation") div.notification_inner .notification_body - | Add an email for: {{ notification.messageOpts.university_name }} + | Add an email for + strong {{ notification.messageOpts.university_name }} a.pull-right.btn.btn-sm.btn-info(href="/user/settings") | Add Affiliation span().notification_close From f20d27986b6165c0841b4954f6325fb6ade60413 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Wed, 5 Sep 2018 10:20:41 +0100 Subject: [PATCH 16/33] create ip match notifications without forcing replacement --- .../Features/Notifications/NotificationsBuilder.coffee | 2 +- .../Features/Notifications/NotificationsHandler.coffee | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 419a2a21f1..1b9eb56cf9 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -53,7 +53,7 @@ module.exports = university_name: body.name content: body.enrolment_ad_html logger.log user_id:userId, key:_key, "creating notification key for user" - NotificationsHandler.createNotification userId, _key, "notification_ip_matched_affiliation", messageOpts, null, callback + NotificationsHandler.createNotification userId, _key, "notification_ip_matched_affiliation", messageOpts, null, false, callback read: (callback = ->)-> NotificationsHandler.markAsReadWithKey userId, @key, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index 5a6ca47c2e..a0f6ae5c12 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -29,12 +29,15 @@ module.exports = unreadNotifications = [] callback(null, unreadNotifications) - createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)-> + createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)-> + if !callback + callback = forceCreate + forceCreate = true payload = { key:key messageOpts:messageOpts templateKey:templateKey - forceCreate: true + forceCreate:forceCreate } if expiryDateTime? payload.expires = expiryDateTime From 23e6292fd7c60dccde57407fef8b74024e12c31d Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Wed, 5 Sep 2018 10:44:34 +0100 Subject: [PATCH 17/33] updating tests for ip matcher logic --- .../Notifications/NotificationsController.coffee | 1 - .../AuthenticationControllerTests.coffee | 1 + .../Notifications/NotificationsBuilderTests.coffee | 10 +++++----- .../Notifications/NotificationsControllerTests.coffee | 5 ++++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee index 53ae2af4ac..08ac42b6ac 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee @@ -18,7 +18,6 @@ module.exports = # in v2 add notifications for matching university IPs if Settings.overleaf? UserGetter.getUser user_id, { 'lastLoginIp': 1 }, (error, user) -> - console.log(user.lastLoginIp) if ip != user.lastLoginIp async.series ([ () -> diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index 24af9971d2..5f28a207b2 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -603,6 +603,7 @@ describe "AuthenticationController", -> @AuthenticationController._loginAsyncHandlers = sinon.stub() @AuthenticationController.afterLoginSessionSetup = sinon.stub().callsArgWith(2, null) @AuthenticationController._clearRedirectFromSession = sinon.stub() + @UserUpdater.updateUser = sinon.stub() @req.headers = {accept: 'application/json, whatever'} @res.json = sinon.stub() @res.redirect = sinon.stub() diff --git a/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee index 8b984a8cee..941e26df1e 100644 --- a/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee +++ b/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee @@ -9,10 +9,10 @@ describe 'NotificationsBuilder', -> beforeEach -> @handler = - createNotification: sinon.stub().callsArgWith(5) + createNotification: sinon.stub().callsArgWith(6) @settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } } - @body = {university_id: 1, university_name: 'stanford', ad_copy: 'v1 ad content'} + @body = {id: 1, name: 'stanford', enrolment_ad_html: 'v1 ad content'} response = {statusCode: 200} @request = sinon.stub().returns(@stubResponse).callsArgWith(1, null, response, @body) @controller = SandboxedModule.require modulePath, requires: @@ -28,9 +28,9 @@ describe 'NotificationsBuilder', -> @controller.ipMatcherAffiliation(user_id, ip).create (callback)=> @request.calledOnce.should.equal true expectedOpts = - university_id: @body.university_id - university_name: @body.university_name - content: @body.ad_copy + university_id: @body.id + university_name: @body.name + content: @body.enrolment_ad_html @handler.createNotification.calledWith( user_id, "ip-matched-affiliation-#{ip}", diff --git a/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee index 9fab8a96f5..191b869fec 100644 --- a/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee +++ b/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee @@ -15,6 +15,8 @@ describe 'NotificationsController', -> markAsRead: sinon.stub().callsArgWith(2) @builder = ipMatcherAffiliation: sinon.stub().returns({create: sinon.stub()}) + @userGetter = + getUser: sinon.stub().callsArgWith 2, null, {lastLoginIp: '192.170.18.2'} @settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } } @req = @@ -33,6 +35,7 @@ describe 'NotificationsController', -> @controller = SandboxedModule.require modulePath, requires: "./NotificationsHandler":@handler "./NotificationsBuilder":@builder + "../User/UserGetter": @userGetter "settings-sharelatex":@settings "underscore":@underscore = map:(arr)-> return arr @@ -49,7 +52,7 @@ describe 'NotificationsController', -> @handler.getUserNotifications.calledWith(user_id).should.equal true done() - it 'should send a delete request when a delete has been received to mark a notification', (done)-> + it 'should send a remove request when notification read', (done)-> @controller.markNotificationAsRead @req, send:=> @handler.markAsRead.calledWith(user_id, notification_id).should.equal true done() From 5605e1c5c3ddae9144002fc7249542f273aec957 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Wed, 5 Sep 2018 11:02:13 +0100 Subject: [PATCH 18/33] update copy for ip match notifications --- services/web/app/views/project/list/notifications.pug | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/list/notifications.pug b/services/web/app/views/project/list/notifications.pug index ef2ac66440..04ba3827dc 100644 --- a/services/web/app/views/project/list/notifications.pug +++ b/services/web/app/views/project/list/notifications.pug @@ -63,8 +63,12 @@ span(ng-controller="NotificationsController").userNotifications .alert.alert-info(ng-switch-when="notification_ip_matched_affiliation") div.notification_inner .notification_body - | Add an email for - strong {{ notification.messageOpts.university_name }} + | It looks like you're at + strong {{ notification.messageOpts.university_name }}!
+ | Did you know that {{notification.messageOpts.university_name}} is providing + strong free Overleaf Professional accounts + | to everyone at {{notification.messageOpts.university_name}}?
+ | Add an institutional email address to claim your account. a.pull-right.btn.btn-sm.btn-info(href="/user/settings") | Add Affiliation span().notification_close From 8ef90a0dcbdfcd19ea9e8753e3bafe03aaf9075c Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Wed, 5 Sep 2018 15:28:26 +0100 Subject: [PATCH 19/33] move call for creating ip matched notifcation to project controller --- .../AuthenticationController.coffee | 2 +- .../Notifications/NotificationsBuilder.coffee | 2 +- .../NotificationsController.coffee | 19 ------------------ .../Features/Project/ProjectController.coffee | 12 +++++++++++ .../AuthenticationControllerTests.coffee | 3 +-- .../NotificationsControllerTests.coffee | 20 +------------------ .../Project/ProjectControllerTests.coffee | 16 +++++++++++++++ 7 files changed, 32 insertions(+), 42 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 926d72a257..49dcd25359 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -73,7 +73,6 @@ module.exports = AuthenticationController = finishLogin: (user, req, res, next) -> redir = AuthenticationController._getRedirectFromSession(req) || "/project" AuthenticationController._loginAsyncHandlers(req, user) - AuthenticationController.ipMatchCheck(req, user) AuthenticationController.afterLoginSessionSetup req, user, (err) -> if err? return next(err) @@ -114,6 +113,7 @@ module.exports = AuthenticationController = UserHandler.setupLoginData(user, ()->) LoginRateLimiter.recordSuccessfulLogin(user.email) AuthenticationController._recordSuccessfulLogin(user._id) + AuthenticationController.ipMatchCheck(req, user) Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip}) Analytics.identifyUser(user._id, req.sessionID) logger.log email: user.email, user_id: user._id.toString(), "successful log in" diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 1b9eb56cf9..c0280450ab 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -46,7 +46,7 @@ module.exports = timeout: 20 * 1000 }, (error, response, body) -> return error if error? - return null if response.statusCode == 204 + return null unless response.statusCode == 200 messageOpts = university_id: body.id diff --git a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee index 08ac42b6ac..5b83a60248 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee @@ -1,31 +1,12 @@ NotificationsHandler = require("./NotificationsHandler") -NotificationsBuilder = require("./NotificationsBuilder") AuthenticationController = require("../Authentication/AuthenticationController") -Settings = require 'settings-sharelatex' -UserGetter = require("../User/UserGetter") logger = require("logger-sharelatex") -async = require "async" _ = require("underscore") module.exports = getAllUnreadNotifications: (req, res)-> - ip = req.headers['x-forwarded-for'] || - req.connection.remoteAddress || - req.socket.remoteAddress user_id = AuthenticationController.getLoggedInUserId(req) - - # in v2 add notifications for matching university IPs - if Settings.overleaf? - UserGetter.getUser user_id, { 'lastLoginIp': 1 }, (error, user) -> - if ip != user.lastLoginIp - async.series ([ - () -> - NotificationsBuilder.ipMatcherAffiliation(user_id, ip).create((err) -> - return err - ) - ]) - NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)-> unreadNotifications = _.map unreadNotifications, (notification)-> notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 24dca35e96..e5acd1dfb9 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -26,6 +26,8 @@ TokenAccessHandler = require '../TokenAccess/TokenAccessHandler' CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler' Modules = require '../../infrastructure/Modules' ProjectEntityHandler = require './ProjectEntityHandler' +UserGetter = require("../User/UserGetter") +NotificationsBuilder = require("../Notifications/NotificationsBuilder") crypto = require 'crypto' { V1ConnectionError } = require '../Errors/Errors' Features = require('../../infrastructure/Features') @@ -209,6 +211,16 @@ module.exports = ProjectController = user = results.user warnings = ProjectController._buildWarningsList results.v1Projects + # in v2 add notifications for matching university IPs + if Settings.overleaf? + ip = req.headers['x-forwarded-for'] || + req.connection.remoteAddress || + req.socket.remoteAddress + UserGetter.getUser user_id, { 'lastLoginIp': 1 }, (error, user) -> + if ip != user.lastLoginIp + NotificationsBuilder.ipMatcherAffiliation(user._id, ip).create((err) -> + return err + ) ProjectController._injectProjectOwners projects, (error, projects) -> return next(error) if error? diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index 5f28a207b2..300a4663e7 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -15,7 +15,7 @@ describe "AuthenticationController", -> tk.freeze(Date.now()) @AuthenticationController = SandboxedModule.require modulePath, requires: "./AuthenticationManager": @AuthenticationManager = {} - "../User/UserUpdater" : @UserUpdater = {} + "../User/UserUpdater" : @UserUpdater = {updateUser:sinon.stub()} "metrics-sharelatex": @Metrics = { inc: sinon.stub() } "../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() } "../User/UserHandler": @UserHandler = {setupLoginData:sinon.stub()} @@ -603,7 +603,6 @@ describe "AuthenticationController", -> @AuthenticationController._loginAsyncHandlers = sinon.stub() @AuthenticationController.afterLoginSessionSetup = sinon.stub().callsArgWith(2, null) @AuthenticationController._clearRedirectFromSession = sinon.stub() - @UserUpdater.updateUser = sinon.stub() @req.headers = {accept: 'application/json, whatever'} @res.json = sinon.stub() @res.redirect = sinon.stub() diff --git a/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee index 191b869fec..126b223f04 100644 --- a/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee +++ b/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee @@ -13,16 +13,7 @@ describe 'NotificationsController', -> @handler = getUserNotifications: sinon.stub().callsArgWith(1) markAsRead: sinon.stub().callsArgWith(2) - @builder = - ipMatcherAffiliation: sinon.stub().returns({create: sinon.stub()}) - @userGetter = - getUser: sinon.stub().callsArgWith 2, null, {lastLoginIp: '192.170.18.2'} - @settings = - apis: { v1: { url: 'v1.url', user: '', pass: '' } } @req = - headers: {} - connection: - remoteAddress: "192.170.18.1" params: notification_id:notification_id session: @@ -34,9 +25,6 @@ describe 'NotificationsController', -> getLoggedInUserId: sinon.stub().returns(@req.session.user._id) @controller = SandboxedModule.require modulePath, requires: "./NotificationsHandler":@handler - "./NotificationsBuilder":@builder - "../User/UserGetter": @userGetter - "settings-sharelatex":@settings "underscore":@underscore = map:(arr)-> return arr 'logger-sharelatex': @@ -52,13 +40,7 @@ describe 'NotificationsController', -> @handler.getUserNotifications.calledWith(user_id).should.equal true done() - it 'should send a remove request when notification read', (done)-> + it 'should send a delete request when a delete has been received to mark a notification', (done)-> @controller.markNotificationAsRead @req, send:=> @handler.markAsRead.calledWith(user_id, notification_id).should.equal true done() - - it 'should call the builder with the user ip if v2', (done)-> - @settings.overleaf = true - @controller.getAllUnreadNotifications @req, send:(body)=> - @builder.ipMatcherAffiliation.calledWith(user_id, @req.connection.remoteAddress).should.equal true - done() diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index b909376cca..f7edc94ad1 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -69,6 +69,10 @@ describe "ProjectController", -> @CollaboratorsHandler = userIsTokenMember: sinon.stub().callsArgWith(2, null, false) @ProjectEntityHandler = {} + @NotificationBuilder = + ipMatcherAffiliation: sinon.stub().returns({create: sinon.stub()}) + @UserGetter = + getUser: sinon.stub().callsArgWith 2, null, {lastLoginIp: '192.170.18.2'} @Modules = hooks: fire: sinon.stub() @@ -105,11 +109,16 @@ describe "ProjectController", -> "./ProjectEntityHandler": @ProjectEntityHandler "../Errors/Errors": Errors "../../infrastructure/Features": @Features + "../Notifications/NotificationsBuilder":@NotificationBuilder + "../User/UserGetter": @UserGetter @projectName = "£12321jkj9ujkljds" @req = params: Project_id: @project_id + headers: {} + connection: + remoteAddress: "192.170.18.1" session: user: @user body: @@ -301,6 +310,13 @@ describe "ProjectController", -> done() @ProjectController.projectListPage @req, @res + it "should create trigger ip matcher notifications", (done)-> + @settings.overleaf = true + @res.render = (pageName, opts)=> + @NotificationBuilder.ipMatcherAffiliation.called.should.equal true + done() + @ProjectController.projectListPage @req, @res + it "should send the projects", (done)-> @res.render = (pageName, opts)=> opts.projects.length.should.equal (@projects.length + @collabertions.length + @readOnly.length + @tokenReadAndWrite.length + @tokenReadOnly.length) From 1e04a09ec6369fd4db94e7771d31b8d4fdcc86f0 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Fri, 7 Sep 2018 18:15:32 +0100 Subject: [PATCH 20/33] remove unnecessary error returns and ip fetching --- .../Authentication/AuthenticationController.coffee | 4 +--- .../app/coffee/Features/Project/ProjectController.coffee | 9 ++------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 49dcd25359..f8d90756b2 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -123,9 +123,7 @@ module.exports = AuthenticationController = ipMatchCheck: (req, user) -> if req.ip != user.lastLoginIp - NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create((err) -> - return err - ) + NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create() UserUpdater.updateUser user._id.toString(), { $set: { "lastLoginIp": req.ip } } diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index e5acd1dfb9..59c0647c19 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -213,14 +213,9 @@ module.exports = ProjectController = # in v2 add notifications for matching university IPs if Settings.overleaf? - ip = req.headers['x-forwarded-for'] || - req.connection.remoteAddress || - req.socket.remoteAddress UserGetter.getUser user_id, { 'lastLoginIp': 1 }, (error, user) -> - if ip != user.lastLoginIp - NotificationsBuilder.ipMatcherAffiliation(user._id, ip).create((err) -> - return err - ) + if req.ip != user.lastLoginIp + NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create() ProjectController._injectProjectOwners projects, (error, projects) -> return next(error) if error? From b6a4bb74f89da700d8165a39c98859ce8e8c618e Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 10 Sep 2018 11:02:08 +0100 Subject: [PATCH 21/33] Store cursor & line position when switching editor When tearing down the source editor, we need to store the updated cursor position, so that the position can be moved to when opening the rich text editor. --- .../coffee/ide/editor/directives/aceEditor.coffee | 1 + .../cursor-position/CursorPositionManager.coffee | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index e3ab0b29be..015a4e65ee 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -516,6 +516,7 @@ define [ scope.$on '$destroy', () -> if scope.sharejsDoc? + scope.$broadcast('changeEditor') tearDownSpellCheck() tearDownCursorPosition() detachFromAce(scope.sharejsDoc) 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 bb957f8cf6..764fcb1c98 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 @@ -3,12 +3,12 @@ define [], () -> constructor: (@$scope, @adapter, @localStorage) -> @$scope.$on 'editorInit', @jumpToPositionInNewDoc - @$scope.$on 'beforeChangeDocument', () => - @storeCursorPosition() - @storeFirstVisibleLine() + @$scope.$on 'beforeChangeDocument', @storePositionAndLine @$scope.$on 'afterChangeDocument', @jumpToPositionInNewDoc + @$scope.$on 'changeEditor', @storePositionAndLine + @$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) => if line? setTimeout () => @@ -24,6 +24,10 @@ define [], () -> @$scope.$on "#{@$scope.name}:clearSelection", (e) => @adapter.clearSelection() + storePositionAndLine: () => + @storeCursorPosition() + @storeFirstVisibleLine() + jumpToPositionInNewDoc: () => @doc_id = @$scope.sharejsDoc?.doc_id setTimeout () => From dd056e36aec8f5a17984c49b893df4e5ea43fa5e Mon Sep 17 00:00:00 2001 From: Ersun Warncke Date: Wed, 22 Aug 2018 09:20:12 -0400 Subject: [PATCH 22/33] add overleaf method --- services/web/test/acceptance/coffee/helpers/User.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index b8740f709c..691a214c8b 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -80,6 +80,9 @@ class User update["features.#{key}"] = value UserModel.update { _id: @id }, update, callback + setOverleafId: (overleaf_id, callback = (error) ->) -> + UserModel.update { _id: @id }, { 'overleaf.id': overleaf_id }, callback + logout: (callback = (error) ->) -> @getCsrfToken (error) => return callback(error) if error? From 7d3e17651fa68160fe0b2401dcf628cac185b1c0 Mon Sep 17 00:00:00 2001 From: Ersun Warncke Date: Fri, 27 Jul 2018 10:43:03 -0400 Subject: [PATCH 23/33] set options and method for request, pass cookies and form body --- .../coffee/infrastructure/ProxyManager.coffee | 10 ++- .../infrastructure/ProxyManagerTests.coffee | 70 +++++++++++++++++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ProxyManager.coffee b/services/web/app/coffee/infrastructure/ProxyManager.coffee index 4d64b25bae..3e7a037054 100644 --- a/services/web/app/coffee/infrastructure/ProxyManager.coffee +++ b/services/web/app/coffee/infrastructure/ProxyManager.coffee @@ -7,14 +7,20 @@ module.exports = ProxyManager = apply: (publicApiRouter) -> for proxyUrl, target of settings.proxyUrls do (target) -> - publicApiRouter.get proxyUrl, ProxyManager.createProxy(target) + method = target.options?.method || 'get' + publicApiRouter[method] proxyUrl, ProxyManager.createProxy(target) createProxy: (target) -> (req, res, next) -> targetUrl = makeTargetUrl(target, req) logger.log targetUrl: targetUrl, reqUrl: req.url, "proxying url" - upstream = request(targetUrl) + options = + url: targetUrl + options.headers = { Cookie: req.headers.cookie } if req.headers?.cookie + Object.assign(options, target.options) if target?.options? + options.form = req.body if options.method in ['post', 'put'] + upstream = request(options) upstream.on "error", (error) -> logger.error err: error, "error in ProxyManager" diff --git a/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee b/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee index 3f1c1e9f78..9a1d9ea17c 100644 --- a/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee +++ b/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee @@ -35,11 +35,26 @@ describe "ProxyManager", -> assertCalledWith(@router.get, '/foo/bar') assertCalledWith(@router.get, '/foo/:id') + it 'applies methods other than get', -> + @router = + post: sinon.stub() + put: sinon.stub() + @settings.proxyUrls = + '/foo/bar': {options: {method: 'post'}} + '/foo/:id': {options: {method: 'put'}} + @proxyManager.apply(@router) + sinon.assert.calledOnce(@router.post) + sinon.assert.calledOnce(@router.put) + assertCalledWith(@router.post, '/foo/bar') + assertCalledWith(@router.put, '/foo/:id') + describe 'createProxy', -> beforeEach -> @req.url = @proxyPath @req.route.path = @proxyPath @req.query = {} + @req.params = {} + @req.headers = {} @settings.proxyUrls = {} afterEach -> @@ -57,7 +72,7 @@ describe "ProxyManager", -> targetUrl = 'https://user:pass@foo.bar:123/pa/th.ext?query#hash' @settings.proxyUrls[@proxyPath] = targetUrl @proxyManager.createProxy(targetUrl)(@req) - assertCalledWith(@request, targetUrl) + assertCalledWith(@request, {url: targetUrl}) it 'overwrite query', -> targetUrl = 'foo.bar/baz?query' @@ -65,19 +80,19 @@ describe "ProxyManager", -> @settings.proxyUrls[@proxyPath] = targetUrl @proxyManager.createProxy(targetUrl)(@req) newTargetUrl = 'foo.bar/baz?requestQuery=important' - assertCalledWith(@request, newTargetUrl) + assertCalledWith(@request, {url: newTargetUrl}) it 'handles target objects', -> target = { baseUrl: 'api.v1', path: '/pa/th'} @settings.proxyUrls[@proxyPath] = target @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, 'api.v1/pa/th') + assertCalledWith(@request, {url: 'api.v1/pa/th'}) it 'handles missing baseUrl', -> target = { path: '/pa/th'} @settings.proxyUrls[@proxyPath] = target @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, 'undefined/pa/th') + assertCalledWith(@request, {url: 'undefined/pa/th'}) it 'handles dynamic path', -> target = baseUrl: 'api.v1', path: (params) -> "/resource/#{params.id}" @@ -86,4 +101,49 @@ describe "ProxyManager", -> @req.route.path = '/res/:id' @req.params = id: 123 @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, 'api.v1/resource/123') + assertCalledWith(@request, {url: 'api.v1/resource/123'}) + + it 'set arbitrary options on request', -> + target = baseUrl: 'api.v1', path: '/foo', options: foo: 'bar' + @req.url = '/foo' + @req.route.path = '/foo' + @proxyManager.createProxy(target)(@req, @res, @next) + assertCalledWith(@request, + foo: 'bar' + url: 'api.v1/foo' + ) + + it 'passes cookies', -> + target = baseUrl: 'api.v1', path: '/foo' + @req.url = '/foo' + @req.route.path = '/foo' + @req.headers = cookie: 'cookie' + @proxyManager.createProxy(target)(@req, @res, @next) + assertCalledWith(@request, + headers: Cookie: 'cookie' + url: 'api.v1/foo' + ) + + it 'passes body for post', -> + target = baseUrl: 'api.v1', path: '/foo', options: method: 'post' + @req.url = '/foo' + @req.route.path = '/foo' + @req.body = foo: 'bar' + @proxyManager.createProxy(target)(@req, @res, @next) + assertCalledWith(@request, + form: foo: 'bar' + method: 'post' + url: 'api.v1/foo' + ) + + it 'passes body for put', -> + target = baseUrl: 'api.v1', path: '/foo', options: method: 'put' + @req.url = '/foo' + @req.route.path = '/foo' + @req.body = foo: 'bar' + @proxyManager.createProxy(target)(@req, @res, @next) + assertCalledWith(@request, + form: foo: 'bar' + method: 'put' + url: 'api.v1/foo' + ) \ No newline at end of file From 66e288864ba162f852348b7038a7ca35950f80c1 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Tue, 11 Sep 2018 07:46:38 -0500 Subject: [PATCH 24/33] Wiki and CMS images height and width --- services/web/public/stylesheets/app/cms-page.less | 4 ++++ services/web/public/stylesheets/app/wiki.less | 1 + 2 files changed, 5 insertions(+) diff --git a/services/web/public/stylesheets/app/cms-page.less b/services/web/public/stylesheets/app/cms-page.less index f6cdf734d4..e6a3778cda 100644 --- a/services/web/public/stylesheets/app/cms-page.less +++ b/services/web/public/stylesheets/app/cms-page.less @@ -4,6 +4,10 @@ including About and Blog */ .cms-page { + img { + height: auto; + max-width: 100%; + } .btn-description { margin-right: @margin-sm; } diff --git a/services/web/public/stylesheets/app/wiki.less b/services/web/public/stylesheets/app/wiki.less index 29ccf505e6..fabdaa232f 100644 --- a/services/web/public/stylesheets/app/wiki.less +++ b/services/web/public/stylesheets/app/wiki.less @@ -15,6 +15,7 @@ } img { + height: auto; max-width: 100%; } From 6d54e843e86a36a4a6a38302a18e92d5cf3acff0 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 12 Sep 2018 10:27:15 +0100 Subject: [PATCH 25/33] fix typo in Errors, only two underscores in __proto__ --- .../web/app/coffee/Features/Errors/Errors.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index f33f3e523e..94aeaa2a90 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -31,56 +31,56 @@ UnsupportedFileTypeError = (message) -> error.name = "UnsupportedFileTypeError" error.__proto__ = UnsupportedFileTypeError.prototype return error -UnsupportedFileTypeError.prototype.__proto___ = Error.prototype +UnsupportedFileTypeError.prototype.__proto__ = Error.prototype UnsupportedBrandError = (message) -> error = new Error(message) error.name = "UnsupportedBrandError" error.__proto__ = UnsupportedBrandError.prototype return error -UnsupportedBrandError.prototype.__proto___ = Error.prototype +UnsupportedBrandError.prototype.__proto__ = Error.prototype UnsupportedExportRecordsError = (message) -> error = new Error(message) error.name = "UnsupportedExportRecordsError" error.__proto__ = UnsupportedExportRecordsError.prototype return error -UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype +UnsupportedExportRecordsError.prototype.__proto__ = Error.prototype V1HistoryNotSyncedError = (message) -> error = new Error(message) error.name = "V1HistoryNotSyncedError" error.__proto__ = V1HistoryNotSyncedError.prototype return error -V1HistoryNotSyncedError.prototype.__proto___ = Error.prototype +V1HistoryNotSyncedError.prototype.__proto__ = Error.prototype ProjectHistoryDisabledError = (message) -> error = new Error(message) error.name = "ProjectHistoryDisabledError" error.__proto__ = ProjectHistoryDisabledError.prototype return error -ProjectHistoryDisabledError.prototype.__proto___ = Error.prototype +ProjectHistoryDisabledError.prototype.__proto__ = Error.prototype V1ConnectionError = (message) -> error = new Error(message) error.name = "V1ConnectionError" error.__proto__ = V1ConnectionError.prototype return error -V1ConnectionError.prototype.__proto___ = Error.prototype +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 +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 +EmailExistsError.prototype.__proto__ = Error.prototype module.exports = Errors = NotFoundError: NotFoundError From 75a6b8f32b0dda579151b2d0756613132e8b9bc1 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 12 Sep 2018 08:54:09 -0500 Subject: [PATCH 26/33] Move custom tab styling to file Custom tab styling was nested within a portal class. This styling is also needed on the CMS pages. --- .../web/public/stylesheets/app/portals.less | 42 ------------------ .../public/stylesheets/components/tabs.less | 43 +++++++++++++++++++ services/web/public/stylesheets/ol-style.less | 1 + 3 files changed, 44 insertions(+), 42 deletions(-) create mode 100644 services/web/public/stylesheets/components/tabs.less diff --git a/services/web/public/stylesheets/app/portals.less b/services/web/public/stylesheets/app/portals.less index ae6fcdf230..5e987f9d58 100644 --- a/services/web/public/stylesheets/app/portals.less +++ b/services/web/public/stylesheets/app/portals.less @@ -100,52 +100,10 @@ } // 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; diff --git a/services/web/public/stylesheets/components/tabs.less b/services/web/public/stylesheets/components/tabs.less new file mode 100644 index 0000000000..3fa4fb106c --- /dev/null +++ b/services/web/public/stylesheets/components/tabs.less @@ -0,0 +1,43 @@ +.ol-tabs { + // Overrides for nav.less + .nav-tabs { + 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!important; + border-bottom: 1px solid @accent-color-secondary!important; + color: @accent-color-secondary!important; + &:hover { + color: @accent-color-secondary!important; + } + } + .tab-content:extend(.container) { + background-color: transparent!important; + border: none!important; + } +} + + \ No newline at end of file diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less index be8a97ea98..88c6953509 100644 --- a/services/web/public/stylesheets/ol-style.less +++ b/services/web/public/stylesheets/ol-style.less @@ -10,6 +10,7 @@ @import "components/icons.less"; @import "components/navs-ol.less"; @import "components/pagination.less"; +@import "components/tabs.less"; // Pages @import "app/about.less"; From ab10336110c136590815fac6345972125d08f331 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 10 Sep 2018 11:18:15 +0100 Subject: [PATCH 27/33] Record last update time and user from project-history --- .../Features/History/HistoryController.coffee | 9 ++++ .../Features/Project/ProjectController.coffee | 7 +++- .../Project/ProjectEntityUpdateHandler.coffee | 2 - .../Project/ProjectUpdateHandler.coffee | 23 ++++------ services/web/app/coffee/models/Project.coffee | 1 + services/web/app/coffee/router.coffee | 1 + .../coffee/ProjectLastUpdatedTests.coffee | 42 +++++++++++++++++++ .../History/HistoryControllerTests.coffee | 1 + .../ProjectEntityUpdateHandlerTests.coffee | 5 --- .../Project/ProjectUpdateHandlerTests.coffee | 15 ++++--- 10 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 services/web/test/acceptance/coffee/ProjectLastUpdatedTests.coffee diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee index 189e07b0f5..8b014751f9 100644 --- a/services/web/app/coffee/Features/History/HistoryController.coffee +++ b/services/web/app/coffee/Features/History/HistoryController.coffee @@ -7,6 +7,7 @@ HistoryManager = require "./HistoryManager" ProjectDetailsHandler = require "../Project/ProjectDetailsHandler" ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" RestoreManager = require "./RestoreManager" +ProjectUpdateHandler = require "../Project/ProjectUpdateHandler" module.exports = HistoryController = selectHistoryApi: (req, res, next = (error) ->) -> @@ -143,3 +144,11 @@ module.exports = HistoryController = 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) + + setLastUpdated: (req, res, next) -> + {project_id} = req.params + {user_id, timestamp} = req.body + logger.log {project_id, user_id, timestamp}, 'updating last updated date' + ProjectUpdateHandler.markAsUpdated project_id, user_id, timestamp, (error) -> + return next(error) if error? + res.sendStatus 200 diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 59c0647c19..ed5e219e3f 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -184,7 +184,7 @@ module.exports = ProjectController = notifications: (cb)-> NotificationsHandler.getUserNotifications user_id, cb projects: (cb)-> - ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref tokens', cb + ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated lastUpdatedBy publicAccesLevel archived owner_ref tokens', cb v1Projects: (cb) -> Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) -> if error? and error instanceof V1ConnectionError @@ -392,6 +392,7 @@ module.exports = ProjectController = id: project._id name: project.name lastUpdated: project.lastUpdated + lastUpdatedBy: project.lastUpdatedBy publicAccessLevel: project.publicAccesLevel accessLevel: accessLevel source: source @@ -430,6 +431,8 @@ module.exports = ProjectController = for project in projects if project.owner_ref? users[project.owner_ref.toString()] = true + if project.lastUpdatedBy? + users[project.lastUpdatedBy] = true jobs = [] for user_id, _ of users @@ -444,6 +447,8 @@ module.exports = ProjectController = for project in projects if project.owner_ref? project.owner = users[project.owner_ref.toString()] + if project.lastUpdatedBy? + project.lastUpdatedBy = users[project.lastUpdatedBy.toString()] callback null, projects _buildWarningsList: (v1ProjectData = {}) -> diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index b7be80cb47..4800de05d4 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -114,8 +114,6 @@ module.exports = ProjectEntityUpdateHandler = self = logger.log {project_id, doc_id, modified}, "finished updating doc lines" # path will only be present if the doc is not deleted if modified && !isDeletedDoc - # Don't need to block for marking as updated - ProjectUpdateHandler.markAsUpdated project_id TpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, doc_id:doc_id, project_name:project.name, rev:rev}, callback else callback() diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee index ba3b3b6295..d89d7c7e89 100644 --- a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee @@ -2,30 +2,25 @@ Project = require('../../models/Project').Project logger = require('logger-sharelatex') module.exports = - markAsUpdated : (project_id, callback)-> + markAsUpdated : (project_id, user_id, timestamp, callback)-> conditions = {_id:project_id} - update = {lastUpdated:Date.now()} - Project.update conditions, update, {}, (err)-> - if callback? - callback() + update = { + lastUpdated: new Date(timestamp), + lastUpdatedBy: user_id + } + Project.update conditions, update, {}, callback markAsOpened : (project_id, callback)-> conditions = {_id:project_id} update = {lastOpened:Date.now()} - Project.update conditions, update, {}, (err)-> - if callback? - callback() + Project.update conditions, update, {}, callback markAsInactive: (project_id, callback)-> conditions = {_id:project_id} update = {active:false} - Project.update conditions, update, {}, (err)-> - if callback? - callback() + Project.update conditions, update, {}, callback markAsActive: (project_id, callback)-> conditions = {_id:project_id} update = {active:true} - Project.update conditions, update, {}, (err)-> - if callback? - callback() \ No newline at end of file + Project.update conditions, update, {}, callback diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 00c2f1be52..f4b478d64e 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -19,6 +19,7 @@ DeletedFileSchema = new Schema ProjectSchema = new Schema name : {type:String, default:'new project'} lastUpdated : {type:Date, default: () -> new Date()} + lastUpdatedBy : {type:ObjectId, ref: 'User'} lastOpened : {type:Date} active : { type: Boolean, default: true } owner_ref : {type:ObjectId, ref:'User'} diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index b196c56870..2413632581 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -238,6 +238,7 @@ module.exports = class Router webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory + privateApiRouter.post "/project/:project_id/last_updated", AuthenticationController.httpAuth, HistoryController.setLastUpdated 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 diff --git a/services/web/test/acceptance/coffee/ProjectLastUpdatedTests.coffee b/services/web/test/acceptance/coffee/ProjectLastUpdatedTests.coffee new file mode 100644 index 0000000000..5798d0236c --- /dev/null +++ b/services/web/test/acceptance/coffee/ProjectLastUpdatedTests.coffee @@ -0,0 +1,42 @@ +expect = require("chai").expect +async = require("async") +User = require "./helpers/User" +request = require "./helpers/request" +settings = require "settings-sharelatex" +Project = require("../../../app/js/models/Project").Project + +markAsUpdated = (project_id, user_id, timestamp, callback) -> + request.post { + url: "/project/#{project_id}/last_updated" + json: { + user_id, + timestamp + } + auth: + user: settings.apis.web.user + pass: settings.apis.web.pass + sendImmediately: true + jar: false + }, callback + +describe "ProjectLastUpdated", -> + before (done) -> + @timeout(90000) + @owner = new User() + @timestamp = Date.now() + @user_id = "abcdef1234567890abcdef12" + async.series [ + (cb) => @owner.login cb + (cb) => @owner.createProject "private-project", (error, @project_id) => cb(error) + ], done + + describe "with user_id and timestamp", -> + it 'should update the project', (done) -> + markAsUpdated @project_id, @user_id, @timestamp, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 200 + Project.findOne _id: @project_id, (error, project) => + return done(error) if error? + expect(project.lastUpdated.getTime()).to.equal @timestamp + expect(project.lastUpdatedBy.toString()).to.equal @user_id + done() diff --git a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee index d5777f5490..4e29bbae90 100644 --- a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee +++ b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee @@ -23,6 +23,7 @@ describe "HistoryController", -> "../Project/ProjectDetailsHandler": @ProjectDetailsHandler = {} "../Project/ProjectEntityUpdateHandler": @ProjectEntityUpdateHandler = {} "./RestoreManager": @RestoreManager = {} + "../Project/ProjectUpdateHandler": @ProjectUpdateHandler = {} @settings.apis = trackchanges: enabled: false diff --git a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee index 3de4546e6d..1b4739000f 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee @@ -190,11 +190,6 @@ describe 'ProjectEntityUpdateHandler', -> .calledWith(project_id, doc_id, @docLines, @version, @ranges) .should.equal true - it "should mark the project as updated", -> - @ProjectUpdater.markAsUpdated - .calledWith(project_id) - .should.equal true - it "should send the doc the to the TPDS", -> @TpdsUpdateSender.addDoc .calledWith({ diff --git a/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee index a68a9be1f1..da719c91f2 100644 --- a/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee @@ -16,12 +16,15 @@ describe 'ProjectUpdateHandler', -> describe 'marking a project as recently updated', -> it 'should send an update to mongo', (done)-> project_id = "project_id" - @handler.markAsUpdated project_id, (err)=> - args = @ProjectModel.update.args[0] - args[0]._id.should.equal project_id - date = args[1].lastUpdated+"" - now = Date.now()+"" - date.substring(0,5).should.equal now.substring(0,5) + user_id = "mock_user_id" + timestamp = Date.now() + @handler.markAsUpdated project_id, user_id, timestamp, (err)=> + @ProjectModel.update.calledWith({ + _id: project_id, + }, { + lastUpdated: new Date(timestamp), + lastUpdatedBy: user_id + }).should.equal true done() describe "markAsOpened", -> From c0729611832aa86550f370b0f3deeb20d89dd137 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 10 Sep 2018 11:18:44 +0100 Subject: [PATCH 28/33] Update project-list list to a table and show last updated user --- services/web/app/views/project/list/item.pug | 149 ++++++++---------- .../app/views/project/list/project-list.pug | 82 +++++----- .../web/app/views/project/list/v1-item.pug | 2 +- .../main/project-list/project-list.coffee | 8 +- .../public/stylesheets/app/project-list.less | 37 ++++- .../public/stylesheets/components/tables.less | 2 +- .../stylesheets/core/_common-variables.less | 2 +- 7 files changed, 144 insertions(+), 138 deletions(-) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index deb9863d04..2e80810be4 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -1,7 +1,4 @@ -- var titleClasses = settings.overleaf ? "col-xs-6 col-sm-4 col-md-6" : "col-xs-6" -- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4" - -div(class=titleClasses) +td.selectProject input.select-item( select-individual, type="checkbox", @@ -10,29 +7,29 @@ div(class=titleClasses) stop-propagation="click" aria-label=translate('select_project') + " '{{ project.name }}'" ) - span - a.projectName( - ng-href="{{projectLink(project)}}" +td.projectName + a.projectName( + ng-href="{{projectLink(project)}}" + stop-propagation="click" + ) {{project.name}} + span( + ng-controller="TagListController" + ) + .tag-label( + ng-repeat='tag in project.tags' stop-propagation="click" - ) {{project.name}} - span( - ng-controller="TagListController" ) - .tag-label( - ng-repeat='tag in project.tags' - stop-propagation="click" - ) - a.label.label-default.tag-label-name( - href, - ng-click="selectTag(tag)" - ) {{tag.name}} - a.label.label-default.tag-label-remove( - href - ng-click="removeProjectFromTag(project, tag)" - ) × + a.label.label-default.tag-label-name( + href, + ng-click="selectTag(tag)" + ) {{tag.name}} + a.label.label-default.tag-label-remove( + href + ng-click="removeProjectFromTag(project, tag)" + ) × -.col-xs-2 - span.owner {{ownerName()}} +td + span.owner {{userDisplayName(project.owner)}} span(ng-if="isLinkSharingProject(project)") |   i.fa.fa-link.small( @@ -41,64 +38,52 @@ div(class=titleClasses) tooltip-append-to-body="true" ) -div(class=lastUpdatedClasses) - if settings.overleaf - span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} - else - span.last-modified {{project.lastUpdated | formatDate}} +td + span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") + | {{project.lastUpdated | fromNowDate}} + span(ng-show='project.lastUpdatedBy') + | + | by {{userDisplayName(project.lastUpdatedBy)}} -if settings.overleaf - .hidden-xs.col-sm-3.col-md-2.action-btn-row - div( - ng-if="!project.isTableActionInflight" +td.text-right + div( + ng-if="!project.isTableActionInflight" + ) + button.btn.btn-link.action-btn( + tooltip=translate('copy'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="clone($event)" ) - button.btn.btn-link.action-btn( - tooltip=translate('copy'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="clone($event)" - ) - i.icon.fa.fa-files-o - button.btn.btn-link.action-btn( - tooltip=translate('download'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="download($event)" - ) - i.icon.fa.fa-cloud-download - button.btn.btn-link.action-btn( - ng-if="!project.archived && isOwner()" - tooltip=translate('archive'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="archiveOrLeave($event)" - ) - i.icon.fa.fa-inbox - button.btn.btn-link.action-btn( - ng-if="!project.archived && !isOwner()" - tooltip=translate('leave'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="archiveOrLeave($event)" - ) - i.icon.fa.fa-sign-out - button.btn.btn-link.action-btn( - ng-if="project.archived" - tooltip=translate('unarchive'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="restore($event)" - ) - i.icon.fa.fa-reply - button.btn.btn-link.action-btn( - ng-if="project.archived && isOwner()" - tooltip=translate('delete_forever'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="deleteProject($event)" - ) - i.icon.fa.fa-trash - div( - ng-if="project.isTableActionInflight" + i.icon.fa.fa-files-o + button.btn.btn-link.action-btn( + tooltip=translate('download'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="download($event)" ) - i.fa.fa-spinner.fa-spin \ No newline at end of file + i.icon.fa.fa-cloud-download + button.btn.btn-link.action-btn( + ng-if="!project.archived && isOwner()" + tooltip=translate('archive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="archiveOrLeave($event)" + ) + i.icon.fa.fa-inbox + button.btn.btn-link.action-btn( + ng-if="!project.archived && !isOwner()" + tooltip=translate('leave'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="archiveOrLeave($event)" + ) + i.icon.fa.fa-sign-out + button.btn.btn-link.action-btn( + ng-if="project.archived" + tooltip=translate('unarchive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="restore($event)" + ) + i.icon.fa.fa-reply diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index cfea4aa6c6..af3a569f3b 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -123,54 +123,44 @@ .col-xs-12 .card.card-thin.project-list-card - ul.list-unstyled.project-list.structured-list( - select-all-list, - ng-if="projects.length > 0", - max-height="projectListHeight - 25", - ng-cloak - ) - li.container-fluid - .row - - var titleClasses = settings.overleaf ? " col-xs-6 col-sm-4 col-md-6" : "col-xs-6" - - var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4" - - div(class=titleClasses) - input.select-all( - select-all, - type="checkbox" - aria-label=translate('select_all_projects') - ) - span.header.clickable(ng-click="changePredicate('name')") #{translate("title")} - i.tablesort.fa(ng-class="getSortIconClass('name')") - .col-xs-2 - span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} - i.tablesort.fa(ng-class="getSortIconClass('accessLevel')") - div(class=lastUpdatedClasses) - span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} - i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") - if settings.overleaf - .hidden-xs.col-sm-3.col-md-2.action-btn-row-header - span.header #{translate("actions")} - li.project_entry.container-fluid( - ng-repeat="project in visibleProjects | orderBy:predicate:reverse", - ng-controller="ProjectListItemController" - ) - .row( - ng-if="!project.isV1Project" - select-row - ) - include ./item - .row( - ng-if="project.isV1Project" - ) - include ./v1-item - li( - ng-if="visibleProjects.length == 0", + .table-wrapper(max-height="projectListHeight - 25",) + table.table.table-hover.project-list( + select-all-list, + ng-if="projects.length > 0", ng-cloak ) - .row - .col-xs-12.text-centered - small #{translate("no_projects")} + thead + tr + th.selectProject + input.select-all( + select-all, + type="checkbox" + aria-label=translate('select_all_projects') + ) + th.projectName + span.header.clickable(ng-click="changePredicate('name')") #{translate("title")} + i.tablesort.fa(ng-class="getSortIconClass('name')") + th + span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} + i.tablesort.fa(ng-class="getSortIconClass('accessLevel')") + th + span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} + i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") + if settings.overleaf + th.text-right + span.header #{translate("actions")} + tbody + tr.project_entry( + ng-repeat="project in visibleProjects | orderBy:predicate:reverse", + ng-controller="ProjectListItemController" + ) + include ./item + tr( + ng-if="visibleProjects.length == 0", + ng-cloak + ) + td(colspan=5).text-center + small #{translate("no_projects")} div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak) h2 #{translate("welcome_to_sl")} diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug index 5a8e37bca0..bf8d7db9e9 100644 --- a/services/web/app/views/project/list/v1-item.pug +++ b/services/web/app/views/project/list/v1-item.pug @@ -19,7 +19,7 @@ ) {{project.name}} .col-xs-2 - span.owner {{ownerName()}} + span.owner {{userDisplayName(project.owner)}} .col-xs-4.col-sm-3.col-md-2 span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} \ No newline at end of file diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 9a8d7f7a73..a6a4d48ef1 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -485,11 +485,11 @@ define [ $scope.isLinkSharingProject = (project) -> return project.source == 'token' - $scope.ownerName = () -> - if $scope.project.accessLevel == "owner" + $scope.userDisplayName = (user) -> + if user._id == window.user_id return "You" - else if $scope.project.owner? - return [$scope.project.owner.first_name, $scope.project.owner.last_name].filter((n) -> n?).join(" ") + else if user? + return [user.first_name, user.last_name].filter((n) -> n?).join(" ") else return "None" diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index e5bc8da14c..34d3e25c75 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -314,8 +314,38 @@ ul.structured-list { padding: 0 (@line-height-computed / 4); } -ul.project-list { - li { +.table-wrapper { + overflow: scroll; +} + +table.project-list { + margin: 0; + width: 100%; + + th when (@is-overleaf = true) { + font-weight: 600; + } + th when (@is-overleaf = false) { + text-transform: uppercase; + font-weight: normal; + } + + // thead > tr > th { + // padding-top: @line-height-computed / 8; + // } + + td { + @media (min-width: @screen-md-min) { + white-space: nowrap; + } + &.projectName { + white-space: normal; + width: 50%; + } + &.selectProject { + width: 1%; + } + .last-modified when (@is-overleaf = false) { font-size: .8rem; } @@ -325,12 +355,13 @@ ul.project-list { .owner when (@is-overleaf = false) { margin-right: 0; } - .projectName { + a.projectName { margin-right: @line-height-computed / 4; padding: 0; vertical-align: inherit; white-space: normal; text-align: left; + color: @structured-list-link-color; } .tag-label { diff --git a/services/web/public/stylesheets/components/tables.less b/services/web/public/stylesheets/components/tables.less index c41989c04d..30ede6697c 100755 --- a/services/web/public/stylesheets/components/tables.less +++ b/services/web/public/stylesheets/components/tables.less @@ -34,7 +34,7 @@ th { // Bottom align for column headings > thead > tr > th { vertical-align: bottom; - border-bottom: 2px solid @table-border-color; + border-bottom: 1px solid @table-border-color; } // Remove top border from thead by default > caption + thead, diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 9d7eab3493..41bdbd2d9b 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -110,7 +110,7 @@ //## Customizes the `.table` component with basic values, each used across all table variations. //** Padding for ``s and ``s. -@table-cell-padding: 8px; +@table-cell-padding: @line-height-computed / 4; //** Padding for cells in `.table-condensed`. @table-condensed-cell-padding: 5px; From 225d30ffd202b31a2ae092670526191f4c47c78f Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 11 Sep 2018 10:26:59 +0100 Subject: [PATCH 29/33] Add missed lines from rebase --- services/web/app/views/project/list/item.pug | 174 ++++----- .../app/views/project/list/project-list.pug | 330 +++++++++--------- .../web/app/views/project/list/v1-item.pug | 48 +-- 3 files changed, 288 insertions(+), 264 deletions(-) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index 2e80810be4..379f939915 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -1,89 +1,101 @@ td.selectProject - input.select-item( - select-individual, - type="checkbox", - ng-disabled="shouldDisableCheckbox(project)", - ng-model="project.selected" - stop-propagation="click" - aria-label=translate('select_project') + " '{{ project.name }}'" - ) + input.select-item( + select-individual, + type="checkbox", + ng-disabled="shouldDisableCheckbox(project)", + ng-model="project.selected" + stop-propagation="click" + aria-label=translate('select_project') + " '{{ project.name }}'" + ) td.projectName - a.projectName( - ng-href="{{projectLink(project)}}" - stop-propagation="click" - ) {{project.name}} - span( - ng-controller="TagListController" - ) - .tag-label( - ng-repeat='tag in project.tags' - stop-propagation="click" - ) - a.label.label-default.tag-label-name( - href, - ng-click="selectTag(tag)" - ) {{tag.name}} - a.label.label-default.tag-label-remove( - href - ng-click="removeProjectFromTag(project, tag)" - ) × + a.projectName( + ng-href="{{projectLink(project)}}" + stop-propagation="click" + ) {{project.name}} + span( + ng-controller="TagListController" + ) + .tag-label( + ng-repeat='tag in project.tags' + stop-propagation="click" + ) + a.label.label-default.tag-label-name( + href, + ng-click="selectTag(tag)" + ) {{tag.name}} + a.label.label-default.tag-label-remove( + href + ng-click="removeProjectFromTag(project, tag)" + ) × td - span.owner {{userDisplayName(project.owner)}} - span(ng-if="isLinkSharingProject(project)") - |   - i.fa.fa-link.small( - tooltip=translate("link_sharing") - tooltip-placement="right" - tooltip-append-to-body="true" - ) + span.owner {{userDisplayName(project.owner)}} + span(ng-if="isLinkSharingProject(project)") + |   + i.fa.fa-link.small( + tooltip=translate("link_sharing") + tooltip-placement="right" + tooltip-append-to-body="true" + ) td - span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") - | {{project.lastUpdated | fromNowDate}} - span(ng-show='project.lastUpdatedBy') - | - | by {{userDisplayName(project.lastUpdatedBy)}} + span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") + | {{project.lastUpdated | fromNowDate}} + span(ng-show='project.lastUpdatedBy') + | + | by {{userDisplayName(project.lastUpdatedBy)}} td.text-right - div( - ng-if="!project.isTableActionInflight" - ) - button.btn.btn-link.action-btn( - tooltip=translate('copy'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="clone($event)" - ) - i.icon.fa.fa-files-o - button.btn.btn-link.action-btn( - tooltip=translate('download'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="download($event)" - ) - i.icon.fa.fa-cloud-download - button.btn.btn-link.action-btn( - ng-if="!project.archived && isOwner()" - tooltip=translate('archive'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="archiveOrLeave($event)" - ) - i.icon.fa.fa-inbox - button.btn.btn-link.action-btn( - ng-if="!project.archived && !isOwner()" - tooltip=translate('leave'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="archiveOrLeave($event)" - ) - i.icon.fa.fa-sign-out - button.btn.btn-link.action-btn( - ng-if="project.archived" - tooltip=translate('unarchive'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="restore($event)" - ) - i.icon.fa.fa-reply + div( + ng-if="!project.isTableActionInflight" + ) + button.btn.btn-link.action-btn( + tooltip=translate('copy'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="clone($event)" + ) + i.icon.fa.fa-files-o + button.btn.btn-link.action-btn( + tooltip=translate('download'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="download($event)" + ) + i.icon.fa.fa-cloud-download + button.btn.btn-link.action-btn( + ng-if="!project.archived && isOwner()" + tooltip=translate('archive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="archiveOrLeave($event)" + ) + i.icon.fa.fa-inbox + button.btn.btn-link.action-btn( + ng-if="!project.archived && !isOwner()" + tooltip=translate('leave'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="archiveOrLeave($event)" + ) + i.icon.fa.fa-sign-out + button.btn.btn-link.action-btn( + ng-if="project.archived" + tooltip=translate('unarchive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="restore($event)" + ) + i.icon.fa.fa-reply + button.btn.btn-link.action-btn( + ng-if="project.archived && isOwner()" + tooltip=translate('delete_forever'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="deleteProject($event)" + ) + i.icon.fa.fa-trash + div( + ng-if="project.isTableActionInflight" + ) + i.fa.fa-spinner.fa-spin diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index af3a569f3b..e261795fea 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -1,175 +1,183 @@ .row - .col-xs-12(ng-cloak) + .col-xs-12(ng-cloak) - form.project-search.form-horizontal(role="form") - .form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12 - input.form-control.col-md-7.col-xs-12( - placeholder=translate('search_projects')+"…", - aria-label=translate('search_projects')+"…", - autofocus='autofocus', - ng-model="searchText.value", - focus-on='search:clear', - ng-keyup="searchProjects()" - ) - i.fa.fa-search.form-control-feedback-left - i.fa.fa-times.form-control-feedback( - ng-click="clearSearchText()", - style="cursor: pointer;", - ng-show="searchText.value.length > 0" - ) - //- i.fa.fa-remove + form.project-search.form-horizontal(role="form") + .form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12 + input.form-control.col-md-7.col-xs-12( + placeholder=translate('search_projects')+"…", + aria-label=translate('search_projects')+"…", + autofocus='autofocus', + ng-model="searchText.value", + focus-on='search:clear', + ng-keyup="searchProjects()" + ) + i.fa.fa-search.form-control-feedback-left + i.fa.fa-times.form-control-feedback( + ng-click="clearSearchText()", + style="cursor: pointer;", + ng-show="searchText.value.length > 0" + ) + //- i.fa.fa-remove - .project-tools(ng-cloak) - .btn-toolbar(ng-show="filter != 'archived'") - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-default( - href, - tooltip=translate('download'), - tooltip-placement="bottom", - tooltip-append-to-body="true", - ng-click="downloadSelectedProjects()" - ) - i.fa.fa-cloud-download - - var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete") - - var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o" - a.btn.btn-default( - href, - tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`, - tooltip-placement="bottom", - tooltip-append-to-body="true", - ng-click="openArchiveProjectsModal()" - ) - i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`) + .project-tools(ng-cloak) + .btn-toolbar(ng-show="filter != 'archived'") + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-default( + href, + tooltip=translate('download'), + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="downloadSelectedProjects()" + ) + i.fa.fa-cloud-download + - var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete") + - var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o" + a.btn.btn-default( + href, + tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`, + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="openArchiveProjectsModal()" + ) + i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`) - .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) - a.btn.btn-default.dropdown-toggle( - href, - data-toggle="dropdown", - dropdown-toggle, - tooltip=translate('add_to_folders'), - tooltip-append-to-body="true", - tooltip-placement="bottom" - ) - i.fa.fa-folder-open-o - | - span.caret - ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu.tags-dropdown-menu( - role="menu" - ng-controller="TagListController" - ) - li.dropdown-header #{translate("add_to_folder")} - li( - ng-repeat="tag in tags | orderBy:'name'", - ng-controller="TagDropdownItemController" - ng-if="!tag.isV1" - ) - a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") - i.fa( - ng-class="{\ - 'fa-check-square-o': areSelectedProjectsInTag == true,\ - 'fa-square-o': areSelectedProjectsInTag == false,\ - 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\ - }" - ) - | {{tag.name}} - li.divider - li - a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} + .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) + a.btn.btn-default.dropdown-toggle( + href, + data-toggle="dropdown", + dropdown-toggle, + tooltip=translate('add_to_folders'), + tooltip-append-to-body="true", + tooltip-placement="bottom" + ) + i.fa.fa-folder-open-o + | + span.caret + ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu.tags-dropdown-menu( + role="menu" + ng-controller="TagListController" + ) + li.dropdown-header #{translate("add_to_folder")} + li( + ng-repeat="tag in tags | orderBy:'name'", + ng-controller="TagDropdownItemController" + ng-if="!tag.isV1" + ) + a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") + i.fa( + ng-class="{\ + 'fa-check-square-o': areSelectedProjectsInTag == true,\ + 'fa-square-o': areSelectedProjectsInTag == false,\ + 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\ + }" + ) + | {{tag.name}} + li.divider + li + a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} - .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown - a.btn.btn-default.dropdown-toggle( - href, - data-toggle="dropdown", - dropdown-toggle - ) #{translate("more")} - span.caret - ul.dropdown-menu.dropdown-menu-right(role="menu") - li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") - a( - href, - ng-click="openRenameProjectModal()" - ) #{translate("rename")} - li - a( - href, - ng-click="openCloneProjectModal()" - ) #{translate("make_copy")} + .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown + a.btn.btn-default.dropdown-toggle( + href, + data-toggle="dropdown", + dropdown-toggle + ) #{translate("more")} + span.caret + ul.dropdown-menu.dropdown-menu-right(role="menu") + li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") + a( + href, + ng-click="openRenameProjectModal()" + ) #{translate("rename")} + li + a( + href, + ng-click="openCloneProjectModal()" + ) #{translate("make_copy")} - .btn-toolbar(ng-show="filter == 'archived'") - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-default( - href, - data-original-title="Restore", - data-toggle="tooltip", - data-placement="bottom", - ng-click="restoreSelectedProjects()" - ) #{translate("restore")} + .btn-toolbar(ng-show="filter == 'archived'") + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-default( + href, + data-original-title="Restore", + data-toggle="tooltip", + data-placement="bottom", + ng-click="restoreSelectedProjects()" + ) #{translate("restore")} - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-danger( - href, - data-original-title="Delete Forever", - data-toggle="tooltip", - data-placement="bottom", - ng-click="openDeleteProjectsModal()" - ) #{translate("delete_forever")} + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-danger( + href, + data-original-title="Delete Forever", + data-toggle="tooltip", + data-placement="bottom", + ng-click="openDeleteProjectsModal()" + ) #{translate("delete_forever")} .row.row-spaced - each warning in warnings - .col-xs-12 - .alert.alert-warning(role="alert")= warning + each warning in warnings + .col-xs-12 + .alert.alert-warning(role="alert")= warning - .col-xs-12 - .card.card-thin.project-list-card - .table-wrapper(max-height="projectListHeight - 25",) - table.table.table-hover.project-list( - select-all-list, - ng-if="projects.length > 0", - ng-cloak - ) - thead - tr - th.selectProject - input.select-all( - select-all, - type="checkbox" - aria-label=translate('select_all_projects') - ) - th.projectName - span.header.clickable(ng-click="changePredicate('name')") #{translate("title")} - i.tablesort.fa(ng-class="getSortIconClass('name')") - th - span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} - i.tablesort.fa(ng-class="getSortIconClass('accessLevel')") - th - span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} - i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") - if settings.overleaf - th.text-right - span.header #{translate("actions")} - tbody - tr.project_entry( - ng-repeat="project in visibleProjects | orderBy:predicate:reverse", - ng-controller="ProjectListItemController" - ) - include ./item - tr( - ng-if="visibleProjects.length == 0", - ng-cloak - ) - td(colspan=5).text-center - small #{translate("no_projects")} + .col-xs-12 + .card.card-thin.project-list-card + .table-wrapper(max-height="projectListHeight - 25",) + table.table.table-hover.project-list( + select-all-list, + ng-if="projects.length > 0", + ng-cloak + ) + thead + tr + th.selectProject + input.select-all( + select-all, + type="checkbox" + aria-label=translate('select_all_projects') + ) + th.projectName + span.header.clickable(ng-click="changePredicate('name')") #{translate("title")} + i.tablesort.fa(ng-class="getSortIconClass('name')") + th + span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} + i.tablesort.fa(ng-class="getSortIconClass('accessLevel')") + th + span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} + i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") + if settings.overleaf + th.text-right + span.header #{translate("actions")} + tbody + tr.project_entry( + ng-repeat="project in visibleProjects | orderBy:predicate:reverse", + ng-controller="ProjectListItemController" + ) + .row( + ng-if="!project.isV1Project" + select-row + ) + include ./item + .row( + ng-if="project.isV1Project" + ) + include ./v1-item + tr( + ng-if="visibleProjects.length == 0", + ng-cloak + ) + td(colspan=5).text-center + small #{translate("no_projects")} + + div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak) + h2 #{translate("welcome_to_sl")} + p #{translate("new_to_latex_look_at")} + a(href="/templates") #{translate("templates").toLowerCase()} + | #{translate("or")} + a(href="/learn") #{translate("latex_help_guide")} + | , + br + | #{translate("or_create_project_left")} - div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak) - h2 #{translate("welcome_to_sl")} - p #{translate("new_to_latex_look_at")} - a(href="/templates") #{translate("templates").toLowerCase()} - | #{translate("or")} - a(href="/learn") #{translate("latex_help_guide")} - | , - br - | #{translate("or_create_project_left")} - diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug index bf8d7db9e9..a74c48980f 100644 --- a/services/web/app/views/project/list/v1-item.pug +++ b/services/web/app/views/project/list/v1-item.pug @@ -1,25 +1,29 @@ -.col-xs-6.col-sm-4.col-md-6 - .select-item - span.v1-badge( - aria-label=translate("v1_badge") - tooltip-template="'v1ProjectTooltipTemplate'" - tooltip-append-to-body="true" - ) - span - if settings.overleaf && settings.overleaf.host - button.btn.btn-link.projectName( - ng-click="openV1ImportModal(project)" - stop-propagation="click" - ng-show="project.accessLevel == 'owner'" - ) {{project.name}} - a.projectName( - href=settings.overleaf.host + "/{{project.id}}" - target="_blank" - ng-hide="project.accessLevel == 'owner'" - ) {{project.name}} +td -.col-xs-2 +td.selectProject + span.v1-badge( + aria-label=translate("v1_badge") + tooltip-template="'v1ProjectTooltipTemplate'" + tooltip-append-to-body="true" + ) + +td.projectName + if settings.overleaf && settings.overleaf.host + button.btn.btn-link.projectName( + ng-click="openV1ImportModal(project)" + stop-propagation="click" + ng-show="project.accessLevel == 'owner'" + ) {{project.name}} + a.projectName( + href=settings.overleaf.host + "/{{project.id}}" + target="_blank" + ng-hide="project.accessLevel == 'owner'" + ) {{project.name}} + +td span.owner {{userDisplayName(project.owner)}} -.col-xs-4.col-sm-3.col-md-2 - span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} \ No newline at end of file +td + span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} + +td From dd4b85b809051c20e67ed03f7d78c221dc127861 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 11 Sep 2018 11:12:46 +0100 Subject: [PATCH 30/33] Combine v1 and v2 projects into item.pug --- services/web/app/views/project/list/item.pug | 58 +++++++++++++------ .../app/views/project/list/project-list.pug | 10 +--- .../web/app/views/project/list/v1-item.pug | 29 ---------- .../main/project-list/project-list.coffee | 16 ++--- .../public/stylesheets/app/project-list.less | 11 ++-- 5 files changed, 54 insertions(+), 70 deletions(-) delete mode 100644 services/web/app/views/project/list/v1-item.pug diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index 379f939915..0ab99893da 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -1,5 +1,6 @@ td.selectProject input.select-item( + ng-if="!project.isV1Project", select-individual, type="checkbox", ng-disabled="shouldDisableCheckbox(project)", @@ -7,26 +8,45 @@ td.selectProject stop-propagation="click" aria-label=translate('select_project') + " '{{ project.name }}'" ) -td.projectName - a.projectName( - ng-href="{{projectLink(project)}}" - stop-propagation="click" - ) {{project.name}} - span( - ng-controller="TagListController" + span.v1-badge( + ng-if="project.isV1Project", + aria-label=translate("v1_badge") + tooltip-template="'v1ProjectTooltipTemplate'" + tooltip-append-to-body="true" ) - .tag-label( - ng-repeat='tag in project.tags' +td.projectName + span(ng-if="project.isV1Project") + if settings.overleaf && settings.overleaf.host + button.btn.btn-link.projectName( + ng-click="openV1ImportModal(project)" + stop-propagation="click" + ng-show="project.accessLevel == 'owner'" + ) {{project.name}} + a.projectName( + href=settings.overleaf.host + "/{{project.id}}" + target="_blank" + ng-hide="project.accessLevel == 'owner'" + ) {{project.name}} + span(ng-if="!project.isV1Project") + a.projectName( + ng-href="{{projectLink(project)}}" stop-propagation="click" + ) {{project.name}} + span( + ng-controller="TagListController" ) - a.label.label-default.tag-label-name( - href, - ng-click="selectTag(tag)" - ) {{tag.name}} - a.label.label-default.tag-label-remove( - href - ng-click="removeProjectFromTag(project, tag)" - ) × + .tag-label( + ng-repeat='tag in project.tags' + stop-propagation="click" + ) + a.label.label-default.tag-label-name( + href, + ng-click="selectTag(tag)" + ) {{tag.name}} + a.label.label-default.tag-label-remove( + href + ng-click="removeProjectFromTag(project, tag)" + ) × td span.owner {{userDisplayName(project.owner)}} @@ -41,13 +61,13 @@ td td span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") | {{project.lastUpdated | fromNowDate}} - span(ng-show='project.lastUpdatedBy') + span(ng-if='project.lastUpdatedBy') | | by {{userDisplayName(project.lastUpdatedBy)}} td.text-right div( - ng-if="!project.isTableActionInflight" + ng-if="!project.isTableActionInflight && !project.isV1Project" ) button.btn.btn-link.action-btn( tooltip=translate('copy'), diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index e261795fea..a6971aa49a 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -154,15 +154,7 @@ ng-repeat="project in visibleProjects | orderBy:predicate:reverse", ng-controller="ProjectListItemController" ) - .row( - ng-if="!project.isV1Project" - select-row - ) - include ./item - .row( - ng-if="project.isV1Project" - ) - include ./v1-item + include ./item tr( ng-if="visibleProjects.length == 0", ng-cloak diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug deleted file mode 100644 index a74c48980f..0000000000 --- a/services/web/app/views/project/list/v1-item.pug +++ /dev/null @@ -1,29 +0,0 @@ -td - -td.selectProject - span.v1-badge( - aria-label=translate("v1_badge") - tooltip-template="'v1ProjectTooltipTemplate'" - tooltip-append-to-body="true" - ) - -td.projectName - if settings.overleaf && settings.overleaf.host - button.btn.btn-link.projectName( - ng-click="openV1ImportModal(project)" - stop-propagation="click" - ng-show="project.accessLevel == 'owner'" - ) {{project.name}} - a.projectName( - href=settings.overleaf.host + "/{{project.id}}" - target="_blank" - ng-hide="project.accessLevel == 'owner'" - ) {{project.name}} - -td - span.owner {{userDisplayName(project.owner)}} - -td - span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} - -td diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index a6a4d48ef1..0146ff3348 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -13,7 +13,7 @@ define [ $scope.predicate = "lastUpdated" $scope.nUntagged = 0 $scope.reverse = true - $scope.searchText = + $scope.searchText = value : "" $timeout () -> @@ -37,7 +37,7 @@ define [ angular.element($window).bind "resize", () -> recalculateProjectListHeight() $scope.$apply() - + # Allow tags to be accessed on projects as well projectsById = {} for project in $scope.projects @@ -56,7 +56,7 @@ define [ tag.selected = true else tag.selected = false - + $scope.changePredicate = (newPredicate)-> if $scope.predicate == newPredicate $scope.reverse = !$scope.reverse @@ -145,7 +145,7 @@ define [ # We don't want hidden selections project.selected = false - localStorage("project_list", JSON.stringify({ + localStorage("project_list", JSON.stringify({ filter: $scope.filter, selectedTagId: selectedTag?._id })) @@ -461,7 +461,7 @@ define [ resolve: project: () -> project ) - + if storedUIOpts?.filter? if storedUIOpts.filter == "tag" and storedUIOpts.selectedTagId? markTagAsSelected(storedUIOpts.selectedTagId) @@ -486,7 +486,7 @@ define [ return project.source == 'token' $scope.userDisplayName = (user) -> - if user._id == window.user_id + if user? and user._id == window.user_id return "You" else if user? return [user.first_name, user.last_name].filter((n) -> n?).join(" ") @@ -535,11 +535,11 @@ define [ url: "/project/#{$scope.project.id}?forever=true" headers: "X-CSRF-Token": window.csrfToken - }).then () -> + }).then () -> $scope.project.isTableActionInflight = false $scope._removeProjectFromList $scope.project for tag in $scope.tags $scope._removeProjectIdsFromTagArray(tag, [ $scope.project.id ]) $scope.updateVisibleProjects() - .catch () -> + .catch () -> $scope.project.isTableActionInflight = false diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 34d3e25c75..f6b2e4cfce 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -62,7 +62,7 @@ .small { color: @sidebar-color; } - } + } .project-list-sidebar when (@is-overleaf) { overflow-x: hidden; @@ -271,7 +271,7 @@ ul.structured-list { li { border-bottom: 1px solid @structured-list-border-color; padding: (@line-height-computed / 4) 0; - + &:last-child { border-bottom: 0 none; } @@ -294,7 +294,7 @@ ul.structured-list { .header when (@is-overleaf = false) { text-transform: uppercase; } - + .select-item, .select-all { position: absolute; left: @line-height-computed; @@ -355,7 +355,7 @@ table.project-list { .owner when (@is-overleaf = false) { margin-right: 0; } - a.projectName { + a.projectName, button.projectName { margin-right: @line-height-computed / 4; padding: 0; vertical-align: inherit; @@ -400,6 +400,7 @@ table.project-list { .v1-badge { margin-left: -4px; + margin-right: -4px; } .action-btn-row-header, .action-btn-row { @@ -595,7 +596,7 @@ table.project-list { &:last-child { margin-bottom: 0; } - } + } .announcement-header { .page-header; margin: 0; From ab871b9ba73f88cd5985ed8f67a9050e6e9aa3d5 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 12 Sep 2018 11:07:35 +0100 Subject: [PATCH 31/33] Fix tabs vs spaces --- services/web/public/stylesheets/app/project-list.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index f6b2e4cfce..86a72436c4 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -331,7 +331,7 @@ table.project-list { } // thead > tr > th { - // padding-top: @line-height-computed / 8; + // padding-top: @line-height-computed / 8; // } td { @@ -400,7 +400,7 @@ table.project-list { .v1-badge { margin-left: -4px; - margin-right: -4px; + margin-right: -4px; } .action-btn-row-header, .action-btn-row { From 5dbebc0693d7c8c22959b8425607d2b4e044caee Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 13 Sep 2018 10:36:34 +0100 Subject: [PATCH 32/33] Translate 'by' --- services/web/app/views/project/list/item.pug | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index 0ab99893da..5266571da7 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -63,7 +63,9 @@ td | {{project.lastUpdated | fromNowDate}} span(ng-if='project.lastUpdatedBy') | - | by {{userDisplayName(project.lastUpdatedBy)}} + | #{translate('by')} + | + | {{userDisplayName(project.lastUpdatedBy)}} td.text-right div( From ef11161ddbe88e3b825bf828a93297635edea5dc Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 13 Sep 2018 14:00:30 +0100 Subject: [PATCH 33/33] Revert "Record and show last modified by user for projects" --- .../Features/History/HistoryController.coffee | 9 - .../Features/Project/ProjectController.coffee | 7 +- .../Project/ProjectEntityUpdateHandler.coffee | 2 + .../Project/ProjectUpdateHandler.coffee | 23 +- services/web/app/coffee/models/Project.coffee | 1 - services/web/app/coffee/router.coffee | 1 - services/web/app/views/project/list/item.pug | 221 ++++++------ .../app/views/project/list/project-list.pug | 330 +++++++++--------- .../web/app/views/project/list/v1-item.pug | 25 ++ .../main/project-list/project-list.coffee | 22 +- .../public/stylesheets/app/project-list.less | 46 +-- .../public/stylesheets/components/tables.less | 2 +- .../stylesheets/core/_common-variables.less | 2 +- .../coffee/ProjectLastUpdatedTests.coffee | 42 --- .../History/HistoryControllerTests.coffee | 1 - .../ProjectEntityUpdateHandlerTests.coffee | 5 + .../Project/ProjectUpdateHandlerTests.coffee | 15 +- 17 files changed, 344 insertions(+), 410 deletions(-) create mode 100644 services/web/app/views/project/list/v1-item.pug delete mode 100644 services/web/test/acceptance/coffee/ProjectLastUpdatedTests.coffee diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee index 8b014751f9..189e07b0f5 100644 --- a/services/web/app/coffee/Features/History/HistoryController.coffee +++ b/services/web/app/coffee/Features/History/HistoryController.coffee @@ -7,7 +7,6 @@ HistoryManager = require "./HistoryManager" ProjectDetailsHandler = require "../Project/ProjectDetailsHandler" ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" RestoreManager = require "./RestoreManager" -ProjectUpdateHandler = require "../Project/ProjectUpdateHandler" module.exports = HistoryController = selectHistoryApi: (req, res, next = (error) ->) -> @@ -144,11 +143,3 @@ module.exports = HistoryController = 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) - - setLastUpdated: (req, res, next) -> - {project_id} = req.params - {user_id, timestamp} = req.body - logger.log {project_id, user_id, timestamp}, 'updating last updated date' - ProjectUpdateHandler.markAsUpdated project_id, user_id, timestamp, (error) -> - return next(error) if error? - res.sendStatus 200 diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index ed5e219e3f..59c0647c19 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -184,7 +184,7 @@ module.exports = ProjectController = notifications: (cb)-> NotificationsHandler.getUserNotifications user_id, cb projects: (cb)-> - ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated lastUpdatedBy publicAccesLevel archived owner_ref tokens', cb + ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref tokens', cb v1Projects: (cb) -> Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) -> if error? and error instanceof V1ConnectionError @@ -392,7 +392,6 @@ module.exports = ProjectController = id: project._id name: project.name lastUpdated: project.lastUpdated - lastUpdatedBy: project.lastUpdatedBy publicAccessLevel: project.publicAccesLevel accessLevel: accessLevel source: source @@ -431,8 +430,6 @@ module.exports = ProjectController = for project in projects if project.owner_ref? users[project.owner_ref.toString()] = true - if project.lastUpdatedBy? - users[project.lastUpdatedBy] = true jobs = [] for user_id, _ of users @@ -447,8 +444,6 @@ module.exports = ProjectController = for project in projects if project.owner_ref? project.owner = users[project.owner_ref.toString()] - if project.lastUpdatedBy? - project.lastUpdatedBy = users[project.lastUpdatedBy.toString()] callback null, projects _buildWarningsList: (v1ProjectData = {}) -> diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 4800de05d4..b7be80cb47 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -114,6 +114,8 @@ module.exports = ProjectEntityUpdateHandler = self = logger.log {project_id, doc_id, modified}, "finished updating doc lines" # path will only be present if the doc is not deleted if modified && !isDeletedDoc + # Don't need to block for marking as updated + ProjectUpdateHandler.markAsUpdated project_id TpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, doc_id:doc_id, project_name:project.name, rev:rev}, callback else callback() diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee index d89d7c7e89..ba3b3b6295 100644 --- a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee @@ -2,25 +2,30 @@ Project = require('../../models/Project').Project logger = require('logger-sharelatex') module.exports = - markAsUpdated : (project_id, user_id, timestamp, callback)-> + markAsUpdated : (project_id, callback)-> conditions = {_id:project_id} - update = { - lastUpdated: new Date(timestamp), - lastUpdatedBy: user_id - } - Project.update conditions, update, {}, callback + update = {lastUpdated:Date.now()} + Project.update conditions, update, {}, (err)-> + if callback? + callback() markAsOpened : (project_id, callback)-> conditions = {_id:project_id} update = {lastOpened:Date.now()} - Project.update conditions, update, {}, callback + Project.update conditions, update, {}, (err)-> + if callback? + callback() markAsInactive: (project_id, callback)-> conditions = {_id:project_id} update = {active:false} - Project.update conditions, update, {}, callback + Project.update conditions, update, {}, (err)-> + if callback? + callback() markAsActive: (project_id, callback)-> conditions = {_id:project_id} update = {active:true} - Project.update conditions, update, {}, callback + Project.update conditions, update, {}, (err)-> + if callback? + callback() \ No newline at end of file diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index f4b478d64e..00c2f1be52 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -19,7 +19,6 @@ DeletedFileSchema = new Schema ProjectSchema = new Schema name : {type:String, default:'new project'} lastUpdated : {type:Date, default: () -> new Date()} - lastUpdatedBy : {type:ObjectId, ref: 'User'} lastOpened : {type:Date} active : { type: Boolean, default: true } owner_ref : {type:ObjectId, ref:'User'} diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 2413632581..b196c56870 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -238,7 +238,6 @@ module.exports = class Router webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory - privateApiRouter.post "/project/:project_id/last_updated", AuthenticationController.httpAuth, HistoryController.setLastUpdated 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 diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index 5266571da7..deb9863d04 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -1,123 +1,104 @@ -td.selectProject - input.select-item( - ng-if="!project.isV1Project", - select-individual, - type="checkbox", - ng-disabled="shouldDisableCheckbox(project)", - ng-model="project.selected" - stop-propagation="click" - aria-label=translate('select_project') + " '{{ project.name }}'" - ) - span.v1-badge( - ng-if="project.isV1Project", - aria-label=translate("v1_badge") - tooltip-template="'v1ProjectTooltipTemplate'" - tooltip-append-to-body="true" - ) -td.projectName - span(ng-if="project.isV1Project") - if settings.overleaf && settings.overleaf.host - button.btn.btn-link.projectName( - ng-click="openV1ImportModal(project)" - stop-propagation="click" - ng-show="project.accessLevel == 'owner'" - ) {{project.name}} - a.projectName( - href=settings.overleaf.host + "/{{project.id}}" - target="_blank" - ng-hide="project.accessLevel == 'owner'" - ) {{project.name}} - span(ng-if="!project.isV1Project") - a.projectName( - ng-href="{{projectLink(project)}}" - stop-propagation="click" - ) {{project.name}} - span( - ng-controller="TagListController" - ) - .tag-label( - ng-repeat='tag in project.tags' - stop-propagation="click" - ) - a.label.label-default.tag-label-name( - href, - ng-click="selectTag(tag)" - ) {{tag.name}} - a.label.label-default.tag-label-remove( - href - ng-click="removeProjectFromTag(project, tag)" - ) × +- var titleClasses = settings.overleaf ? "col-xs-6 col-sm-4 col-md-6" : "col-xs-6" +- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4" -td - span.owner {{userDisplayName(project.owner)}} - span(ng-if="isLinkSharingProject(project)") - |   - i.fa.fa-link.small( - tooltip=translate("link_sharing") - tooltip-placement="right" - tooltip-append-to-body="true" - ) +div(class=titleClasses) + input.select-item( + select-individual, + type="checkbox", + ng-disabled="shouldDisableCheckbox(project)", + ng-model="project.selected" + stop-propagation="click" + aria-label=translate('select_project') + " '{{ project.name }}'" + ) + span + a.projectName( + ng-href="{{projectLink(project)}}" + stop-propagation="click" + ) {{project.name}} + span( + ng-controller="TagListController" + ) + .tag-label( + ng-repeat='tag in project.tags' + stop-propagation="click" + ) + a.label.label-default.tag-label-name( + href, + ng-click="selectTag(tag)" + ) {{tag.name}} + a.label.label-default.tag-label-remove( + href + ng-click="removeProjectFromTag(project, tag)" + ) × -td - span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") - | {{project.lastUpdated | fromNowDate}} - span(ng-if='project.lastUpdatedBy') - | - | #{translate('by')} - | - | {{userDisplayName(project.lastUpdatedBy)}} +.col-xs-2 + span.owner {{ownerName()}} + span(ng-if="isLinkSharingProject(project)") + |   + i.fa.fa-link.small( + tooltip=translate("link_sharing") + tooltip-placement="right" + tooltip-append-to-body="true" + ) -td.text-right - div( - ng-if="!project.isTableActionInflight && !project.isV1Project" - ) - button.btn.btn-link.action-btn( - tooltip=translate('copy'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="clone($event)" - ) - i.icon.fa.fa-files-o - button.btn.btn-link.action-btn( - tooltip=translate('download'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="download($event)" - ) - i.icon.fa.fa-cloud-download - button.btn.btn-link.action-btn( - ng-if="!project.archived && isOwner()" - tooltip=translate('archive'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="archiveOrLeave($event)" - ) - i.icon.fa.fa-inbox - button.btn.btn-link.action-btn( - ng-if="!project.archived && !isOwner()" - tooltip=translate('leave'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="archiveOrLeave($event)" - ) - i.icon.fa.fa-sign-out - button.btn.btn-link.action-btn( - ng-if="project.archived" - tooltip=translate('unarchive'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="restore($event)" - ) - i.icon.fa.fa-reply - button.btn.btn-link.action-btn( - ng-if="project.archived && isOwner()" - tooltip=translate('delete_forever'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="deleteProject($event)" - ) - i.icon.fa.fa-trash - div( - ng-if="project.isTableActionInflight" - ) - i.fa.fa-spinner.fa-spin +div(class=lastUpdatedClasses) + if settings.overleaf + span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} + else + span.last-modified {{project.lastUpdated | formatDate}} + +if settings.overleaf + .hidden-xs.col-sm-3.col-md-2.action-btn-row + div( + ng-if="!project.isTableActionInflight" + ) + button.btn.btn-link.action-btn( + tooltip=translate('copy'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="clone($event)" + ) + i.icon.fa.fa-files-o + button.btn.btn-link.action-btn( + tooltip=translate('download'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="download($event)" + ) + i.icon.fa.fa-cloud-download + button.btn.btn-link.action-btn( + ng-if="!project.archived && isOwner()" + tooltip=translate('archive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="archiveOrLeave($event)" + ) + i.icon.fa.fa-inbox + button.btn.btn-link.action-btn( + ng-if="!project.archived && !isOwner()" + tooltip=translate('leave'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="archiveOrLeave($event)" + ) + i.icon.fa.fa-sign-out + button.btn.btn-link.action-btn( + ng-if="project.archived" + tooltip=translate('unarchive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="restore($event)" + ) + i.icon.fa.fa-reply + button.btn.btn-link.action-btn( + ng-if="project.archived && isOwner()" + tooltip=translate('delete_forever'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="deleteProject($event)" + ) + i.icon.fa.fa-trash + div( + ng-if="project.isTableActionInflight" + ) + i.fa.fa-spinner.fa-spin \ No newline at end of file diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index a6971aa49a..cfea4aa6c6 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -1,175 +1,185 @@ .row - .col-xs-12(ng-cloak) + .col-xs-12(ng-cloak) - form.project-search.form-horizontal(role="form") - .form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12 - input.form-control.col-md-7.col-xs-12( - placeholder=translate('search_projects')+"…", - aria-label=translate('search_projects')+"…", - autofocus='autofocus', - ng-model="searchText.value", - focus-on='search:clear', - ng-keyup="searchProjects()" - ) - i.fa.fa-search.form-control-feedback-left - i.fa.fa-times.form-control-feedback( - ng-click="clearSearchText()", - style="cursor: pointer;", - ng-show="searchText.value.length > 0" - ) - //- i.fa.fa-remove + form.project-search.form-horizontal(role="form") + .form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12 + input.form-control.col-md-7.col-xs-12( + placeholder=translate('search_projects')+"…", + aria-label=translate('search_projects')+"…", + autofocus='autofocus', + ng-model="searchText.value", + focus-on='search:clear', + ng-keyup="searchProjects()" + ) + i.fa.fa-search.form-control-feedback-left + i.fa.fa-times.form-control-feedback( + ng-click="clearSearchText()", + style="cursor: pointer;", + ng-show="searchText.value.length > 0" + ) + //- i.fa.fa-remove - .project-tools(ng-cloak) - .btn-toolbar(ng-show="filter != 'archived'") - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-default( - href, - tooltip=translate('download'), - tooltip-placement="bottom", - tooltip-append-to-body="true", - ng-click="downloadSelectedProjects()" - ) - i.fa.fa-cloud-download - - var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete") - - var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o" - a.btn.btn-default( - href, - tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`, - tooltip-placement="bottom", - tooltip-append-to-body="true", - ng-click="openArchiveProjectsModal()" - ) - i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`) + .project-tools(ng-cloak) + .btn-toolbar(ng-show="filter != 'archived'") + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-default( + href, + tooltip=translate('download'), + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="downloadSelectedProjects()" + ) + i.fa.fa-cloud-download + - var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete") + - var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o" + a.btn.btn-default( + href, + tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`, + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="openArchiveProjectsModal()" + ) + i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`) - .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) - a.btn.btn-default.dropdown-toggle( - href, - data-toggle="dropdown", - dropdown-toggle, - tooltip=translate('add_to_folders'), - tooltip-append-to-body="true", - tooltip-placement="bottom" - ) - i.fa.fa-folder-open-o - | - span.caret - ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu.tags-dropdown-menu( - role="menu" - ng-controller="TagListController" - ) - li.dropdown-header #{translate("add_to_folder")} - li( - ng-repeat="tag in tags | orderBy:'name'", - ng-controller="TagDropdownItemController" - ng-if="!tag.isV1" - ) - a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") - i.fa( - ng-class="{\ - 'fa-check-square-o': areSelectedProjectsInTag == true,\ - 'fa-square-o': areSelectedProjectsInTag == false,\ - 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\ - }" - ) - | {{tag.name}} - li.divider - li - a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} + .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) + a.btn.btn-default.dropdown-toggle( + href, + data-toggle="dropdown", + dropdown-toggle, + tooltip=translate('add_to_folders'), + tooltip-append-to-body="true", + tooltip-placement="bottom" + ) + i.fa.fa-folder-open-o + | + span.caret + ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu.tags-dropdown-menu( + role="menu" + ng-controller="TagListController" + ) + li.dropdown-header #{translate("add_to_folder")} + li( + ng-repeat="tag in tags | orderBy:'name'", + ng-controller="TagDropdownItemController" + ng-if="!tag.isV1" + ) + a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") + i.fa( + ng-class="{\ + 'fa-check-square-o': areSelectedProjectsInTag == true,\ + 'fa-square-o': areSelectedProjectsInTag == false,\ + 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\ + }" + ) + | {{tag.name}} + li.divider + li + a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} - .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown - a.btn.btn-default.dropdown-toggle( - href, - data-toggle="dropdown", - dropdown-toggle - ) #{translate("more")} - span.caret - ul.dropdown-menu.dropdown-menu-right(role="menu") - li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") - a( - href, - ng-click="openRenameProjectModal()" - ) #{translate("rename")} - li - a( - href, - ng-click="openCloneProjectModal()" - ) #{translate("make_copy")} + .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown + a.btn.btn-default.dropdown-toggle( + href, + data-toggle="dropdown", + dropdown-toggle + ) #{translate("more")} + span.caret + ul.dropdown-menu.dropdown-menu-right(role="menu") + li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") + a( + href, + ng-click="openRenameProjectModal()" + ) #{translate("rename")} + li + a( + href, + ng-click="openCloneProjectModal()" + ) #{translate("make_copy")} - .btn-toolbar(ng-show="filter == 'archived'") - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-default( - href, - data-original-title="Restore", - data-toggle="tooltip", - data-placement="bottom", - ng-click="restoreSelectedProjects()" - ) #{translate("restore")} + .btn-toolbar(ng-show="filter == 'archived'") + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-default( + href, + data-original-title="Restore", + data-toggle="tooltip", + data-placement="bottom", + ng-click="restoreSelectedProjects()" + ) #{translate("restore")} - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-danger( - href, - data-original-title="Delete Forever", - data-toggle="tooltip", - data-placement="bottom", - ng-click="openDeleteProjectsModal()" - ) #{translate("delete_forever")} + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-danger( + href, + data-original-title="Delete Forever", + data-toggle="tooltip", + data-placement="bottom", + ng-click="openDeleteProjectsModal()" + ) #{translate("delete_forever")} .row.row-spaced - each warning in warnings - .col-xs-12 - .alert.alert-warning(role="alert")= warning + each warning in warnings + .col-xs-12 + .alert.alert-warning(role="alert")= warning - .col-xs-12 - .card.card-thin.project-list-card - .table-wrapper(max-height="projectListHeight - 25",) - table.table.table-hover.project-list( - select-all-list, - ng-if="projects.length > 0", - ng-cloak - ) - thead - tr - th.selectProject - input.select-all( - select-all, - type="checkbox" - aria-label=translate('select_all_projects') - ) - th.projectName - span.header.clickable(ng-click="changePredicate('name')") #{translate("title")} - i.tablesort.fa(ng-class="getSortIconClass('name')") - th - span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} - i.tablesort.fa(ng-class="getSortIconClass('accessLevel')") - th - span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} - i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") - if settings.overleaf - th.text-right - span.header #{translate("actions")} - tbody - tr.project_entry( - ng-repeat="project in visibleProjects | orderBy:predicate:reverse", - ng-controller="ProjectListItemController" - ) - include ./item - tr( - ng-if="visibleProjects.length == 0", - ng-cloak - ) - td(colspan=5).text-center - small #{translate("no_projects")} + .col-xs-12 + .card.card-thin.project-list-card + ul.list-unstyled.project-list.structured-list( + select-all-list, + ng-if="projects.length > 0", + max-height="projectListHeight - 25", + ng-cloak + ) + li.container-fluid + .row + - var titleClasses = settings.overleaf ? " col-xs-6 col-sm-4 col-md-6" : "col-xs-6" + - var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4" - div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak) - h2 #{translate("welcome_to_sl")} - p #{translate("new_to_latex_look_at")} - a(href="/templates") #{translate("templates").toLowerCase()} - | #{translate("or")} - a(href="/learn") #{translate("latex_help_guide")} - | , - br - | #{translate("or_create_project_left")} + div(class=titleClasses) + input.select-all( + select-all, + type="checkbox" + aria-label=translate('select_all_projects') + ) + span.header.clickable(ng-click="changePredicate('name')") #{translate("title")} + i.tablesort.fa(ng-class="getSortIconClass('name')") + .col-xs-2 + span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} + i.tablesort.fa(ng-class="getSortIconClass('accessLevel')") + div(class=lastUpdatedClasses) + span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} + i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") + if settings.overleaf + .hidden-xs.col-sm-3.col-md-2.action-btn-row-header + span.header #{translate("actions")} + li.project_entry.container-fluid( + ng-repeat="project in visibleProjects | orderBy:predicate:reverse", + ng-controller="ProjectListItemController" + ) + .row( + ng-if="!project.isV1Project" + select-row + ) + include ./item + .row( + ng-if="project.isV1Project" + ) + include ./v1-item + li( + ng-if="visibleProjects.length == 0", + ng-cloak + ) + .row + .col-xs-12.text-centered + small #{translate("no_projects")} + div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak) + h2 #{translate("welcome_to_sl")} + p #{translate("new_to_latex_look_at")} + a(href="/templates") #{translate("templates").toLowerCase()} + | #{translate("or")} + a(href="/learn") #{translate("latex_help_guide")} + | , + br + | #{translate("or_create_project_left")} + diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug new file mode 100644 index 0000000000..5a8e37bca0 --- /dev/null +++ b/services/web/app/views/project/list/v1-item.pug @@ -0,0 +1,25 @@ +.col-xs-6.col-sm-4.col-md-6 + .select-item + span.v1-badge( + aria-label=translate("v1_badge") + tooltip-template="'v1ProjectTooltipTemplate'" + tooltip-append-to-body="true" + ) + span + if settings.overleaf && settings.overleaf.host + button.btn.btn-link.projectName( + ng-click="openV1ImportModal(project)" + stop-propagation="click" + ng-show="project.accessLevel == 'owner'" + ) {{project.name}} + a.projectName( + href=settings.overleaf.host + "/{{project.id}}" + target="_blank" + ng-hide="project.accessLevel == 'owner'" + ) {{project.name}} + +.col-xs-2 + span.owner {{ownerName()}} + +.col-xs-4.col-sm-3.col-md-2 + span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} \ No newline at end of file diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 0146ff3348..9a8d7f7a73 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -13,7 +13,7 @@ define [ $scope.predicate = "lastUpdated" $scope.nUntagged = 0 $scope.reverse = true - $scope.searchText = + $scope.searchText = value : "" $timeout () -> @@ -37,7 +37,7 @@ define [ angular.element($window).bind "resize", () -> recalculateProjectListHeight() $scope.$apply() - + # Allow tags to be accessed on projects as well projectsById = {} for project in $scope.projects @@ -56,7 +56,7 @@ define [ tag.selected = true else tag.selected = false - + $scope.changePredicate = (newPredicate)-> if $scope.predicate == newPredicate $scope.reverse = !$scope.reverse @@ -145,7 +145,7 @@ define [ # We don't want hidden selections project.selected = false - localStorage("project_list", JSON.stringify({ + localStorage("project_list", JSON.stringify({ filter: $scope.filter, selectedTagId: selectedTag?._id })) @@ -461,7 +461,7 @@ define [ resolve: project: () -> project ) - + if storedUIOpts?.filter? if storedUIOpts.filter == "tag" and storedUIOpts.selectedTagId? markTagAsSelected(storedUIOpts.selectedTagId) @@ -485,11 +485,11 @@ define [ $scope.isLinkSharingProject = (project) -> return project.source == 'token' - $scope.userDisplayName = (user) -> - if user? and user._id == window.user_id + $scope.ownerName = () -> + if $scope.project.accessLevel == "owner" return "You" - else if user? - return [user.first_name, user.last_name].filter((n) -> n?).join(" ") + else if $scope.project.owner? + return [$scope.project.owner.first_name, $scope.project.owner.last_name].filter((n) -> n?).join(" ") else return "None" @@ -535,11 +535,11 @@ define [ url: "/project/#{$scope.project.id}?forever=true" headers: "X-CSRF-Token": window.csrfToken - }).then () -> + }).then () -> $scope.project.isTableActionInflight = false $scope._removeProjectFromList $scope.project for tag in $scope.tags $scope._removeProjectIdsFromTagArray(tag, [ $scope.project.id ]) $scope.updateVisibleProjects() - .catch () -> + .catch () -> $scope.project.isTableActionInflight = false diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 86a72436c4..e5bc8da14c 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -62,7 +62,7 @@ .small { color: @sidebar-color; } - } + } .project-list-sidebar when (@is-overleaf) { overflow-x: hidden; @@ -271,7 +271,7 @@ ul.structured-list { li { border-bottom: 1px solid @structured-list-border-color; padding: (@line-height-computed / 4) 0; - + &:last-child { border-bottom: 0 none; } @@ -294,7 +294,7 @@ ul.structured-list { .header when (@is-overleaf = false) { text-transform: uppercase; } - + .select-item, .select-all { position: absolute; left: @line-height-computed; @@ -314,38 +314,8 @@ ul.structured-list { padding: 0 (@line-height-computed / 4); } -.table-wrapper { - overflow: scroll; -} - -table.project-list { - margin: 0; - width: 100%; - - th when (@is-overleaf = true) { - font-weight: 600; - } - th when (@is-overleaf = false) { - text-transform: uppercase; - font-weight: normal; - } - - // thead > tr > th { - // padding-top: @line-height-computed / 8; - // } - - td { - @media (min-width: @screen-md-min) { - white-space: nowrap; - } - &.projectName { - white-space: normal; - width: 50%; - } - &.selectProject { - width: 1%; - } - +ul.project-list { + li { .last-modified when (@is-overleaf = false) { font-size: .8rem; } @@ -355,13 +325,12 @@ table.project-list { .owner when (@is-overleaf = false) { margin-right: 0; } - a.projectName, button.projectName { + .projectName { margin-right: @line-height-computed / 4; padding: 0; vertical-align: inherit; white-space: normal; text-align: left; - color: @structured-list-link-color; } .tag-label { @@ -400,7 +369,6 @@ table.project-list { .v1-badge { margin-left: -4px; - margin-right: -4px; } .action-btn-row-header, .action-btn-row { @@ -596,7 +564,7 @@ table.project-list { &:last-child { margin-bottom: 0; } - } + } .announcement-header { .page-header; margin: 0; diff --git a/services/web/public/stylesheets/components/tables.less b/services/web/public/stylesheets/components/tables.less index 30ede6697c..c41989c04d 100755 --- a/services/web/public/stylesheets/components/tables.less +++ b/services/web/public/stylesheets/components/tables.less @@ -34,7 +34,7 @@ th { // Bottom align for column headings > thead > tr > th { vertical-align: bottom; - border-bottom: 1px solid @table-border-color; + border-bottom: 2px solid @table-border-color; } // Remove top border from thead by default > caption + thead, diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 41bdbd2d9b..9d7eab3493 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -110,7 +110,7 @@ //## Customizes the `.table` component with basic values, each used across all table variations. //** Padding for ``s and ``s. -@table-cell-padding: @line-height-computed / 4; +@table-cell-padding: 8px; //** Padding for cells in `.table-condensed`. @table-condensed-cell-padding: 5px; diff --git a/services/web/test/acceptance/coffee/ProjectLastUpdatedTests.coffee b/services/web/test/acceptance/coffee/ProjectLastUpdatedTests.coffee deleted file mode 100644 index 5798d0236c..0000000000 --- a/services/web/test/acceptance/coffee/ProjectLastUpdatedTests.coffee +++ /dev/null @@ -1,42 +0,0 @@ -expect = require("chai").expect -async = require("async") -User = require "./helpers/User" -request = require "./helpers/request" -settings = require "settings-sharelatex" -Project = require("../../../app/js/models/Project").Project - -markAsUpdated = (project_id, user_id, timestamp, callback) -> - request.post { - url: "/project/#{project_id}/last_updated" - json: { - user_id, - timestamp - } - auth: - user: settings.apis.web.user - pass: settings.apis.web.pass - sendImmediately: true - jar: false - }, callback - -describe "ProjectLastUpdated", -> - before (done) -> - @timeout(90000) - @owner = new User() - @timestamp = Date.now() - @user_id = "abcdef1234567890abcdef12" - async.series [ - (cb) => @owner.login cb - (cb) => @owner.createProject "private-project", (error, @project_id) => cb(error) - ], done - - describe "with user_id and timestamp", -> - it 'should update the project', (done) -> - markAsUpdated @project_id, @user_id, @timestamp, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 200 - Project.findOne _id: @project_id, (error, project) => - return done(error) if error? - expect(project.lastUpdated.getTime()).to.equal @timestamp - expect(project.lastUpdatedBy.toString()).to.equal @user_id - done() diff --git a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee index 4e29bbae90..d5777f5490 100644 --- a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee +++ b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee @@ -23,7 +23,6 @@ describe "HistoryController", -> "../Project/ProjectDetailsHandler": @ProjectDetailsHandler = {} "../Project/ProjectEntityUpdateHandler": @ProjectEntityUpdateHandler = {} "./RestoreManager": @RestoreManager = {} - "../Project/ProjectUpdateHandler": @ProjectUpdateHandler = {} @settings.apis = trackchanges: enabled: false diff --git a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee index 1b4739000f..3de4546e6d 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee @@ -190,6 +190,11 @@ describe 'ProjectEntityUpdateHandler', -> .calledWith(project_id, doc_id, @docLines, @version, @ranges) .should.equal true + it "should mark the project as updated", -> + @ProjectUpdater.markAsUpdated + .calledWith(project_id) + .should.equal true + it "should send the doc the to the TPDS", -> @TpdsUpdateSender.addDoc .calledWith({ diff --git a/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee index da719c91f2..a68a9be1f1 100644 --- a/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee @@ -16,15 +16,12 @@ describe 'ProjectUpdateHandler', -> describe 'marking a project as recently updated', -> it 'should send an update to mongo', (done)-> project_id = "project_id" - user_id = "mock_user_id" - timestamp = Date.now() - @handler.markAsUpdated project_id, user_id, timestamp, (err)=> - @ProjectModel.update.calledWith({ - _id: project_id, - }, { - lastUpdated: new Date(timestamp), - lastUpdatedBy: user_id - }).should.equal true + @handler.markAsUpdated project_id, (err)=> + args = @ProjectModel.update.args[0] + args[0]._id.should.equal project_id + date = args[1].lastUpdated+"" + now = Date.now()+"" + date.substring(0,5).should.equal now.substring(0,5) done() describe "markAsOpened", ->