mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 21:23:45 -05:00
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:
parent
5eed78a3e8
commit
00e792b022
9 changed files with 47 additions and 10 deletions
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -64,6 +64,8 @@
|
||||||
"wed_love_you_to_stay": "We’d love you to stay",
|
"wed_love_you_to_stay": "We’d 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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue