From fa23ea75b8080f09f198e26d81236e7006537c75 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Sun, 2 Sep 2018 13:47:16 +0100 Subject: [PATCH 001/122] 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 002/122] 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 003/122] 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 004/122] 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 005/122] 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 006/122] 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 007/122] 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 008/122] 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 009/122] 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 46eadfada44515650c008666d78885fe929d1d8d Mon Sep 17 00:00:00 2001 From: Chrystal Griffiths Date: Fri, 7 Sep 2018 17:23:03 +0100 Subject: [PATCH 010/122] Conditionally show sharing bits --- services/web/app/views/project/editor/share.pug | 14 +++++++------- .../ide/share/controllers/ShareController.coffee | 4 +--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index d2aa1cc173..cb46cb9a5f 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -8,9 +8,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate') h3 #{translate("share_project")} .modal-body.modal-body-share .container-fluid - //- Private (with token-access available) - .row.public-access-level(ng-show="project.publicAccesLevel == 'private'") + .row.public-access-level(ng-show="isAdmin && project.publicAccesLevel == 'private'") .col-xs-12.text-center | #{translate('link_sharing_is_off')} |    @@ -28,7 +27,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') ) //- Token-based access - .row.public-access-level(ng-show="project.publicAccesLevel == 'tokenBased'") + .row.public-access-level(ng-show="isAdmin && project.publicAccesLevel == 'tokenBased'") .col-xs-12.text-center strong | #{translate('link_sharing_is_on')}. @@ -57,7 +56,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') pre.access-token(ng-hide="readOnlyTokenLink") #{translate('loading')}... //- legacy public-access - .row.public-access-level(ng-show="project.publicAccesLevel == 'readAndWrite' || project.publicAccesLevel == 'readOnly'") + .row.public-access-level(ng-show="isAdmin && (project.publicAccesLevel == 'readAndWrite' || project.publicAccesLevel == 'readOnly')") .col-xs-12.text-center strong(ng-if="project.publicAccesLevel == 'readAndWrite'") #{translate("this_project_is_public")} strong(ng-if="project.publicAccesLevel == 'readOnly'") #{translate("this_project_is_public_read_only")} @@ -102,7 +101,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') ng-click="revokeInvite(invite)" ) i.fa.fa-times - .row.invite-controls + .row.invite-controls(ng-show="isAdmin") form(ng-show="canAddCollaborators") .small #{translate("share_with_your_collabs")} .form-group @@ -177,8 +176,9 @@ script(type='text/ng-template', id='shareProjectModalTemplate') ) #{translate("start_free_trial")} p.small(ng-show="startedFreeTrial") - | #{translate("refresh_page_after_starting_free_trial")}. - + | #{translate("refresh_page_after_starting_free_trial")} + .row.public-access-level(ng-show="!isAdmin") + .col-xs-12.text-center To add more collaborators, please ask the project owner .modal-footer.modal-footer-share .modal-footer-left i.fa.fa-refresh.fa-spin(ng-show="state.inflight") diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee index e729b92f08..3f71a63fdc 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee @@ -4,9 +4,7 @@ define [ App.controller "ShareController", ["$scope", "$modal", "ide", "projectInvites", "projectMembers", "event_tracking", ($scope, $modal, ide, projectInvites, projectMembers, event_tracking) -> $scope.openShareProjectModal = (isAdmin) -> - if !isAdmin - return - + $scope.isAdmin = isAdmin; event_tracking.sendMBOnce "ide-open-share-modal-once" $modal.open( From 1e04a09ec6369fd4db94e7771d31b8d4fdcc86f0 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Fri, 7 Sep 2018 18:15:32 +0100 Subject: [PATCH 011/122] 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 012/122] 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 66e288864ba162f852348b7038a7ca35950f80c1 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Tue, 11 Sep 2018 07:46:38 -0500 Subject: [PATCH 013/122] 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 014/122] 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 015/122] 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 016/122] 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 017/122] 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 018/122] 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 019/122] 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 020/122] 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 021/122] 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 e0ce988d32761b35d8a18d89c7d90807a167d1c9 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 10 Sep 2018 17:21:58 +0100 Subject: [PATCH 022/122] Intelligently redirect to v1 if no v2 project found for token --- .../Features/TokenAccess/TokenAccessController.coffee | 10 ++++++++++ services/web/app/coffee/router.coffee | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee index b6b65cc7a7..395489c608 100644 --- a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee +++ b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee @@ -3,9 +3,19 @@ AuthenticationController = require '../Authentication/AuthenticationController' TokenAccessHandler = require './TokenAccessHandler' Errors = require '../Errors/Errors' logger = require 'logger-sharelatex' +settings = require 'settings-sharelatex' module.exports = TokenAccessController = + redirectNotFoundErrorToV1: (err, req, res, next) -> + if err instanceof Errors.NotFoundError and settings.overleaf + logger.log { + token: req.params['read_and_write_token'] + }, "[TokenAccess] No project found for token, redirecting to v1" + res.redirect(settings.overleaf.host + req.url) + else + next(err) + _loadEditor: (projectId, req, res, next) -> req.params.Project_id = projectId.toString() return ProjectController.loadEditor(req, res, next) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index b196c56870..1fc42c354a 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -426,6 +426,7 @@ module.exports = class Router maxRequests: 10, timeInterval: 60 }), - TokenAccessController.readAndWriteToken + TokenAccessController.readAndWriteToken, + TokenAccessController.redirectNotFoundErrorToV1 webRouter.get '*', ErrorController.notFound From 24495f33405112e77fe418a35b35620653bddd56 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 11 Sep 2018 09:57:51 +0100 Subject: [PATCH 023/122] Also redirect not found read tokens to v1 --- services/web/app/coffee/router.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 1fc42c354a..35de9f804f 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -418,7 +418,8 @@ module.exports = class Router maxRequests: 10, timeInterval: 60 }), - TokenAccessController.readOnlyToken + TokenAccessController.readOnlyToken, + TokenAccessController.redirectNotFoundErrorToV1 webRouter.get '/:read_and_write_token([0-9]+[a-z]+)', RateLimiterMiddlewear.rateLimit({ From cf8ae7c28c304715068040ee5f31399be59cdf69 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 11 Sep 2018 16:45:39 +0100 Subject: [PATCH 024/122] Add test for redirecting to v1 if project unimported --- .../web/test/acceptance/coffee/TokenAccessTests.coffee | 7 +++++++ services/web/test/acceptance/config/settings.test.coffee | 3 +++ 2 files changed, 10 insertions(+) diff --git a/services/web/test/acceptance/coffee/TokenAccessTests.coffee b/services/web/test/acceptance/coffee/TokenAccessTests.coffee index 0e2985f75a..d54f19d0eb 100644 --- a/services/web/test/acceptance/coffee/TokenAccessTests.coffee +++ b/services/web/test/acceptance/coffee/TokenAccessTests.coffee @@ -415,3 +415,10 @@ describe 'TokenAccess', -> try_content_access(@other2, @project_id, (response, body) => expect(body.privilegeLevel).to.equal false , done) + + describe 'unimported v1 project', -> + it 'should redirect to v1', (done) -> + unimportedV1Token = '123abc' + try_read_and_write_token_access(@owner, unimportedV1Token, (response, body) => + expect(response.statusCode).to.equal 302 + , done) diff --git a/services/web/test/acceptance/config/settings.test.coffee b/services/web/test/acceptance/config/settings.test.coffee index 4dd820f4a7..4d1fcabe34 100644 --- a/services/web/test/acceptance/config/settings.test.coffee +++ b/services/web/test/acceptance/config/settings.test.coffee @@ -105,3 +105,6 @@ module.exports = path: (params) -> "/universities/list/#{params.id}" '/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' } '/proxy/missing/baseUrl': path: '/foo/bar' + + overleaf: + host: "http://#{process.env['V1_HOST']}:5000" From 9d600afdf83c5a0eda098ef65587d0c1c1e6ae42 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 12 Sep 2018 11:06:05 +0100 Subject: [PATCH 025/122] Fix failing tests for token access If project was changed from token access to private, then we want to 404 on v2 (not redirect to v1). So the logic was changed to check if the project exists and if it does then a 404 is returned. If it does not then it redirects to v1. --- services/web/app/coffee/Features/Errors/Errors.coffee | 8 ++++++++ .../Features/TokenAccess/TokenAccessController.coffee | 8 ++++++-- .../Features/TokenAccess/TokenAccessHandler.coffee | 10 +++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index 94aeaa2a90..720b092012 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -82,6 +82,13 @@ EmailExistsError = (message) -> return error EmailExistsError.prototype.__proto__ = Error.prototype +ProjectNotTokenAccessError = (message) -> + error = new Error(message) + error.name = "ProjectNotTokenAccessError" + error.__proto__ = ProjectNotTokenAccessError.prototype + return error +ProjectNotTokenAccessError.prototype.__proto__ = Error.prototype + module.exports = Errors = NotFoundError: NotFoundError ServiceNotConfiguredError: ServiceNotConfiguredError @@ -95,3 +102,4 @@ module.exports = Errors = V1ConnectionError: V1ConnectionError UnconfirmedEmailError: UnconfirmedEmailError EmailExistsError: EmailExistsError + ProjectNotTokenAccessError: ProjectNotTokenAccessError diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee index 395489c608..6b03e6c600 100644 --- a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee +++ b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee @@ -8,7 +8,7 @@ settings = require 'settings-sharelatex' module.exports = TokenAccessController = redirectNotFoundErrorToV1: (err, req, res, next) -> - if err instanceof Errors.NotFoundError and settings.overleaf + if err instanceof Errors.ProjectNotTokenAccessError and settings.overleaf logger.log { token: req.params['read_and_write_token'] }, "[TokenAccess] No project found for token, redirecting to v1" @@ -21,11 +21,15 @@ module.exports = TokenAccessController = return ProjectController.loadEditor(req, res, next) _tryHigherAccess: (token, userId, req, res, next) -> - TokenAccessHandler.findProjectWithHigherAccess token, userId, (err, project) -> + TokenAccessHandler.findProjectWithHigherAccess token, userId, (err, project, projectExists) -> if err? logger.err {err, token, userId}, "[TokenAccess] error finding project with higher access" return next(err) + if !projectExists + logger.log {token, userId}, + "[TokenAccess] no project found for this token" + return next(new Errors.ProjectNotTokenAccessError()) if !project? logger.log {token, userId}, "[TokenAccess] no project with higher access found for this user and token" diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee index 9eb792e55b..ed7f51f0d7 100644 --- a/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee +++ b/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee @@ -22,7 +22,7 @@ module.exports = TokenAccessHandler = 'publicAccesLevel': PublicAccessLevels.TOKEN_BASED }, {_id: 1, publicAccesLevel: 1, owner_ref: 1}, callback - findProjectWithHigherAccess: (token, userId, callback=(err, project)->) -> + findProjectWithHigherAccess: (token, userId, callback=(err, project, projectExists)->) -> Project.findOne { $or: [ {'tokens.readAndWrite': token}, @@ -32,12 +32,16 @@ module.exports = TokenAccessHandler = if err? return callback(err) if !project? - return callback(null, null) + return callback(null, null, false) # Project doesn't exist, so we handle differently projectId = project._id CollaboratorsHandler.isUserInvitedMemberOfProject userId, projectId, (err, isMember) -> if err? return callback(err) - callback(null, if isMember == true then project else null) + callback( + null, + if isMember == true then project else null, + true # Project does exist, but user doesn't have access + ) addReadOnlyUserToProject: (userId, projectId, callback=(err)->) -> userId = ObjectId(userId.toString()) From 893e2dd2350b236f4a0a09c95c053bcfaf348efd Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 12 Sep 2018 11:08:28 +0100 Subject: [PATCH 026/122] Add test for location of redirect to v1 --- services/web/test/acceptance/coffee/TokenAccessTests.coffee | 3 +++ services/web/test/acceptance/config/settings.test.coffee | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/services/web/test/acceptance/coffee/TokenAccessTests.coffee b/services/web/test/acceptance/coffee/TokenAccessTests.coffee index d54f19d0eb..b0fbd7a0a1 100644 --- a/services/web/test/acceptance/coffee/TokenAccessTests.coffee +++ b/services/web/test/acceptance/coffee/TokenAccessTests.coffee @@ -421,4 +421,7 @@ describe 'TokenAccess', -> unimportedV1Token = '123abc' try_read_and_write_token_access(@owner, unimportedV1Token, (response, body) => expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal( + 'http://overleaf.test:5000/123abc' + ) , done) diff --git a/services/web/test/acceptance/config/settings.test.coffee b/services/web/test/acceptance/config/settings.test.coffee index 4d1fcabe34..1ca1b55e48 100644 --- a/services/web/test/acceptance/config/settings.test.coffee +++ b/services/web/test/acceptance/config/settings.test.coffee @@ -107,4 +107,4 @@ module.exports = '/proxy/missing/baseUrl': path: '/foo/bar' overleaf: - host: "http://#{process.env['V1_HOST']}:5000" + host: "http://overleaf.test:5000" From 0c658127ef944348acc8deb0a13b4d2e17bbb354 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 12 Sep 2018 12:48:13 +0100 Subject: [PATCH 027/122] Add tests for ProjectNotTokenAccessError --- .../TokenAccessControllerTests.coffee | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee b/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee index 747822b896..0fe8ab358a 100644 --- a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee +++ b/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee @@ -231,6 +231,26 @@ describe "TokenAccessController", -> @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId.toString()) + describe 'when project does not exist', -> + beforeEach -> + @req = new MockRequest() + @res = new MockResponse() + @res.redirect = sinon.stub() + @next = sinon.stub() + @req.params['read_and_write_token'] = '123abc' + @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() + .callsArgWith(1, null, null) + @TokenAccessHandler.findProjectWithHigherAccess = + sinon.stub() + .callsArgWith(2, null, @project, false) + @TokenAccessController.readAndWriteToken @req, @res, @next + + it 'should return a ProjectNotTokenAccessError', (done) -> + expect(@next.callCount).to.equal 1 + expect(@next.firstCall.args[0].name) + .to.equal 'ProjectNotTokenAccessError' + done() + describe 'when token access is off, but user has higher access anyway', -> beforeEach -> @req = new MockRequest() @@ -242,7 +262,7 @@ describe "TokenAccessController", -> .callsArgWith(1, null, null) @TokenAccessHandler.findProjectWithHigherAccess = sinon.stub() - .callsArgWith(2, null, @project) + .callsArgWith(2, null, @project, true) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -479,6 +499,26 @@ describe "TokenAccessController", -> describe 'when findProject does not find a project', -> beforeEach -> + describe 'when project does not exist', -> + beforeEach -> + @req = new MockRequest() + @res = new MockResponse() + @res.redirect = sinon.stub() + @next = sinon.stub() + @req.params['read_and_write_token'] = '123abc' + @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() + .callsArgWith(1, null, null) + @TokenAccessHandler.findProjectWithHigherAccess = + sinon.stub() + .callsArgWith(2, null, @project, false) + @TokenAccessController.readOnlyToken @req, @res, @next + + it 'should return a ProjectNotTokenAccessError', (done) -> + expect(@next.callCount).to.equal 1 + expect(@next.firstCall.args[0].name) + .to.equal 'ProjectNotTokenAccessError' + done() + describe 'when token access is off, but user has higher access anyway', -> beforeEach -> @req = new MockRequest() @@ -490,7 +530,7 @@ describe "TokenAccessController", -> .callsArgWith(1, null, null) @TokenAccessHandler.findProjectWithHigherAccess = sinon.stub() - .callsArgWith(2, null, @project) + .callsArgWith(2, null, @project, true) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() From 8a969d1c25c8ee67b6a1172c84c668a58afacf96 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 12 Sep 2018 18:31:08 +0100 Subject: [PATCH 028/122] Redirect directly from controller instead of via handler --- .../app/coffee/Features/Errors/Errors.coffee | 8 ------- .../TokenAccess/TokenAccessController.coffee | 12 ++-------- services/web/app/coffee/router.coffee | 6 ++--- .../TokenAccessControllerTests.coffee | 24 ++++++++++++------- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index 720b092012..94aeaa2a90 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -82,13 +82,6 @@ EmailExistsError = (message) -> return error EmailExistsError.prototype.__proto__ = Error.prototype -ProjectNotTokenAccessError = (message) -> - error = new Error(message) - error.name = "ProjectNotTokenAccessError" - error.__proto__ = ProjectNotTokenAccessError.prototype - return error -ProjectNotTokenAccessError.prototype.__proto__ = Error.prototype - module.exports = Errors = NotFoundError: NotFoundError ServiceNotConfiguredError: ServiceNotConfiguredError @@ -102,4 +95,3 @@ module.exports = Errors = V1ConnectionError: V1ConnectionError UnconfirmedEmailError: UnconfirmedEmailError EmailExistsError: EmailExistsError - ProjectNotTokenAccessError: ProjectNotTokenAccessError diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee index 6b03e6c600..9e35c622b0 100644 --- a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee +++ b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee @@ -7,15 +7,6 @@ settings = require 'settings-sharelatex' module.exports = TokenAccessController = - redirectNotFoundErrorToV1: (err, req, res, next) -> - if err instanceof Errors.ProjectNotTokenAccessError and settings.overleaf - logger.log { - token: req.params['read_and_write_token'] - }, "[TokenAccess] No project found for token, redirecting to v1" - res.redirect(settings.overleaf.host + req.url) - else - next(err) - _loadEditor: (projectId, req, res, next) -> req.params.Project_id = projectId.toString() return ProjectController.loadEditor(req, res, next) @@ -29,7 +20,8 @@ module.exports = TokenAccessController = if !projectExists logger.log {token, userId}, "[TokenAccess] no project found for this token" - return next(new Errors.ProjectNotTokenAccessError()) + # Project does not exist, but may be unimported - try it on v1 + return res.redirect(settings.overleaf.host + req.url) if !project? logger.log {token, userId}, "[TokenAccess] no project with higher access found for this user and token" diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 35de9f804f..b196c56870 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -418,8 +418,7 @@ module.exports = class Router maxRequests: 10, timeInterval: 60 }), - TokenAccessController.readOnlyToken, - TokenAccessController.redirectNotFoundErrorToV1 + TokenAccessController.readOnlyToken webRouter.get '/:read_and_write_token([0-9]+[a-z]+)', RateLimiterMiddlewear.rateLimit({ @@ -427,7 +426,6 @@ module.exports = class Router maxRequests: 10, timeInterval: 60 }), - TokenAccessController.readAndWriteToken, - TokenAccessController.redirectNotFoundErrorToV1 + TokenAccessController.readAndWriteToken webRouter.get '*', ErrorController.notFound diff --git a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee b/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee index 0fe8ab358a..0bdb6d59e6 100644 --- a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee +++ b/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee @@ -30,6 +30,10 @@ describe "TokenAccessController", -> '../Authentication/AuthenticationController': @AuthenticationController = {} './TokenAccessHandler': @TokenAccessHandler = {} 'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()} + 'settings-sharelatex': { + overleaf: + host: 'http://overleaf.test:5000' + } @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId.toString()) @@ -234,6 +238,7 @@ describe "TokenAccessController", -> describe 'when project does not exist', -> beforeEach -> @req = new MockRequest() + @req.url = '/123abc' @res = new MockResponse() @res.redirect = sinon.stub() @next = sinon.stub() @@ -245,10 +250,10 @@ describe "TokenAccessController", -> .callsArgWith(2, null, @project, false) @TokenAccessController.readAndWriteToken @req, @res, @next - it 'should return a ProjectNotTokenAccessError', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.firstCall.args[0].name) - .to.equal 'ProjectNotTokenAccessError' + it 'should redirect to v1', (done) -> + expect(@res.redirect.callCount).to.equal 1 + expect(@res.redirect.firstCall.args[0]) + .to.equal 'http://overleaf.test:5000/123abc' done() describe 'when token access is off, but user has higher access anyway', -> @@ -311,7 +316,7 @@ describe "TokenAccessController", -> .callsArgWith(1, null, null) @TokenAccessHandler.findProjectWithHigherAccess = sinon.stub() - .callsArgWith(2, null, null) + .callsArgWith(2, null, null, true) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -502,6 +507,7 @@ describe "TokenAccessController", -> describe 'when project does not exist', -> beforeEach -> @req = new MockRequest() + @req.url = '/123abc' @res = new MockResponse() @res.redirect = sinon.stub() @next = sinon.stub() @@ -514,9 +520,9 @@ describe "TokenAccessController", -> @TokenAccessController.readOnlyToken @req, @res, @next it 'should return a ProjectNotTokenAccessError', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.firstCall.args[0].name) - .to.equal 'ProjectNotTokenAccessError' + expect(@res.redirect.callCount).to.equal 1 + expect(@res.redirect.firstCall.args[0]) + .to.equal 'http://overleaf.test:5000/123abc' done() describe 'when token access is off, but user has higher access anyway', -> @@ -578,7 +584,7 @@ describe "TokenAccessController", -> .callsArgWith(1, null, null) @TokenAccessHandler.findProjectWithHigherAccess = sinon.stub() - .callsArgWith(2, null, null) + .callsArgWith(2, null, null, true) @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() From f37040e4a4798d8f1daf0d8213c28fade38b1d6a Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 13 Sep 2018 12:09:03 +0100 Subject: [PATCH 029/122] Only redirect if has overleaf setting --- .../coffee/Features/TokenAccess/TokenAccessController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee index 9e35c622b0..08aa4663f1 100644 --- a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee +++ b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee @@ -17,7 +17,7 @@ module.exports = TokenAccessController = logger.err {err, token, userId}, "[TokenAccess] error finding project with higher access" return next(err) - if !projectExists + if !projectExists and settings.overleaf logger.log {token, userId}, "[TokenAccess] no project found for this token" # Project does not exist, but may be unimported - try it on v1 From 57ac858004583ecff5534aefd21cc8f9e36e7d9d Mon Sep 17 00:00:00 2001 From: Chrystal Griffiths Date: Thu, 13 Sep 2018 12:19:44 +0100 Subject: [PATCH 030/122] Style the notice --- services/web/app/views/project/editor/share.pug | 2 +- services/web/public/stylesheets/app/editor/share.less | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index cb46cb9a5f..0b45e0d17f 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -177,7 +177,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') p.small(ng-show="startedFreeTrial") | #{translate("refresh_page_after_starting_free_trial")} - .row.public-access-level(ng-show="!isAdmin") + .row.public-access-level.public-access-level--notice(ng-show="!isAdmin") .col-xs-12.text-center To add more collaborators, please ask the project owner .modal-footer.modal-footer-share .modal-footer-left diff --git a/services/web/public/stylesheets/app/editor/share.less b/services/web/public/stylesheets/app/editor/share.less index 13613af561..c4a92e6a53 100644 --- a/services/web/public/stylesheets/app/editor/share.less +++ b/services/web/public/stylesheets/app/editor/share.less @@ -30,6 +30,13 @@ } } + .public-access-level.public-access-level--notice { + background-color: @gray-lightest; + border-bottom: none; + margin-top: @margin-md; + padding-top: @margin-md; + } + .project-member, .project-invite { &:hover { background-color: @gray-lightest; From 1f976a0e04a357abacef225044bfa911022759f8 Mon Sep 17 00:00:00 2001 From: Michael Mazour Date: Thu, 13 Sep 2018 09:59:14 +0100 Subject: [PATCH 031/122] Improve ExportsController unit tests Test the params the handler's called with. --- .../test/unit/coffee/Exports/ExportsControllerTests.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee index c14037d665..15d35707d9 100644 --- a/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee +++ b/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee @@ -34,7 +34,12 @@ describe 'ExportsController', -> it 'should ask the handler to perform the export', (done) -> @handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897}) + expected = + project_id: project_id + user_id: user_id + brand_variation_id: brand_variation_id @controller.exportProject @req, send:(body) => + expect(@handler.exportProject.args[0][0]).to.deep.equal expected expect(body).to.deep.equal {export_v1_id: 897} done() From 8697edc1498f0c14fbedc5008632d77131b679d6 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Thu, 13 Sep 2018 12:22:27 +0100 Subject: [PATCH 032/122] replace profile completion CTA with affiliationa adding CTA --- .../web/app/views/project/list/side-bar.pug | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug index b2c8f11315..32868b57c4 100644 --- a/services/web/app/views/project/list/side-bar.pug +++ b/services/web/app/views/project/list/side-bar.pug @@ -115,18 +115,14 @@ span(ng-controller="LeftHandMenuPromoController", ng-cloak) .row-spaced#userProfileInformation(ng-if="hasProjects") - div(ng-controller="UserProfileController") - hr(ng-show="percentComplete < 100") - .text-centered.user-profile(ng-show="percentComplete < 100") - .progress - .progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}") - - p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})} + div(ng-controller="UserAffiliationsController") + hr(ng-show="userEmails.length == 1 && userEmails[0].affiliations == null") + .text-centered.user-profile(ng-show="userEmails.length == 1 && userEmails[0].affiliations == null") + p Are you affiliated with an institution? - button#completeUserProfileInformation.btn.btn-info( - ng-hide="formVisable", - ng-click="openUserProfileModal()" - ) #{translate("complete")} + a.btn.btn-info( + href="/user/settings" + ) Tell Us .row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered From 26defd08331331c0200261712d586a59a81ba93e Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Thu, 13 Sep 2018 13:54:43 +0100 Subject: [PATCH 033/122] reword CTA --- services/web/app/views/project/list/side-bar.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug index 32868b57c4..a35f702de7 100644 --- a/services/web/app/views/project/list/side-bar.pug +++ b/services/web/app/views/project/list/side-bar.pug @@ -122,7 +122,7 @@ a.btn.btn-info( href="/user/settings" - ) Tell Us + ) Add Affiliation .row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered From ef11161ddbe88e3b825bf828a93297635edea5dc Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 13 Sep 2018 14:00:30 +0100 Subject: [PATCH 034/122] 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", -> From b51fc01bde57836c1a54e45837a2291ab7658983 Mon Sep 17 00:00:00 2001 From: Chrystal Griffiths Date: Thu, 13 Sep 2018 17:17:18 +0100 Subject: [PATCH 035/122] Remove temporary solution --- services/web/app/views/project/editor/header.pug | 4 ---- .../web/public/stylesheets/app/editor/toolbar.less | 11 ----------- 2 files changed, 15 deletions(-) diff --git a/services/web/app/views/project/editor/header.pug b/services/web/app/views/project/editor/header.pug index 835192f298..a7f03f2e3c 100644 --- a/services/web/app/views/project/editor/header.pug +++ b/services/web/app/views/project/editor/header.pug @@ -103,12 +103,8 @@ header.toolbar.toolbar-header.toolbar-with-labels( a.btn.btn-full-height( href - ng-class="{ 'btn-full-height-disabled' : !permissions.admin }" ng-click="openShareProjectModal(permissions.admin);" ng-controller="ShareController" - tooltip-enable="!permissions.admin" - tooltip="Only the project owner can use the Share menu at the moment, but we're working on making it accessible to collaborators, too." - tooltip-placement="bottom" ) i.fa.fa-fw.fa-group p.toolbar-label #{translate("share")} diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less index aeea9da50a..dcc0344c73 100644 --- a/services/web/public/stylesheets/app/editor/toolbar.less +++ b/services/web/public/stylesheets/app/editor/toolbar.less @@ -72,17 +72,6 @@ background-color: @toolbar-btn-active-bg-color; box-shadow: @toolbar-btn-active-shadow; } - &.btn-full-height-disabled { - opacity: 0.65; - &:hover, - &.active, - &:active { - text-shadow: none; - background-color: transparent; - color: @toolbar-btn-color; - box-shadow: none; - } - } .label { top: 4px; right: 4px; From 0051e59309759dd03c0d83358031849d80108163 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Thu, 13 Sep 2018 17:14:28 +0100 Subject: [PATCH 036/122] remove unused call to UserGetter.getUser --- .../Editor/EditorHttpController.coffee | 24 +++++++++---------- .../Editor/EditorHttpControllerTests.coffee | 5 ---- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 8578f5b3f4..743b9ac190 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -41,21 +41,19 @@ module.exports = EditorHttpController = return callback(new Error("not found")) if !project? CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels project_id, (error, members) -> return callback(error) if error? - UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> + token = TokenAccessHandler.getRequestToken(req, project_id) + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) -> return callback(error) if error? - token = TokenAccessHandler.getRequestToken(req, project_id) - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) -> + if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE + logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null" + return callback null, null, false + CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) -> return callback(error) if error? - if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE - logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null" - return callback null, null, false - CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) -> - return callback(error) if error? - logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view" - callback(null, - ProjectEditorHandler.buildProjectModelView(project, members, invites), - privilegeLevel - ) + logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view" + callback(null, + ProjectEditorHandler.buildProjectModelView(project, members, invites), + privilegeLevel + ) _nameIsAcceptableLength: (name)-> return name? and name.length < 150 and name.length != 0 diff --git a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee index 08e9482778..01f6b17d39 100644 --- a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee @@ -137,11 +137,6 @@ describe "EditorHttpController", -> .calledWith(@project_id) .should.equal true - it "should look up the user", -> - @UserGetter.getUser - .calledWith(@user_id, { isAdmin: true }) - .should.equal true - it "should check the privilege level", -> @AuthorizationManager.getPrivilegeLevelForProject .calledWith(@user_id, @project_id, @token) From 3b43cf9075f7af71e75f14a8777540a8ff45e717 Mon Sep 17 00:00:00 2001 From: Chrystal Griffiths Date: Thu, 13 Sep 2018 17:57:11 +0100 Subject: [PATCH 037/122] Slight copy change --- services/web/app/views/project/editor/share.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index 0b45e0d17f..737cedf5cc 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -178,7 +178,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') p.small(ng-show="startedFreeTrial") | #{translate("refresh_page_after_starting_free_trial")} .row.public-access-level.public-access-level--notice(ng-show="!isAdmin") - .col-xs-12.text-center To add more collaborators, please ask the project owner + .col-xs-12.text-center To add more collaborators or turn on link sharing, please ask the project owner .modal-footer.modal-footer-share .modal-footer-left i.fa.fa-refresh.fa-spin(ng-show="state.inflight") From 10fcdd6daf38960b382322e77e18e9b21a2212fb Mon Sep 17 00:00:00 2001 From: Michael Mazour Date: Thu, 13 Sep 2018 12:14:06 +0100 Subject: [PATCH 038/122] Add optional gallery fields to export request Support the optional (well, gallery-only) fields `title`, `description`, `author`, `license`, and `show_source` in export requests. --- .../Features/Exports/ExportsController.coffee | 9 ++- .../Features/Exports/ExportsHandler.coffee | 9 ++- .../acceptance/coffee/ExportsTests.coffee | 14 ++++- .../Exports/ExportsControllerTests.coffee | 59 +++++++++++++++---- .../coffee/Exports/ExportsHandlerTests.coffee | 20 +++++++ 5 files changed, 96 insertions(+), 15 deletions(-) diff --git a/services/web/app/coffee/Features/Exports/ExportsController.coffee b/services/web/app/coffee/Features/Exports/ExportsController.coffee index e724951d2d..2f14fdb183 100644 --- a/services/web/app/coffee/Features/Exports/ExportsController.coffee +++ b/services/web/app/coffee/Features/Exports/ExportsController.coffee @@ -16,10 +16,17 @@ module.exports = if req.body && req.body.firstName && req.body.lastName export_params.first_name = req.body.firstName.trim() export_params.last_name = req.body.lastName.trim() + # additional parameters for gallery exports + if req.body + export_params.title = req.body.title.trim() if req.body.title + export_params.description = req.body.description.trim() if req.body.description + export_params.author = req.body.author.trim() if req.body.author + export_params.license = req.body.license.trim() if req.body.license + export_params.show_source = req.body.show_source if req.body.show_source ExportsHandler.exportProject export_params, (err, export_data) -> return next(err) if err? - logger.log + logger.log user_id:user_id project_id: project_id brand_variation_id:brand_variation_id diff --git a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee index 234a334241..885f063c8b 100644 --- a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee +++ b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee @@ -20,9 +20,7 @@ module.exports = ExportsHandler = self = callback null, export_data _buildExport: (export_params, callback=(err, export_data) ->) -> - project_id = export_params.project_id - user_id = export_params.user_id - brand_variation_id = export_params.brand_variation_id + {project_id, user_id, brand_variation_id, title, description, author, license, show_source} = export_params jobs = project: (cb) -> ProjectGetter.getProject project_id, cb @@ -60,6 +58,11 @@ module.exports = ExportsHandler = self = metadata: compiler: project.compiler imageName: project.imageName + title: title + description: description + author: author + license: license + show_source: show_source user: id: user_id firstName: user.first_name diff --git a/services/web/test/acceptance/coffee/ExportsTests.coffee b/services/web/test/acceptance/coffee/ExportsTests.coffee index 7a6784008d..95e7b2984c 100644 --- a/services/web/test/acceptance/coffee/ExportsTests.coffee +++ b/services/web/test/acceptance/coffee/ExportsTests.coffee @@ -30,7 +30,13 @@ describe 'Exports', -> @owner.request { method: 'POST', url: "/project/#{@project_id}/export/#{@brand_variation_id}", - json: {}, + json: true, + body: + title: 'title' + description: 'description' + author: 'author' + license: 'other' + show_source: true }, (error, response, body) => throw error if error? expect(response.statusCode).to.equal 200 @@ -42,6 +48,12 @@ describe 'Exports', -> # project details should match expect(project.id).to.equal @project_id expect(project.rootDocPath).to.equal '/main.tex' + # gallery details should match + expect(project.metadata.title).to.equal 'title' + expect(project.metadata.description).to.equal 'description' + expect(project.metadata.author).to.equal 'author' + expect(project.metadata.license).to.equal 'other' + expect(project.metadata.show_source).to.equal true # version should match what was retrieved from project-history expect(project.historyVersion).to.equal @version # user details should match diff --git a/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee index 15d35707d9..4e96f68926 100644 --- a/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee +++ b/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee @@ -10,6 +10,13 @@ describe 'ExportsController', -> project_id = "123njdskj9jlk" user_id = "123nd3ijdks" brand_variation_id = 22 + firstName = 'first' + lastName = 'last' + title = "title" + description = "description" + author = "author" + license = "other" + show_source = true beforeEach -> @handler = @@ -18,6 +25,9 @@ describe 'ExportsController', -> params: project_id: project_id brand_variation_id: brand_variation_id + body: + firstName: firstName + lastName: lastName session: user: _id:user_id @@ -32,16 +42,45 @@ describe 'ExportsController', -> err:-> '../Authentication/AuthenticationController': @AuthenticationController - it 'should ask the handler to perform the export', (done) -> - @handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897}) - expected = - project_id: project_id - user_id: user_id - brand_variation_id: brand_variation_id - @controller.exportProject @req, send:(body) => - expect(@handler.exportProject.args[0][0]).to.deep.equal expected - expect(body).to.deep.equal {export_v1_id: 897} - done() + describe "without gallery fields",-> + it 'should ask the handler to perform the export', (done) -> + @handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897}) + expected = + project_id: project_id + user_id: user_id + brand_variation_id: brand_variation_id + first_name: firstName + last_name: lastName + @controller.exportProject @req, send:(body) => + expect(@handler.exportProject.args[0][0]).to.deep.equal expected + expect(body).to.deep.equal {export_v1_id: 897} + done() + + describe "with gallery fields",-> + beforeEach -> + @req.body.title = title + @req.body.description = description + @req.body.author = author + @req.body.license = license + @req.body.show_source = true + + it 'should ask the handler to perform the export', (done) -> + @handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897}) + expected = + project_id: project_id + user_id: user_id + brand_variation_id: brand_variation_id + first_name: firstName + last_name: lastName + title: title + description: description + author: author + license: license + show_source: show_source + @controller.exportProject @req, send:(body) => + expect(@handler.exportProject.args[0][0]).to.deep.equal expected + expect(body).to.deep.equal {export_v1_id: 897} + done() it 'should ask the handler to return the status of an export', (done) -> @handler.fetchExport = sinon.stub().yields( diff --git a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee index 8b16088552..edd1ce127a 100644 --- a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee +++ b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee @@ -27,10 +27,20 @@ describe 'ExportsHandler', -> @project_history_id = 987 @user_id = "user-id-456" @brand_variation_id = 789 + @title = "title" + @description = "description" + @author = "author" + @license = "other" + @show_source = true @export_params = { project_id: @project_id, brand_variation_id: @brand_variation_id, user_id: @user_id + title: @title + description: @description + author: @author + license: @license + show_source: @show_source } @callback = sinon.stub() @@ -105,6 +115,11 @@ describe 'ExportsHandler', -> metadata: compiler: 'pdflatex' imageName: 'mock-image-name' + title: @title + description: @description + author: @author + license: @license + show_source: @show_source user: id: @user_id firstName: @user.first_name @@ -140,6 +155,11 @@ describe 'ExportsHandler', -> metadata: compiler: 'pdflatex' imageName: 'mock-image-name' + title: @title + description: @description + author: @author + license: @license + show_source: @show_source user: id: @user_id firstName: @custom_first_name From 79dc4150643a20f4dd786fbb04e7696564a4fc13 Mon Sep 17 00:00:00 2001 From: Michael Mazour Date: Fri, 14 Sep 2018 10:14:12 +0100 Subject: [PATCH 039/122] Slightly refactor exports controller body handling 1. Move all body parsing together 2. Remove `firstName && lastName` condition, which duplicates one present in the Handler. --- .../app/coffee/Features/Exports/ExportsController.coffee | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Exports/ExportsController.coffee b/services/web/app/coffee/Features/Exports/ExportsController.coffee index 2f14fdb183..5e76e9a4b8 100644 --- a/services/web/app/coffee/Features/Exports/ExportsController.coffee +++ b/services/web/app/coffee/Features/Exports/ExportsController.coffee @@ -13,11 +13,10 @@ module.exports = user_id: user_id } - if req.body && req.body.firstName && req.body.lastName - export_params.first_name = req.body.firstName.trim() - export_params.last_name = req.body.lastName.trim() - # additional parameters for gallery exports if req.body + export_params.first_name = req.body.firstName.trim() if req.body.firstName + export_params.last_name = req.body.lastName.trim() if req.body.lastName + # additional parameters for gallery exports export_params.title = req.body.title.trim() if req.body.title export_params.description = req.body.description.trim() if req.body.description export_params.author = req.body.author.trim() if req.body.author From 915bd18058d0bd3367aa41dbd77796b0de5e8485 Mon Sep 17 00:00:00 2001 From: Chrystal Griffiths Date: Fri, 14 Sep 2018 11:47:18 +0100 Subject: [PATCH 040/122] Read-only collaborators table --- services/web/app/views/project/editor/share.pug | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index 737cedf5cc..17da2df67a 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -76,7 +76,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') .col-xs-3.text-left span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")} span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")} - .col-xs-1 + .col-xs-1(ng-show="isAdmin") a( href tooltip=translate('remove_collaborator') @@ -178,7 +178,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') p.small(ng-show="startedFreeTrial") | #{translate("refresh_page_after_starting_free_trial")} .row.public-access-level.public-access-level--notice(ng-show="!isAdmin") - .col-xs-12.text-center To add more collaborators or turn on link sharing, please ask the project owner + .col-xs-12.text-center #{translate("to_add_more_collaborators")} .modal-footer.modal-footer-share .modal-footer-left i.fa.fa-refresh.fa-spin(ng-show="state.inflight") From 41b92d4647396be47b9395720577575842c326e1 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Thu, 13 Sep 2018 17:31:35 +0100 Subject: [PATCH 041/122] prevent calls to UserGetter.getUser with null query --- services/web/app/coffee/Features/User/UserGetter.coffee | 1 + services/web/test/unit/coffee/User/UserGetterTests.coffee | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/services/web/app/coffee/Features/User/UserGetter.coffee b/services/web/app/coffee/Features/User/UserGetter.coffee index f3aa4ad4d6..202e1beed5 100644 --- a/services/web/app/coffee/Features/User/UserGetter.coffee +++ b/services/web/app/coffee/Features/User/UserGetter.coffee @@ -8,6 +8,7 @@ Errors = require("../Errors/Errors") module.exports = UserGetter = getUser: (query, projection, callback = (error, user) ->) -> + return callback(new Error("no query provided")) unless query? if query?.email? return callback(new Error("Don't use getUser to find user by email"), null) if arguments.length == 2 diff --git a/services/web/test/unit/coffee/User/UserGetterTests.coffee b/services/web/test/unit/coffee/User/UserGetterTests.coffee index 79d9032283..c00dc0053a 100644 --- a/services/web/test/unit/coffee/User/UserGetterTests.coffee +++ b/services/web/test/unit/coffee/User/UserGetterTests.coffee @@ -48,6 +48,11 @@ describe "UserGetter", -> error.should.exist done() + it "should not allow null query", (done)-> + @UserGetter.getUser null, {}, (error, user) => + error.should.exist + done() + describe "getUserFullEmails", -> it "should get user", (done)-> @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @fakeUser) From 2e4d3d7aabb62377fab1a4a7667fb13e9e266e65 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Fri, 14 Sep 2018 14:09:43 +0100 Subject: [PATCH 042/122] change links to v1 to sign user in first --- services/web/app/views/project/list.pug | 2 +- services/web/app/views/project/list/modals.pug | 2 +- services/web/app/views/project/list/v1-item.pug | 2 +- services/web/app/views/subscriptions/dashboard.pug | 4 ++-- services/web/app/views/user/settings.pug | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index b1d903ca49..92453e768f 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -98,7 +98,7 @@ block content | To tag or rename your v1 projects, please go back to Overleaf v1. div(ng-show="visible") a.project-list-sidebar-v1-link( - href=settings.overleaf.host + "/dash?prefer-v1-dash=1" + href='/sign_in_to_v1?return_to=%2Fdash%3Fprefer-v1-dash%3D1' ) Go back to v1 if userIsFromSL(user) div(ng-show="visible") diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 2c1f30bbbb..c82c3bdb91 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -356,7 +356,7 @@ script(type="text/ng-template", id="v1ImportModalTemplate") form-messages(for="v1ImportForm") if settings.overleaf && settings.overleaf.host a.btn.btn-primary.v1-import-btn( - ng-href=settings.overleaf.host + "/{{project.id}}" + ng-href='/sign_in_to_v1?return_to=%2F{{project.id}}' ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.response.success}" ) No thanks, open in v1 input.btn.btn-primary.v1-import-btn( diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug index 5a8e37bca0..3eb621f977 100644 --- a/services/web/app/views/project/list/v1-item.pug +++ b/services/web/app/views/project/list/v1-item.pug @@ -13,7 +13,7 @@ ng-show="project.accessLevel == 'owner'" ) {{project.name}} a.projectName( - href=settings.overleaf.host + "/{{project.id}}" + href='/sign_in_to_v1?return_to=%2F{{project.id}}' target="_blank" ng-hide="project.accessLevel == 'owner'" ) {{project.name}} diff --git a/services/web/app/views/subscriptions/dashboard.pug b/services/web/app/views/subscriptions/dashboard.pug index 36a79820de..00be7b0952 100644 --- a/services/web/app/views/subscriptions/dashboard.pug +++ b/services/web/app/views/subscriptions/dashboard.pug @@ -122,7 +122,7 @@ block content p | You are subscribed to Overleaf through Overleaf v1 p - a.btn.btn-primary(href=settings.overleaf.host+"/users/edit#status") Manage v1 Subscription + a.btn.btn-primary(href='/sign_in_to_v1?return_to=%2Fusers%2Fedit%23status') Manage v1 Subscription hr if settings.overleaf && v1Subscriptions && v1Subscriptions.teams && v1Subscriptions.teams.length > 0 @@ -130,7 +130,7 @@ block content p | You are a member of the Overleaf v1 team: #{team.name} p - a.btn.btn-primary(href=settings.overleaf.host+"/teams") Manage v1 Team Membership + a.btn.btn-primary(href="/sign_in_to_v1?return_to=%2Fteams") Manage v1 Team Membership hr .card(ng-if="view == 'cancelation'") diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index b6f4818d7f..d671709c9d 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -127,7 +127,7 @@ block content h3 #{translate("change_password")} p | To change your password, - | please go to #[a(href=settings.overleaf.host+'/users/edit') Overleaf v1 settings] + | please go to #[a(href='/sign_in_to_v1?return_to=%2Fusers%2Fedit%23details') Overleaf v1 settings] | !{moduleIncludes("userSettings", locals)} @@ -193,7 +193,7 @@ block content if settings.createV1AccountOnLogin && settings.overleaf p strong - | This will also delete your user account on #[a(href=settings.overleaf.host target="_blank") Overleaf v1]. + | This will also delete your user account on #[a(href='/sign_in_to_v1?return_to=%2Fdash%3Fprefer-v1-dash%3D1' target="_blank") Overleaf v1]. | If you want to remove your projects from Overleaf v1, you must do this before you | delete your account by going to your My Projects page in Overleaf v1, moving your | projects to the Trash, and then from there either ‘leaving’ or ‘purging’ them, as appropriate. @@ -244,7 +244,7 @@ block content div.alert.alert-info | If you can't remember your password, or if you are using Single-Sign-On with another provider | to sign in (such as Twitter or Google), please - | #[a(href=settings.overleaf.host+'/users/password/new', target='_blank') reset your password], + | #[a(href='/sign_in_to_v1?return_to=%2Fusers%2Fpassword%2Fnew', target='_blank') reset your password], | and try again. .modal-footer button.btn.btn-default( From 09c92c0b69877d6c2d1f1ae59331aefe92abfbf4 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Fri, 14 Sep 2018 16:09:24 +0100 Subject: [PATCH 043/122] don't encode / --- services/web/app/views/project/list.pug | 2 +- services/web/app/views/project/list/modals.pug | 2 +- services/web/app/views/project/list/v1-item.pug | 2 +- services/web/app/views/subscriptions/dashboard.pug | 4 ++-- services/web/app/views/user/settings.pug | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index 92453e768f..984e304d1a 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -98,7 +98,7 @@ block content | To tag or rename your v1 projects, please go back to Overleaf v1. div(ng-show="visible") a.project-list-sidebar-v1-link( - href='/sign_in_to_v1?return_to=%2Fdash%3Fprefer-v1-dash%3D1' + href='/sign_in_to_v1?return_to=/dash%3Fprefer-v1-dash%3D1' ) Go back to v1 if userIsFromSL(user) div(ng-show="visible") diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index c82c3bdb91..50d7f8d2ef 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -356,7 +356,7 @@ script(type="text/ng-template", id="v1ImportModalTemplate") form-messages(for="v1ImportForm") if settings.overleaf && settings.overleaf.host a.btn.btn-primary.v1-import-btn( - ng-href='/sign_in_to_v1?return_to=%2F{{project.id}}' + ng-href='/sign_in_to_v1?return_to=/{{project.id}}' ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.response.success}" ) No thanks, open in v1 input.btn.btn-primary.v1-import-btn( diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug index 3eb621f977..66c609ef2e 100644 --- a/services/web/app/views/project/list/v1-item.pug +++ b/services/web/app/views/project/list/v1-item.pug @@ -13,7 +13,7 @@ ng-show="project.accessLevel == 'owner'" ) {{project.name}} a.projectName( - href='/sign_in_to_v1?return_to=%2F{{project.id}}' + href='/sign_in_to_v1?return_to=/{{project.id}}' target="_blank" ng-hide="project.accessLevel == 'owner'" ) {{project.name}} diff --git a/services/web/app/views/subscriptions/dashboard.pug b/services/web/app/views/subscriptions/dashboard.pug index 00be7b0952..3cd98ae184 100644 --- a/services/web/app/views/subscriptions/dashboard.pug +++ b/services/web/app/views/subscriptions/dashboard.pug @@ -122,7 +122,7 @@ block content p | You are subscribed to Overleaf through Overleaf v1 p - a.btn.btn-primary(href='/sign_in_to_v1?return_to=%2Fusers%2Fedit%23status') Manage v1 Subscription + a.btn.btn-primary(href='/sign_in_to_v1?return_to=/users/edit%23status') Manage v1 Subscription hr if settings.overleaf && v1Subscriptions && v1Subscriptions.teams && v1Subscriptions.teams.length > 0 @@ -130,7 +130,7 @@ block content p | You are a member of the Overleaf v1 team: #{team.name} p - a.btn.btn-primary(href="/sign_in_to_v1?return_to=%2Fteams") Manage v1 Team Membership + a.btn.btn-primary(href="/sign_in_to_v1?return_to=/teams") Manage v1 Team Membership hr .card(ng-if="view == 'cancelation'") diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index d671709c9d..9acf763bfe 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -127,7 +127,7 @@ block content h3 #{translate("change_password")} p | To change your password, - | please go to #[a(href='/sign_in_to_v1?return_to=%2Fusers%2Fedit%23details') Overleaf v1 settings] + | please go to #[a(href='/sign_in_to_v1?return_to=/users/edit%23details') Overleaf v1 settings] | !{moduleIncludes("userSettings", locals)} @@ -193,7 +193,7 @@ block content if settings.createV1AccountOnLogin && settings.overleaf p strong - | This will also delete your user account on #[a(href='/sign_in_to_v1?return_to=%2Fdash%3Fprefer-v1-dash%3D1' target="_blank") Overleaf v1]. + | This will also delete your user account on #[a(href='/sign_in_to_v1?return_to=/dash%3Fprefer-v1-dash%3D1' target="_blank") Overleaf v1]. | If you want to remove your projects from Overleaf v1, you must do this before you | delete your account by going to your My Projects page in Overleaf v1, moving your | projects to the Trash, and then from there either ‘leaving’ or ‘purging’ them, as appropriate. @@ -244,7 +244,7 @@ block content div.alert.alert-info | If you can't remember your password, or if you are using Single-Sign-On with another provider | to sign in (such as Twitter or Google), please - | #[a(href='/sign_in_to_v1?return_to=%2Fusers%2Fpassword%2Fnew', target='_blank') reset your password], + | #[a(href='/sign_in_to_v1?return_to=/users/password/new', target='_blank') reset your password], | and try again. .modal-footer button.btn.btn-default( From bcd465a35de82920c92fee7f68e1b8fada957e47 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Fri, 14 Sep 2018 10:24:06 -0500 Subject: [PATCH 044/122] Allow