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) {
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',

View file

@ -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,

View file

@ -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,
}

View file

@ -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 },

View file

@ -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")

View file

@ -64,6 +64,8 @@
"wed_love_you_to_stay": "Wed 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",

View file

@ -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

View file

@ -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"'
)
})
})

View file

@ -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,
})
})
})