mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-06 07:39:02 +02:00
Merge pull request #30418 from overleaf/mfb-improve-handling-of-debug-copies-of-user-projects
Add isDebugCopyOf property to project, add Debug tag to debug project. GitOrigin-RevId: e3d17de05c6f31db16b861d1adae333211dff018
This commit is contained in:
committed by
Copybot
parent
5829a7fe43
commit
a591f2eb7a
@@ -238,6 +238,28 @@ function personalAndGroupSubscriptions(userId) {
|
||||
}
|
||||
}
|
||||
|
||||
function oldDebugProjects(userId) {
|
||||
return {
|
||||
key: `old-debug-projects-${userId}`,
|
||||
async create(userId) {
|
||||
return await NotificationsHandler.promises.createNotification(
|
||||
userId,
|
||||
this.key,
|
||||
'notification_old_debug_projects',
|
||||
{},
|
||||
null,
|
||||
true
|
||||
)
|
||||
},
|
||||
async read() {
|
||||
return await NotificationsHandler.promises.markAsReadWithKey(
|
||||
userId,
|
||||
this.key
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const NotificationsBuilder = {
|
||||
// Note: notification keys should be url-safe
|
||||
dropboxUnlinkedDueToLapsedReconfirmation(userId) {
|
||||
@@ -279,6 +301,7 @@ NotificationsBuilder.promises = {
|
||||
projectInvite,
|
||||
personalAndGroupSubscriptions,
|
||||
tpdsFileLimit,
|
||||
oldDebugProjects,
|
||||
}
|
||||
|
||||
export default NotificationsBuilder
|
||||
|
||||
@@ -259,8 +259,8 @@ const _ProjectController = {
|
||||
res.setTimeout(5 * 60 * 1000) // allow extra time for the copy to complete
|
||||
metrics.inc('cloned-project')
|
||||
const projectId = req.params.Project_id
|
||||
const { projectName, tags } = req.body
|
||||
logger.debug({ projectId, projectName }, 'cloning project')
|
||||
const { projectName, isDebugCopy, tags } = req.body
|
||||
logger.debug({ projectId, projectName, isDebugCopy }, 'cloning project')
|
||||
if (!SessionManager.isUserLoggedIn(req.session)) {
|
||||
return res.json({ redir: '/register' })
|
||||
}
|
||||
@@ -271,7 +271,8 @@ const _ProjectController = {
|
||||
currentUser,
|
||||
projectId,
|
||||
projectName,
|
||||
tags
|
||||
tags,
|
||||
isDebugCopy
|
||||
)
|
||||
ProjectAuditLogHandler.addEntryIfManagedInBackground(
|
||||
projectId,
|
||||
|
||||
@@ -196,6 +196,11 @@ async function _createBlankProject(
|
||||
if (historyRangesSupportAssignment.variant === 'enabled') {
|
||||
project.overleaf.history.rangesSupportEnabled = true
|
||||
}
|
||||
|
||||
if (attributes.isDebugCopyOf) {
|
||||
project.overleaf.isDebugCopyOf = new ObjectId(attributes.isDebugCopyOf)
|
||||
}
|
||||
|
||||
await project.save()
|
||||
if (!skipCreatingInTPDS) {
|
||||
await TpdsUpdateSender.promises.createProject({
|
||||
|
||||
@@ -22,6 +22,9 @@ import TagsHandler from '../Tags/TagsHandler.mjs'
|
||||
import ClsiCacheManager from '../Compile/ClsiCacheManager.mjs'
|
||||
import Modules from '../../infrastructure/Modules.mjs'
|
||||
|
||||
const TAG_COLOR_RED = '#f04343'
|
||||
const DEBUG_TAG_NAME = 'Debug'
|
||||
|
||||
export default {
|
||||
duplicate: callbackify(duplicate),
|
||||
promises: {
|
||||
@@ -29,7 +32,13 @@ export default {
|
||||
},
|
||||
}
|
||||
|
||||
async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
|
||||
async function duplicate(
|
||||
owner,
|
||||
originalProjectId,
|
||||
newProjectName,
|
||||
tags = [],
|
||||
isDebugCopy
|
||||
) {
|
||||
await DocumentUpdaterHandler.promises.flushProjectToMongo(originalProjectId)
|
||||
const originalProject = await ProjectGetter.promises.getProject(
|
||||
originalProjectId,
|
||||
@@ -58,6 +67,18 @@ async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
|
||||
originalEntries,
|
||||
})
|
||||
|
||||
const attributes = {}
|
||||
if (isDebugCopy) {
|
||||
attributes.isDebugCopyOf = originalProjectId
|
||||
// - Create new tag on owner._id if it doesn't already exist
|
||||
const debugTag = await TagsHandler.promises.createTag(
|
||||
owner._id,
|
||||
DEBUG_TAG_NAME,
|
||||
TAG_COLOR_RED,
|
||||
{ truncate: true }
|
||||
)
|
||||
tags.push(debugTag)
|
||||
}
|
||||
// Pass template ID as analytics segmentation if duplicating project from a template
|
||||
const segmentation = _.pick(originalProject, [
|
||||
'fromV1TemplateId',
|
||||
@@ -72,6 +93,9 @@ async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
|
||||
originalProject._id
|
||||
)
|
||||
segmentation['updated-tags'] = tags.length
|
||||
attributes.segmentation = segmentation
|
||||
|
||||
attributes.segmentation = segmentation
|
||||
|
||||
// remove any leading or trailing spaces
|
||||
newProjectName = newProjectName.trim()
|
||||
@@ -80,7 +104,7 @@ async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
|
||||
const newProject = await ProjectCreationHandler.promises.createBlankProject(
|
||||
owner._id,
|
||||
newProjectName,
|
||||
{ segmentation }
|
||||
attributes
|
||||
)
|
||||
|
||||
let prepareClsiCacheInBackground = Promise.resolve()
|
||||
|
||||
@@ -132,6 +132,30 @@ const ProjectGetter = {
|
||||
return filteredProjects
|
||||
},
|
||||
|
||||
async existUsersDebugProjectsOlderThan(userId, days) {
|
||||
const cutoffDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
|
||||
|
||||
const exists = await Project.exists({
|
||||
owner_ref: userId,
|
||||
'overleaf.isDebugCopyOf': { $type: 'objectId' },
|
||||
lastUpdated: { $lt: cutoffDate },
|
||||
})
|
||||
|
||||
return Boolean(exists)
|
||||
},
|
||||
|
||||
async findAllDebugProjects(fields) {
|
||||
return Project.find(
|
||||
{
|
||||
'overleaf.isDebugCopyOf': { $type: 'objectId' },
|
||||
},
|
||||
fields
|
||||
)
|
||||
.limit(500)
|
||||
.populate('owner_ref', ['email', 'name'])
|
||||
.exec()
|
||||
},
|
||||
|
||||
/**
|
||||
* Return all projects with the given name that belong to the given user.
|
||||
*
|
||||
|
||||
@@ -164,9 +164,10 @@ async function projectListPage(req, res, next) {
|
||||
logger.err({ err, userId }, 'projects listing in background failed')
|
||||
return undefined
|
||||
})
|
||||
|
||||
const user = await User.findById(
|
||||
userId,
|
||||
`email emails features alphaProgram betaProgram lastPrimaryEmailCheck lastActive signUpDate ace refProviders${
|
||||
`email isAdmin emails features alphaProgram betaProgram lastPrimaryEmailCheck lastActive signUpDate ace refProviders${
|
||||
isSaas ? ' enrollment writefull completedTutorials aiErrorAssistant' : ''
|
||||
}`
|
||||
)
|
||||
@@ -187,6 +188,8 @@ async function projectListPage(req, res, next) {
|
||||
let role
|
||||
|
||||
if (isSaas) {
|
||||
if (user.isAdmin) await _checkForOldDebugProjects(userId)
|
||||
|
||||
await SplitTestSessionHandler.promises.sessionMaintenance(req, user)
|
||||
|
||||
try {
|
||||
@@ -595,6 +598,20 @@ async function getProjectsJson(req, res) {
|
||||
res.json(projectsPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @private
|
||||
*/
|
||||
async function _checkForOldDebugProjects(userId) {
|
||||
const exists = await ProjectGetter.promises.existUsersDebugProjectsOlderThan(
|
||||
userId,
|
||||
7
|
||||
)
|
||||
if (exists) {
|
||||
await NotificationsBuilder.promises.oldDebugProjects(userId).create(userId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {Filters} filters
|
||||
|
||||
@@ -6,7 +6,10 @@ import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.mjs'
|
||||
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.mjs'
|
||||
import EditorRealTimeController from '../Editor/EditorRealTimeController.mjs'
|
||||
import SystemMessageManager from '../SystemMessages/SystemMessageManager.mjs'
|
||||
import ProjectGetter from '../Project/ProjectGetter.mjs'
|
||||
import Modules from '../../infrastructure/Modules.mjs'
|
||||
import Features from '../../infrastructure/Features.mjs'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
|
||||
const AdminController = {
|
||||
_sendDisconnectAllUsersMessage: delay => {
|
||||
@@ -16,7 +19,7 @@ const AdminController = {
|
||||
delay
|
||||
)
|
||||
},
|
||||
index: (req, res, next) => {
|
||||
index: expressify(async (req, res, next) => {
|
||||
let url
|
||||
const openSockets = {}
|
||||
for (url in http.globalAgent.sockets) {
|
||||
@@ -31,24 +34,30 @@ const AdminController = {
|
||||
)
|
||||
}
|
||||
|
||||
SystemMessageManager.getMessagesFromDB(
|
||||
async function (error, systemMessages) {
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
const privilegesMatrixResults = await Modules.promises.hooks.fire(
|
||||
'getPrivilegesMatrix'
|
||||
)
|
||||
const privilegesMatrix = privilegesMatrixResults[0] || null
|
||||
res.render('admin/index', {
|
||||
title: 'System Admin',
|
||||
openSockets,
|
||||
systemMessages,
|
||||
privilegesMatrix,
|
||||
})
|
||||
}
|
||||
const systemMessages =
|
||||
await SystemMessageManager.promises.getMessagesFromDB()
|
||||
|
||||
const privilegesMatrixResults = await Modules.promises.hooks.fire(
|
||||
'getPrivilegesMatrix'
|
||||
)
|
||||
},
|
||||
|
||||
const privilegesMatrix = privilegesMatrixResults[0] || null
|
||||
|
||||
const toRender = {
|
||||
title: 'System Admin',
|
||||
openSockets,
|
||||
systemMessages,
|
||||
privilegesMatrix,
|
||||
}
|
||||
|
||||
if (Features.hasFeature('saas')) {
|
||||
const debugProjects = await ProjectGetter.promises.findAllDebugProjects(
|
||||
'name lastUpdated owner_ref'
|
||||
)
|
||||
toRender.debugProjects = debugProjects
|
||||
}
|
||||
res.render('admin/index', toRender)
|
||||
}),
|
||||
|
||||
disconnectAllUsers: (req, res) => {
|
||||
logger.warn('disconecting everyone')
|
||||
|
||||
@@ -19,6 +19,7 @@ block content
|
||||
+bookmarkable-tabset-header('privileges-matrix', 'Privileges Matrix')
|
||||
if hasFeature('saas')
|
||||
+bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management')
|
||||
+bookmarkable-tabset-header('debug-projects', 'Debug Projects')
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active(role='tabpanel' id='system-messages')
|
||||
@@ -133,3 +134,20 @@ block content
|
||||
input.form-control(name='user_id' id='user-id' type='text' required)
|
||||
.form-group
|
||||
button.btn-primary.btn(type='submit') Poll
|
||||
|
||||
.tab-pane(role='tabpanel' id='debug-projects')
|
||||
if debugProjects.length
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th Owner
|
||||
th Project Name
|
||||
th Last updated
|
||||
tbody
|
||||
each project in debugProjects
|
||||
tr
|
||||
td= project.owner_ref.email
|
||||
td= project.name
|
||||
td= moment(project.lastUpdated.toUTCString()).format('Do MMM YYYY, h:mm a') + ' UTC'
|
||||
else
|
||||
p.text-muted No debug projects found
|
||||
|
||||
+6
@@ -287,6 +287,12 @@ function CommonNotification({ notification }: CommonNotificationProps) {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : templateKey === 'notification_old_debug_projects' ? (
|
||||
<Notification
|
||||
type="warning"
|
||||
onDismiss={() => id && handleDismiss(id)}
|
||||
content={html}
|
||||
/>
|
||||
) : (
|
||||
<Notification
|
||||
type="info"
|
||||
|
||||
@@ -1542,6 +1542,7 @@
|
||||
"note_features_under_development": "<0>Please note</0> that features in this program are still being tested and actively developed. This means that they might <0>change</0>, be <0>removed</0> or <0>become part of a premium plan</0>",
|
||||
"notification": "Notification",
|
||||
"notification_features_upgraded_by_affiliation": "Good news! Your affiliated organization __institutionName__ has an Overleaf subscription, and you now have access to all of Overleaf’s Professional features.",
|
||||
"notification_old_debug_projects": "You have some old debug projects that you should delete, if you no longer need them.",
|
||||
"notification_personal_and_group_subscriptions": "We’ve spotted that you’ve got <0>more than one active __appName__ subscription</0>. To avoid paying more than you need to, <1>review your subscriptions</1>.",
|
||||
"notification_personal_subscription_not_required_due_to_affiliation": " Good news! Your affiliated organization __institutionName__ has an Overleaf subscription, and you now have access to Overleaf’s Professional features through your affiliation. You can cancel your individual subscription without losing access to any features.",
|
||||
"notification_project_invite_accepted_message": "You’ve joined <b>__projectName__</b>",
|
||||
|
||||
@@ -20,10 +20,13 @@ describe('ProjectGetter', function () {
|
||||
ctx.Project = {
|
||||
find: sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(),
|
||||
populate: sinon.stub().returnsThis(),
|
||||
limit: sinon.stub().returnsThis(),
|
||||
}),
|
||||
findOne: sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(ctx.project),
|
||||
}),
|
||||
exists: sinon.stub().returns({ exec: sinon.stub().resolves(true) }),
|
||||
}
|
||||
ctx.CollaboratorsGetter = {
|
||||
promises: {
|
||||
@@ -458,4 +461,39 @@ describe('ProjectGetter', function () {
|
||||
expect(docs).to.deep.equal([ctx.deletedProject])
|
||||
})
|
||||
})
|
||||
|
||||
describe('findAllDebugProjects', function () {
|
||||
it('should find all projects with overleaf.isDebugCopyOf of type objectId', async function (ctx) {
|
||||
await ctx.ProjectGetter.promises.findAllDebugProjects('fields')
|
||||
sinon.assert.calledWith(ctx.Project.find, {
|
||||
'overleaf.isDebugCopyOf': { $type: 'objectId' },
|
||||
})
|
||||
sinon.assert.calledWith(ctx.Project.find().populate, 'owner_ref', [
|
||||
'email',
|
||||
'name',
|
||||
])
|
||||
sinon.assert.calledOnce(ctx.Project.find().exec)
|
||||
})
|
||||
})
|
||||
|
||||
describe('existUsersDebugProjectsOlderThan', function () {
|
||||
it('should check for existence of debug projects older than given days', async function (ctx) {
|
||||
const days = 10
|
||||
const cutoffDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
|
||||
|
||||
const exists =
|
||||
await ctx.ProjectGetter.promises.existUsersDebugProjectsOlderThan(
|
||||
ctx.userId,
|
||||
days
|
||||
)
|
||||
|
||||
sinon.assert.calledWith(ctx.Project.exists, {
|
||||
owner_ref: ctx.userId,
|
||||
'overleaf.isDebugCopyOf': { $type: 'objectId' },
|
||||
lastUpdated: { $lt: cutoffDate },
|
||||
})
|
||||
|
||||
expect(exists).to.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user