Răsfoiți Sursa

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
Timothée Alby 4 ani în urmă
părinte
comite
00e792b022

+ 6 - 0
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',

+ 1 - 1
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,

+ 2 - 0
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,
   }

+ 1 - 0
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 },

+ 8 - 2
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")

+ 2 - 0
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",

+ 14 - 2
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

+ 3 - 1
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"'
       )
     })
   })

+ 10 - 4
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,
+      })
     })
   })