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
This commit is contained in:
Timothée Alby 2021-12-14 14:26:16 +01:00 committed by Copybot
parent 5eed78a3e8
commit 00e792b022
9 changed files with 47 additions and 10 deletions

View file

@ -664,6 +664,12 @@ const ProjectController = {
if (userId == null) { if (userId == null) {
cb(null, defaultSettingsForAnonymousUser(userId)) cb(null, defaultSettingsForAnonymousUser(userId))
} else { } else {
User.updateOne(
{ _id: ObjectId(userId) },
{ $set: { lastActive: new Date() } },
{},
() => {}
)
User.findById( User.findById(
userId, userId,
'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace', 'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace',

View file

@ -137,7 +137,7 @@ module.exports = {
exportCsv(req, res, next) { exportCsv(req, res, next) {
const { entity, entityConfig } = req const { entity, entityConfig } = req
const fields = ['email', 'last_logged_in_at'] const fields = ['email', 'last_logged_in_at', 'last_active_at']
return UserMembershipHandler.getUsers( return UserMembershipHandler.getUsers(
entity, entity,

View file

@ -38,6 +38,7 @@ module.exports = UserMembershipViewModel = {
first_name: 1, first_name: 1,
last_name: 1, last_name: 1,
lastLoggedIn: 1, lastLoggedIn: 1,
lastActive: 1,
} }
return UserGetter.getUser(userId, projection, function (error, user) { return UserGetter.getUser(userId, projection, function (error, user) {
if (error != null || user == null) { if (error != null || user == null) {
@ -57,6 +58,7 @@ function buildUserViewModel(user, isInvite) {
email: user.email || null, email: user.email || null,
first_name: user.first_name || null, first_name: user.first_name || null,
last_name: user.last_name || null, last_name: user.last_name || null,
last_active_at: user.lastActive || user.lastLoggedIn || null,
last_logged_in_at: user.lastLoggedIn || null, last_logged_in_at: user.lastLoggedIn || null,
invite: isInvite, invite: isInvite,
} }

View file

@ -56,6 +56,7 @@ const UserSchema = new Schema({
return new Date() return new Date()
}, },
}, },
lastActive: { type: Date },
lastLoggedIn: { type: Date }, lastLoggedIn: { type: Date },
lastLoginIp: { type: String, default: '' }, lastLoginIp: { type: String, default: '' },
loginCount: { type: Number, default: 0 }, loginCount: { type: Number, default: 0 },

View file

@ -44,7 +44,13 @@ block content
.col-md-4 .col-md-4
span.header #{translate("name")} span.header #{translate("name")}
.col-md-2 .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 .col-md-2
span.header #{translate("accepted_invite")} span.header #{translate("accepted_invite")}
li.container-fluid( li.container-fluid(
@ -62,7 +68,7 @@ block content
.col-md-4 .col-md-4
span.name {{ user.first_name }} {{ user.last_name }} span.name {{ user.first_name }} {{ user.last_name }}
.col-md-2 .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 .col-md-2
span.registered span.registered
i.fa.fa-check.text-success(ng-show="!user.invite" aria-hidden="true") i.fa.fa-check.text-success(ng-show="!user.invite" aria-hidden="true")

View file

@ -64,6 +64,8 @@
"wed_love_you_to_stay": "Wed love you to stay", "wed_love_you_to_stay": "Wed love you to stay",
"yes_move_me_to_student_plan": "Yes, move me to the student plan", "yes_move_me_to_student_plan": "Yes, move me to the student plan",
"last_login": "Last Login", "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", "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", "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", "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",

View file

@ -58,7 +58,7 @@ describe('ProjectController', function () {
this.LimitationsManager = { hasPaidSubscription: sinon.stub() } this.LimitationsManager = { hasPaidSubscription: sinon.stub() }
this.TagsHandler = { getAllTags: sinon.stub() } this.TagsHandler = { getAllTags: sinon.stub() }
this.NotificationsHandler = { getUserNotifications: sinon.stub() } this.NotificationsHandler = { getUserNotifications: sinon.stub() }
this.UserModel = { findById: sinon.stub() } this.UserModel = { findById: sinon.stub(), updateOne: sinon.stub() }
this.AuthorizationManager = { this.AuthorizationManager = {
getPrivilegeLevelForProject: sinon.stub(), getPrivilegeLevelForProject: sinon.stub(),
isRestrictedUser: sinon.stub().returns(false), isRestrictedUser: sinon.stub().returns(false),
@ -939,7 +939,7 @@ describe('ProjectController', function () {
brandVariationId: '12', brandVariationId: '12',
} }
this.user = { this.user = {
_id: '588f3ddae8ebc1bac07c9fa4', _id: this.user._id,
ace: { ace: {
fontSize: 'massive', fontSize: 'massive',
theme: 'sexy', theme: 'sexy',
@ -1048,6 +1048,18 @@ describe('ProjectController', function () {
this.ProjectController.loadEditor(this.req, this.res) 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) { it('should mark project as opened', function (done) {
this.res.render = (pageName, opts) => { this.res.render = (pageName, opts) => {
this.ProjectUpdateHandler.markAsOpened this.ProjectUpdateHandler.markAsOpened

View file

@ -46,11 +46,13 @@ describe('UserMembershipController', function () {
_id: 'mock-member-id-1', _id: 'mock-member-id-1',
email: 'mock-email-1@foo.com', email: 'mock-email-1@foo.com',
last_logged_in_at: '2020-08-09T12:43:11.467Z', last_logged_in_at: '2020-08-09T12:43:11.467Z',
last_active_at: '2021-08-09T12:43:11.467Z',
}, },
{ {
_id: 'mock-member-id-2', _id: 'mock-member-id-2',
email: 'mock-email-2@foo.com', email: 'mock-email-2@foo.com',
last_logged_in_at: '2020-05-20T10:41:11.407Z', 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 () { it('should export the correct csv', function () {
return assertCalledWith( return assertCalledWith(
this.res.send, 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"'
) )
}) })
}) })

View file

@ -48,6 +48,7 @@ describe('UserMembershipViewModel', function () {
return expect(viewModel).to.deep.equal({ return expect(viewModel).to.deep.equal({
email: this.email, email: this.email,
invite: true, invite: true,
last_active_at: null,
last_logged_in_at: null, last_logged_in_at: null,
first_name: null, first_name: null,
last_name: null, last_name: null,
@ -57,10 +58,15 @@ describe('UserMembershipViewModel', function () {
it('build user', function () { it('build user', function () {
const viewModel = this.UserMembershipViewModel.build(this.user) const viewModel = this.UserMembershipViewModel.build(this.user)
expect(viewModel._id).to.equal(this.user._id) expect(viewModel).to.deep.equal({
expect(viewModel.email).to.equal(this.user.email) email: this.user.email,
expect(viewModel.last_logged_in_at).to.equal(this.user.lastLoggedIn) invite: false,
return expect(viewModel.invite).to.equal(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,
})
}) })
}) })