From 00e792b022a10e5d80f95e199dba10e8f39c0391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Alby?= Date: Tue, 14 Dec 2021 14:26:16 +0100 Subject: [PATCH] Merge pull request #6093 from overleaf/jpa-last-user-activity [web] add a last active date for denoting account activity in groups mgt GitOrigin-RevId: 32a9e5c1e8f63e794bf6d379685b6dda7e6d4d22 --- .../src/Features/Project/ProjectController.js | 6 ++++++ .../UserMembership/UserMembershipController.js | 2 +- .../UserMembership/UserMembershipViewModel.js | 2 ++ services/web/app/src/models/User.js | 1 + services/web/app/views/user_membership/index.pug | 10 ++++++++-- services/web/locales/en.json | 2 ++ .../unit/src/Project/ProjectControllerTests.js | 16 ++++++++++++++-- .../UserMembershipControllerTests.js | 4 +++- .../UserMembershipViewModelTests.js | 14 ++++++++++---- 9 files changed, 47 insertions(+), 10 deletions(-) diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index b4af42296d..3f76088a37 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -664,6 +664,12 @@ const ProjectController = { if (userId == null) { cb(null, defaultSettingsForAnonymousUser(userId)) } else { + User.updateOne( + { _id: ObjectId(userId) }, + { $set: { lastActive: new Date() } }, + {}, + () => {} + ) User.findById( userId, 'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace', diff --git a/services/web/app/src/Features/UserMembership/UserMembershipController.js b/services/web/app/src/Features/UserMembership/UserMembershipController.js index 050b235509..43ffef3f4c 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipController.js +++ b/services/web/app/src/Features/UserMembership/UserMembershipController.js @@ -137,7 +137,7 @@ module.exports = { exportCsv(req, res, next) { const { entity, entityConfig } = req - const fields = ['email', 'last_logged_in_at'] + const fields = ['email', 'last_logged_in_at', 'last_active_at'] return UserMembershipHandler.getUsers( entity, diff --git a/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js index 66653e6e39..8145cbcf4a 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js +++ b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js @@ -38,6 +38,7 @@ module.exports = UserMembershipViewModel = { first_name: 1, last_name: 1, lastLoggedIn: 1, + lastActive: 1, } return UserGetter.getUser(userId, projection, function (error, user) { if (error != null || user == null) { @@ -57,6 +58,7 @@ function buildUserViewModel(user, isInvite) { email: user.email || null, first_name: user.first_name || null, last_name: user.last_name || null, + last_active_at: user.lastActive || user.lastLoggedIn || null, last_logged_in_at: user.lastLoggedIn || null, invite: isInvite, } diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js index cbf0493bb2..7321f6be82 100644 --- a/services/web/app/src/models/User.js +++ b/services/web/app/src/models/User.js @@ -56,6 +56,7 @@ const UserSchema = new Schema({ return new Date() }, }, + lastActive: { type: Date }, lastLoggedIn: { type: Date }, lastLoginIp: { type: String, default: '' }, loginCount: { type: Number, default: 0 }, diff --git a/services/web/app/views/user_membership/index.pug b/services/web/app/views/user_membership/index.pug index ee0583e52b..8e7b6b2cb4 100644 --- a/services/web/app/views/user_membership/index.pug +++ b/services/web/app/views/user_membership/index.pug @@ -44,7 +44,13 @@ block content .col-md-4 span.header #{translate("name")} .col-md-2 - span.header #{translate("last_login")} + span.header( + tooltip=translate('last_active_description') + tooltip-placement="left" + tooltip-append-to-body="true" + ) + | #{translate("last_active")} + sup (?) .col-md-2 span.header #{translate("accepted_invite")} li.container-fluid( @@ -62,7 +68,7 @@ block content .col-md-4 span.name {{ user.first_name }} {{ user.last_name }} .col-md-2 - span.lastLogin {{ user.last_logged_in_at | formatDate:'Do MMM YYYY' }} + span.lastLogin {{ user.last_active_at | formatDate:'Do MMM YYYY' }} .col-md-2 span.registered i.fa.fa-check.text-success(ng-show="!user.invite" aria-hidden="true") diff --git a/services/web/locales/en.json b/services/web/locales/en.json index b77aa0b0e1..1aa8dda74a 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -64,6 +64,8 @@ "wed_love_you_to_stay": "We’d love you to stay", "yes_move_me_to_student_plan": "Yes, move me to the student plan", "last_login": "Last Login", + "last_active": "Last Active", + "last_active_description": "Last time a project was opened.", "thank_you_for_being_part_of_our_beta_program": "Thank you for being part of our Beta Program, where you can have early access to new features and help us understand your needs better", "you_will_be_able_to_contact_us_any_time_to_share_your_feedback": "You will be able to contact us any time to share your feedback", "we_may_also_contact_you_from_time_to_time_by_email_with_a_survey": "We may also contact you from time to time by email with a survey, or to see if you would like to participate in other user research initiatives", diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js index 5fff321c81..e4c9cbb0d9 100644 --- a/services/web/test/unit/src/Project/ProjectControllerTests.js +++ b/services/web/test/unit/src/Project/ProjectControllerTests.js @@ -58,7 +58,7 @@ describe('ProjectController', function () { this.LimitationsManager = { hasPaidSubscription: sinon.stub() } this.TagsHandler = { getAllTags: sinon.stub() } this.NotificationsHandler = { getUserNotifications: sinon.stub() } - this.UserModel = { findById: sinon.stub() } + this.UserModel = { findById: sinon.stub(), updateOne: sinon.stub() } this.AuthorizationManager = { getPrivilegeLevelForProject: sinon.stub(), isRestrictedUser: sinon.stub().returns(false), @@ -939,7 +939,7 @@ describe('ProjectController', function () { brandVariationId: '12', } this.user = { - _id: '588f3ddae8ebc1bac07c9fa4', + _id: this.user._id, ace: { fontSize: 'massive', theme: 'sexy', @@ -1048,6 +1048,18 @@ describe('ProjectController', function () { this.ProjectController.loadEditor(this.req, this.res) }) + it('should mark user as active', function (done) { + this.res.render = (pageName, opts) => { + expect(this.UserModel.updateOne).to.have.been.calledOnce + expect(this.UserModel.updateOne.args[0][0]).to.deep.equal({ + _id: ObjectId(this.user._id), + }) + expect(this.UserModel.updateOne.args[0][1].$set.lastActive).to.exist + done() + } + this.ProjectController.loadEditor(this.req, this.res) + }) + it('should mark project as opened', function (done) { this.res.render = (pageName, opts) => { this.ProjectUpdateHandler.markAsOpened diff --git a/services/web/test/unit/src/UserMembership/UserMembershipControllerTests.js b/services/web/test/unit/src/UserMembership/UserMembershipControllerTests.js index 7319ee67a9..803fe7e0a4 100644 --- a/services/web/test/unit/src/UserMembership/UserMembershipControllerTests.js +++ b/services/web/test/unit/src/UserMembership/UserMembershipControllerTests.js @@ -46,11 +46,13 @@ describe('UserMembershipController', function () { _id: 'mock-member-id-1', email: 'mock-email-1@foo.com', last_logged_in_at: '2020-08-09T12:43:11.467Z', + last_active_at: '2021-08-09T12:43:11.467Z', }, { _id: 'mock-member-id-2', email: 'mock-email-2@foo.com', last_logged_in_at: '2020-05-20T10:41:11.407Z', + last_active_at: '2021-05-20T10:41:11.407Z', }, ] @@ -307,7 +309,7 @@ describe('UserMembershipController', function () { it('should export the correct csv', function () { return assertCalledWith( this.res.send, - '"email","last_logged_in_at"\n"mock-email-1@foo.com","2020-08-09T12:43:11.467Z"\n"mock-email-2@foo.com","2020-05-20T10:41:11.407Z"' + '"email","last_logged_in_at","last_active_at"\n"mock-email-1@foo.com","2020-08-09T12:43:11.467Z","2021-08-09T12:43:11.467Z"\n"mock-email-2@foo.com","2020-05-20T10:41:11.407Z","2021-05-20T10:41:11.407Z"' ) }) }) diff --git a/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js b/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js index 39d073b1f2..142944a96e 100644 --- a/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js +++ b/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js @@ -48,6 +48,7 @@ describe('UserMembershipViewModel', function () { return expect(viewModel).to.deep.equal({ email: this.email, invite: true, + last_active_at: null, last_logged_in_at: null, first_name: null, last_name: null, @@ -57,10 +58,15 @@ describe('UserMembershipViewModel', function () { it('build user', function () { const viewModel = this.UserMembershipViewModel.build(this.user) - expect(viewModel._id).to.equal(this.user._id) - expect(viewModel.email).to.equal(this.user.email) - expect(viewModel.last_logged_in_at).to.equal(this.user.lastLoggedIn) - return expect(viewModel.invite).to.equal(false) + expect(viewModel).to.deep.equal({ + email: this.user.email, + invite: false, + last_active_at: this.user.lastLoggedIn, + last_logged_in_at: this.user.lastLoggedIn, + first_name: this.user.first_name, + last_name: null, + _id: this.user._id, + }) }) })