Merge pull request #3917 from overleaf/ab-example-project-ab-test

Example Project Split Test

GitOrigin-RevId: a9d68811c878e32b92e0547311c9e477e4096135
This commit is contained in:
Alexandre Bourdin 2021-04-27 10:59:46 +02:00 committed by Copybot
parent 1be43911b4
commit affaae14b7
8 changed files with 417 additions and 898 deletions

View file

@ -1,20 +1,5 @@
/* eslint-disable
camelcase,
node/handle-callback-err,
max-len,
no-path-concat,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const logger = require('logger-sharelatex')
const OError = require('@overleaf/o-error')
const async = require('async')
const metrics = require('@overleaf/metrics')
const Settings = require('settings-sharelatex')
const { ObjectId } = require('mongodb')
@ -25,316 +10,239 @@ const ProjectDetailsHandler = require('./ProjectDetailsHandler')
const HistoryManager = require('../History/HistoryManager')
const { User } = require('../../models/User')
const fs = require('fs')
const Path = require('path')
const { promisify } = require('util')
const path = require('path')
const { callbackify } = require('util')
const _ = require('underscore')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const ProjectCreationHandler = {
createBlankProject(owner_id, projectName, attributes, callback) {
if (callback == null) {
callback = function (error, project) {}
const MONTH_NAMES = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
async function createBlankProject(ownerId, projectName, attributes = {}) {
const isImport = attributes && attributes.overleaf
const project = await _createBlankProject(ownerId, projectName, attributes)
if (isImport) {
AnalyticsManager.recordEvent(ownerId, 'project-imported', {
projectId: project._id,
attributes,
})
} else {
AnalyticsManager.recordEvent(ownerId, 'project-created', {
projectId: project._id,
attributes,
})
}
return project
}
async function createProjectFromSnippet(ownerId, projectName, docLines) {
const project = await _createBlankProject(ownerId, projectName)
AnalyticsManager.recordEvent(ownerId, 'project-created', {
projectId: project._id,
})
await _createRootDoc(project, ownerId, docLines)
return project
}
async function createBasicProject(ownerId, projectName) {
const project = await _createBlankProject(ownerId, projectName)
AnalyticsManager.recordEvent(ownerId, 'project-created', {
projectId: project._id,
})
const docLines = await _buildTemplate('mainbasic.tex', ownerId, projectName)
await _createRootDoc(project, ownerId, docLines)
return project
}
async function createExampleProject(ownerId, projectName) {
const project = await _createBlankProject(ownerId, projectName)
const testSegmentation = SplitTestHandler.getTestSegmentation(
ownerId,
'example-project'
)
if (testSegmentation.variant === 'example-frog') {
await _addSplitTestExampleProjectFiles(ownerId, projectName, project)
} else {
await _addDefaultExampleProjectFiles(ownerId, projectName, project)
}
if (testSegmentation.enabled) {
AnalyticsManager.recordEvent(ownerId, 'project-created', {
projectId: project._id,
splitTestId: 'example-project',
splitTestVariantId: testSegmentation.variant,
})
} else {
AnalyticsManager.recordEvent(ownerId, 'project-created', {
projectId: project._id,
})
}
return project
}
async function _addDefaultExampleProjectFiles(ownerId, projectName, project) {
const mainDocLines = await _buildTemplate('main.tex', ownerId, projectName)
await _createRootDoc(project, ownerId, mainDocLines)
const referenceDocLines = await _buildTemplate(
'references.bib',
ownerId,
projectName
)
await ProjectEntityUpdateHandler.promises.addDoc(
project._id,
project.rootFolder[0]._id,
'references.bib',
referenceDocLines,
ownerId
)
const universePath = path.resolve(
__dirname + '/../../../templates/project_files/universe.jpg'
)
await ProjectEntityUpdateHandler.promises.addFile(
project._id,
project.rootFolder[0]._id,
'universe.jpg',
universePath,
null,
ownerId
)
}
async function _addSplitTestExampleProjectFiles(ownerId, projectName, project) {
const mainDocLines = await _buildTemplate(
'test-example-project/main.tex',
ownerId,
projectName
)
await _createRootDoc(project, ownerId, mainDocLines)
const bibDocLines = await _buildTemplate(
'test-example-project/sample.bib',
ownerId,
projectName
)
await ProjectEntityUpdateHandler.promises.addDoc(
project._id,
project.rootFolder[0]._id,
'sample.bib',
bibDocLines,
ownerId
)
const frogPath = path.resolve(
__dirname +
'/../../../templates/project_files/test-example-project/frog.jpg'
)
await ProjectEntityUpdateHandler.promises.addFile(
project._id,
project.rootFolder[0]._id,
'frog.jpg',
frogPath,
null,
ownerId
)
}
async function _createBlankProject(ownerId, projectName, attributes = {}) {
metrics.inc('project-creation')
await ProjectDetailsHandler.promises.validateProjectName(projectName)
if (!attributes.overleaf) {
const history = await HistoryManager.promises.initializeProject()
attributes.overleaf = {
history: { id: history ? history.overleaf_id : undefined },
}
metrics.inc('project-creation')
if (arguments.length === 3) {
callback = attributes
attributes = {}
}
const rootFolder = new Folder({ name: 'rootFolder' })
attributes.lastUpdatedBy = attributes.owner_ref = new ObjectId(ownerId)
attributes.name = projectName
const project = new Project(attributes)
Object.assign(project, attributes)
if (Settings.apis.project_history.displayHistoryForNewProjects) {
project.overleaf.history.display = true
}
if (Settings.currentImageName) {
// avoid clobbering any imageName already set in attributes (e.g. importedImageName)
if (!project.imageName) {
project.imageName = Settings.currentImageName
}
}
project.rootFolder[0] = rootFolder
const user = await User.findById(ownerId, 'ace.spellCheckLanguage')
project.spellCheckLanguage = user.ace.spellCheckLanguage
return await project.save()
}
return ProjectDetailsHandler.validateProjectName(
projectName,
function (error) {
if (error != null) {
return callback(error)
}
if (attributes.overleaf !== undefined && attributes.overleaf != null) {
return ProjectCreationHandler._createBlankProject(
owner_id,
projectName,
attributes,
function (error, project) {
if (error != null) {
return callback(error)
}
AnalyticsManager.recordEvent(owner_id, 'project-imported', {
projectId: project._id,
attributes,
})
return callback(error, project)
}
)
} else {
return HistoryManager.initializeProject(function (error, history) {
if (error != null) {
return callback(error)
}
attributes.overleaf = {
history: {
id: history != null ? history.overleaf_id : undefined,
},
}
return ProjectCreationHandler._createBlankProject(
owner_id,
projectName,
attributes,
function (error, project) {
if (error != null) {
return callback(error)
}
AnalyticsManager.recordEvent(owner_id, 'project-created', {
projectId: project._id,
attributes,
})
return callback(error, project)
}
)
})
}
}
)
},
_createBlankProject(owner_id, projectName, attributes, callback) {
if (callback == null) {
callback = function (error, project) {}
}
const rootFolder = new Folder({ name: 'rootFolder' })
attributes.lastUpdatedBy = attributes.owner_ref = new ObjectId(owner_id)
attributes.name = projectName
const project = new Project(attributes)
Object.assign(project, attributes)
if (Settings.apis.project_history.displayHistoryForNewProjects) {
project.overleaf.history.display = true
}
if (Settings.currentImageName != null) {
// avoid clobbering any imageName already set in attributes (e.g. importedImageName)
if (project.imageName == null) {
project.imageName = Settings.currentImageName
}
}
project.rootFolder[0] = rootFolder
return User.findById(
owner_id,
'ace.spellCheckLanguage',
function (err, user) {
project.spellCheckLanguage = user.ace.spellCheckLanguage
return project.save(function (err) {
if (err != null) {
return callback(err)
}
return callback(err, project)
})
}
)
},
createProjectFromSnippet(owner_id, projectName, docLines, callback) {
if (callback == null) {
callback = function (error, project) {}
}
return ProjectCreationHandler.createBlankProject(
owner_id,
projectName,
function (error, project) {
if (error != null) {
return callback(error)
}
return ProjectCreationHandler._createRootDoc(
project,
owner_id,
docLines,
callback
)
}
)
},
createBasicProject(owner_id, projectName, callback) {
if (callback == null) {
callback = function (error, project) {}
}
return ProjectCreationHandler.createBlankProject(
owner_id,
projectName,
function (error, project) {
if (error != null) {
return callback(error)
}
return ProjectCreationHandler._buildTemplate(
'mainbasic.tex',
owner_id,
projectName,
function (error, docLines) {
if (error != null) {
return callback(error)
}
return ProjectCreationHandler._createRootDoc(
project,
owner_id,
docLines,
callback
)
}
)
}
)
},
createExampleProject(owner_id, projectName, callback) {
if (callback == null) {
callback = function (error, project) {}
}
return ProjectCreationHandler.createBlankProject(
owner_id,
projectName,
function (error, project) {
if (error != null) {
return callback(error)
}
return async.series(
[
callback =>
ProjectCreationHandler._buildTemplate(
'main.tex',
owner_id,
projectName,
function (error, docLines) {
if (error != null) {
return callback(error)
}
return ProjectCreationHandler._createRootDoc(
project,
owner_id,
docLines,
callback
)
}
),
callback =>
ProjectCreationHandler._buildTemplate(
'references.bib',
owner_id,
projectName,
function (error, docLines) {
if (error != null) {
return callback(error)
}
return ProjectEntityUpdateHandler.addDoc(
project._id,
project.rootFolder[0]._id,
'references.bib',
docLines,
owner_id,
(error, doc) => callback(error)
)
}
),
function (callback) {
const universePath = Path.resolve(
__dirname + '/../../../templates/project_files/universe.jpg'
)
return ProjectEntityUpdateHandler.addFile(
project._id,
project.rootFolder[0]._id,
'universe.jpg',
universePath,
null,
owner_id,
callback
)
},
],
error => callback(error, project)
)
}
)
},
_createRootDoc(project, owner_id, docLines, callback) {
if (callback == null) {
callback = function (error, project) {}
}
return ProjectEntityUpdateHandler.addDoc(
async function _createRootDoc(project, ownerId, docLines) {
try {
const { doc } = await ProjectEntityUpdateHandler.promises.addDoc(
project._id,
project.rootFolder[0]._id,
'main.tex',
docLines,
owner_id,
function (error, doc) {
if (error != null) {
OError.tag(error, 'error adding root doc when creating project')
return callback(error)
}
return ProjectEntityUpdateHandler.setRootDoc(
project._id,
doc._id,
error => callback(error, project)
)
}
ownerId
)
},
await ProjectEntityUpdateHandler.promises.setRootDoc(project._id, doc._id)
} catch (error) {
throw OError.tag(error, 'error adding root doc when creating project')
}
}
_buildTemplate(template_name, user_id, project_name, callback) {
if (callback == null) {
callback = function (error, output) {}
}
return User.findById(
user_id,
'first_name last_name',
function (error, user) {
if (error != null) {
return callback(error)
}
const monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
async function _buildTemplate(templateName, userId, projectName) {
const user = await User.findById(userId, 'first_name last_name')
const templatePath = Path.resolve(
__dirname + `/../../../templates/project_files/${template_name}`
)
return fs.readFile(templatePath, function (error, template) {
if (error != null) {
return callback(error)
}
const data = {
project_name,
user,
year: new Date().getUTCFullYear(),
month: monthNames[new Date().getUTCMonth()],
}
const output = _.template(template.toString(), data)
return callback(null, output.split('\n'))
})
}
)
const templatePath = path.resolve(
__dirname + `/../../../templates/project_files/${templateName}`
)
const template = fs.readFileSync(templatePath)
const data = {
project_name: projectName,
user,
year: new Date().getUTCFullYear(),
month: MONTH_NAMES[new Date().getUTCMonth()],
}
const output = _.template(template.toString(), data)
return output.split('\n')
}
module.exports = {
createBlankProject: callbackify(createBlankProject),
createProjectFromSnippet: callbackify(createProjectFromSnippet),
createBasicProject: callbackify(createBasicProject),
createExampleProject: callbackify(createExampleProject),
promises: {
createBlankProject,
createProjectFromSnippet,
createBasicProject,
createExampleProject,
},
}
metrics.timeAsyncMethod(
ProjectCreationHandler,
module.exports,
'createBlankProject',
'mongo.ProjectCreationHandler',
logger
)
const promises = {
createBlankProject: promisify(ProjectCreationHandler.createBlankProject),
}
ProjectCreationHandler.promises = promises
module.exports = ProjectCreationHandler

View file

@ -0,0 +1,60 @@
const Settings = require('settings-sharelatex')
const _ = require('lodash')
const { ObjectId } = require('mongodb')
const OError = require('@overleaf/o-error')
const ACTIVE_SPLIT_TESTS = []
for (const splitTest of Settings.splitTests) {
for (const variant of splitTest.variants) {
if (variant.id === 'default') {
throw new OError(
`Split test variant ID cannot be 'default' (reserved value), defined in split test ${JSON.stringify(
splitTest
)}`
)
}
}
const totalVariantsRolloutPercent = _.sumBy(
splitTest.variants,
'rolloutPercent'
)
if (splitTest.active) {
if (totalVariantsRolloutPercent > 100) {
for (const variant of splitTest.variants) {
variant.rolloutPercent =
(variant.rolloutPercent * 100) / totalVariantsRolloutPercent
}
}
if (totalVariantsRolloutPercent > 0) {
ACTIVE_SPLIT_TESTS.push(splitTest)
}
}
}
function getTestSegmentation(userId, splitTestId) {
const splitTest = _.find(ACTIVE_SPLIT_TESTS, ['id', splitTestId])
if (splitTest) {
let userIdAsPercentile = (ObjectId(userId).getTimestamp() / 1000) % 100
for (const variant of splitTest.variants) {
if (userIdAsPercentile < variant.rolloutPercent) {
return {
enabled: true,
variant: variant.id,
}
} else {
userIdAsPercentile -= variant.rolloutPercent
}
}
return {
enabled: true,
variant: 'default',
}
}
return {
enabled: false,
}
}
module.exports = {
getTestSegmentation,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View file

@ -0,0 +1,119 @@
\documentclass{article}
% Language setting
% Replace `english' with e.g. `spanish' to change the document language
\usepackage[english]{babel}
% Set page size and margins
% Replace `letterpaper' with`a4paper' for UK/EU standard size
\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry}
% Useful packages
\usepackage{amsmath}
\usepackage{graphicx}
\usepackage[colorlinks=true, allcolors=blue]{hyperref}
\title{Your Paper}
\author{You}
\begin{document}
\maketitle
\begin{abstract}
Your abstract.
\end{abstract}
\section{Introduction}
Your introduction goes here! Simply start writing your document and use the Recompile button to view the updated PDF preview. Examples of commonly used commands and features are listed below, to help you get started.
Once you're familiar with the editor, you can find various project setting in the Overleaf menu, accessed via the button in the very top left of the editor. To view tutorials, user guides, and further documentation, please visit our \href{https://www.overleaf.com/learn}{help library}, or head to our plans page to \href{https://www.overleaf.com/user/subscription/plans}{choose your plan}.
\section{Some examples to get started}
\subsection{How to create Sections and Subsections}
Simply use the section and subsection commands, as in this example document! With Overleaf, all the formatting and numbering is handled automatically according to the template you've chosen. If you're using Rich Text mode, you can also create new section and subsections via the buttons in the editor toolbar.
\subsection{How to include Figures}
First you have to upload the image file from your computer using the upload link in the file-tree menu. Then use the includegraphics command to include it in your document. Use the figure environment and the caption command to add a number and a caption to your figure. See the code for Figure \ref{fig:frog} in this section for an example.
Note that your figure will automatically be placed in the most appropriate place for it, given the surrounding text and taking into account other figures or tables that may be close by. You can find out more about adding images to your documents in this help article on \href{https://www.overleaf.com/learn/how-to/Including_images_on_Overleaf}{including images on Overleaf}.
\begin{figure}
\centering
\includegraphics[width=0.3\textwidth]{frog.jpg}
\caption{\label{fig:frog}This frog was uploaded via the file-tree menu.}
\end{figure}
\subsection{How to add Tables}
Use the table and tabular environments for basic tables --- see Table~\ref{tab:widgets}, for example. For more information, please see this help article on \href{https://www.overleaf.com/learn/latex/tables}{tables}.
\begin{table}
\centering
\begin{tabular}{l|r}
Item & Quantity \\\hline
Widgets & 42 \\
Gadgets & 13
\end{tabular}
\caption{\label{tab:widgets}An example table.}
\end{table}
\subsection{How to add Comments and Track Changes}
Comments can be added to your project by highlighting some text and clicking ``Add comment'' in the top right of the editor pane. To view existing comments, click on the Review menu in the toolbar above. To reply to a comment, click on the Reply button in the lower right corner of the comment. You can close the Review pane by clicking its name on the toolbar when you're done reviewing for the time being.
Track changes are available on all our \href{https://www.overleaf.com/user/subscription/plans}{premium plans}, and can be toggled on or off using the option at the top of the Review pane. Track changes allow you to keep track of every change made to the document, along with the person making the change.
\subsection{How to add Lists}
You can make lists with automatic numbering \dots
\begin{enumerate}
\item Like this,
\item and like this.
\end{enumerate}
\dots or bullet points \dots
\begin{itemize}
\item Like this,
\item and like this.
\end{itemize}
\subsection{How to write Mathematics}
\LaTeX{} is great at typesetting mathematics. Let $X_1, X_2, \ldots, X_n$ be a sequence of independent and identically distributed random variables with $\text{E}[X_i] = \mu$ and $\text{Var}[X_i] = \sigma^2 < \infty$, and let
\[S_n = \frac{X_1 + X_2 + \cdots + X_n}{n}
= \frac{1}{n}\sum_{i}^{n} X_i\]
denote their mean. Then as $n$ approaches infinity, the random variables $\sqrt{n}(S_n - \mu)$ converge in distribution to a normal $\mathcal{N}(0, \sigma^2)$.
\subsection{How to change the margins and paper size}
Usually the template you're using will have the page margins and paper size set correctly for that use-case. For example, if you're using a journal article template provided by the journal publisher, that template will be formatted according to their requirements. In these cases, it's best not to alter the margins directly.
If however you're using a more general template, such as this one, and would like to alter the margins, a common way to do so is via the geometry package. You can find the geometry package loaded in the preamble at the top of this example file, and if you'd like to learn more about how to adjust the settings, please visit this help article on \href{https://www.overleaf.com/learn/latex/page_size_and_margins}{page size and margins}.
\subsection{How to change the document language and spell check settings}
Overleaf supports many different languages, including multiple different languages within one document.
To configure the document language, simply edit the option provided to the babel package in the preamble at the top of this example project. To learn more about the different options, please visit this help article on \href{https://www.overleaf.com/learn/latex/International_language_support}{international language support}.
To change the spell check language, simply open the Overleaf menu at the top left of the editor window, scroll down to the spell check setting, and adjust accordingly.
\subsection{How to add Citations and a References List}
You can simply upload a \verb|.bib| file containing your BibTeX entries, created with a tool such as JabRef. You can then cite entries from it, like this: \cite{greenwade93}. Just remember to specify a bibliography style, as well as the filename of the \verb|.bib|. You can find a \href{https://www.overleaf.com/help/97-how-to-include-a-bibliography-using-bibtex}{video tutorial here} to learn more about BibTeX.
If you have an \href{https://www.overleaf.com/user/subscription/plans}{upgraded account}, you can also import your Mendeley or Zotero library directly as a \verb|.bib| file, via the upload menu in the file-tree.
\subsection{Good luck!}
We hope you find Overleaf useful, and do take a look at our \href{https://www.overleaf.com/learn}{help library} for more tutorials and user guides! Please also let us know if you have any feedback using the Contact Us link at the bottom of the Overleaf menu --- or use the contact form at \url{https://www.overleaf.com/contact}.
\bibliographystyle{alpha}
\bibliography{sample}
\end{document}

View file

@ -0,0 +1,9 @@
@article{greenwade93,
author = "George D. Greenwade",
title = "The {C}omprehensive {T}ex {A}rchive {N}etwork ({CTAN})",
year = "1993",
journal = "TUGBoat",
volume = "14",
number = "3",
pages = "342--351"
}

View file

@ -236,6 +236,19 @@ module.exports = settings =
showSocialButtons: false
showComments: false
splitTests: [
{
id: 'example-project'
active: process.env['SPLITTEST_EXAMPLE_PROJECT_ACTIVE'] == 'true'
variants: [
{
id: 'example-frog'
rolloutPercent: parseInt(process.env['SPLITTEST_EXAMPLE_PROJECT_FROG_VARIANT_ROLLOUT_PERCENT'] || '0', 10)
}
]
}
]
# cdn:
# web:
# host:"http://nowhere.sharelatex.dev"

View file

@ -11,13 +11,9 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const { expect } = require('chai')
const request = require('./helpers/request')
const _ = require('underscore')
const { expect } = require('chai')
const User = require('./helpers/User')
const ProjectGetter = require('../../../app/src/Features/Project/ProjectGetter.js')
const ExportsHandler = require('../../../app/src/Features/Exports/ExportsHandler.js')
const MockProjectHistoryApiClass = require('./mocks/MockProjectHistoryApi')
const MockV1ApiClass = require('./mocks/MockV1Api')

View file

@ -1,586 +0,0 @@
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath =
'../../../../app/src/Features/Project/ProjectCreationHandler.js'
const SandboxedModule = require('sandboxed-module')
const Path = require('path')
const { ObjectId } = require('mongodb')
describe('ProjectCreationHandler', function () {
const ownerId = '4eecb1c1bffa66588e0000a1'
const projectName = 'project name goes here'
const projectId = '4eecaffcbffa66588e000008'
const docId = '4eecb17ebffa66588e00003f'
const rootFolderId = '234adfa3r2afe'
beforeEach(function () {
this.ProjectModel = class Project {
constructor(options) {
if (options == null) {
options = {}
}
this._id = projectId
this.owner_ref = options.owner_ref
this.name = options.name
this.overleaf = { history: {} }
}
}
this.ProjectModel.prototype.save = sinon.stub().callsArg(0)
this.ProjectModel.prototype.rootFolder = [
{
_id: rootFolderId,
docs: [],
},
]
this.FolderModel = class Folder {
constructor(options) {
this.name = options.name
}
}
this.ProjectEntityUpdateHandler = {
addDoc: sinon.stub().callsArgWith(5, null, { _id: docId }),
addFile: sinon.stub().callsArg(6),
setRootDoc: sinon.stub().callsArg(2),
}
this.ProjectDetailsHandler = { validateProjectName: sinon.stub().yields() }
this.HistoryManager = { initializeProject: sinon.stub().callsArg(0) }
this.user = {
first_name: 'first name here',
last_name: 'last name here',
ace: {
spellCheckLanguage: 'de',
},
}
this.User = { findById: sinon.stub().callsArgWith(2, null, this.user) }
this.callback = sinon.stub()
this.Settings = { apis: { project_history: {} } }
this.AnalyticsManager = { recordEvent: sinon.stub() }
this.handler = SandboxedModule.require(modulePath, {
requires: {
mongodb: { ObjectId },
'../../models/User': {
User: this.User,
},
'../../models/Project': { Project: this.ProjectModel },
'../../models/Folder': { Folder: this.FolderModel },
'../History/HistoryManager': this.HistoryManager,
'./ProjectEntityUpdateHandler': this.ProjectEntityUpdateHandler,
'./ProjectDetailsHandler': this.ProjectDetailsHandler,
'settings-sharelatex': this.Settings,
'../Analytics/AnalyticsManager': this.AnalyticsManager,
'@overleaf/metrics': {
inc() {},
timeAsyncMethod() {},
},
},
})
})
describe('Creating a Blank project', function () {
beforeEach(function () {
this.overleafId = 1234
this.HistoryManager.initializeProject = sinon
.stub()
.callsArgWith(0, null, { overleaf_id: this.overleafId })
this.ProjectModel.prototype.save = sinon.stub().callsArg(0)
})
describe('successfully', function () {
it('should save the project', function (done) {
this.handler.createBlankProject(ownerId, projectName, () => {
this.ProjectModel.prototype.save.called.should.equal(true)
done()
})
})
it('should return the project in the callback', function (done) {
this.handler.createBlankProject(
ownerId,
projectName,
(err, project) => {
if (err != null) {
return done(err)
}
project.name.should.equal(projectName)
expect(project.owner_ref + '').to.equal(ownerId)
expect(project.lastUpdatedBy + '').to.equal(ownerId)
done()
}
)
})
it('should initialize the project overleaf if history id not provided', function (done) {
this.handler.createBlankProject(ownerId, projectName, done)
this.HistoryManager.initializeProject.calledWith().should.equal(true)
})
it('should set the overleaf id if overleaf id not provided', function (done) {
this.handler.createBlankProject(
ownerId,
projectName,
(err, project) => {
if (err != null) {
return done(err)
}
project.overleaf.history.id.should.equal(this.overleafId)
done()
}
)
})
it('should set the overleaf id if overleaf id provided', function (done) {
const overleafId = 2345
const attributes = {
overleaf: {
history: {
id: overleafId,
},
},
}
this.handler.createBlankProject(
ownerId,
projectName,
attributes,
(err, project) => {
if (err != null) {
return done(err)
}
project.overleaf.history.id.should.equal(overleafId)
done()
}
)
})
it('should set the language from the user', function (done) {
this.handler.createBlankProject(
ownerId,
projectName,
(err, project) => {
if (err != null) {
return done(err)
}
project.spellCheckLanguage.should.equal('de')
done()
}
)
})
it('should set the imageName to currentImageName if set and no imageName attribute', function (done) {
this.Settings.currentImageName = 'mock-image-name'
this.handler.createBlankProject(
ownerId,
projectName,
(err, project) => {
if (err != null) {
return done(err)
}
project.imageName.should.equal(this.Settings.currentImageName)
done()
}
)
})
it('should not set the imageName if no currentImageName', function (done) {
this.Settings.currentImageName = null
this.handler.createBlankProject(
ownerId,
projectName,
(err, project) => {
if (err != null) {
return done(err)
}
expect(project.imageName).to.not.exist
done()
}
)
})
it('should set the imageName to the attribute value if set and not overwrite it with the currentImageName', function (done) {
this.Settings.currentImageName = 'mock-image-name'
const attributes = { imageName: 'attribute-image-name' }
this.handler.createBlankProject(
ownerId,
projectName,
attributes,
(err, project) => {
if (err != null) {
return done(err)
}
project.imageName.should.equal(attributes.imageName)
done()
}
)
})
it('should not set the overleaf.history.display if not configured in settings', function (done) {
this.Settings.apis.project_history.displayHistoryForNewProjects = false
this.handler.createBlankProject(
ownerId,
projectName,
(err, project) => {
if (err != null) {
return done(err)
}
expect(project.overleaf.history.display).to.not.exist
done()
}
)
})
it('should set the overleaf.history.display if configured in settings', function (done) {
this.Settings.apis.project_history.displayHistoryForNewProjects = true
this.handler.createBlankProject(
ownerId,
projectName,
(err, project) => {
if (err != null) {
return done(err)
}
expect(project.overleaf.history.display).to.equal(true)
done()
}
)
})
it('should send a project-created event to analytics', function (done) {
this.handler.createBlankProject(
ownerId,
projectName,
(err, project) => {
if (err != null) {
return done(err)
}
expect(this.AnalyticsManager.recordEvent.callCount).to.equal(1)
expect(
this.AnalyticsManager.recordEvent.calledWith(
ownerId,
'project-created'
)
).to.equal(true)
done()
}
)
})
it('should send a project-created event with template information if provided', function (done) {
const attributes = {
fromV1TemplateId: 100,
}
this.handler.createBlankProject(
ownerId,
projectName,
attributes,
(err, project) => {
if (err != null) {
return done(err)
}
expect(this.AnalyticsManager.recordEvent.callCount).to.equal(1)
expect(
this.AnalyticsManager.recordEvent.calledWith(
ownerId,
'project-created',
{ projectId: project._id, attributes }
)
).to.equal(true)
done()
}
)
})
it('should send a project-imported event when importing a project', function (done) {
const attributes = {
overleaf: {
history: {
id: 100,
},
},
}
this.handler.createBlankProject(
ownerId,
projectName,
attributes,
(err, project) => {
if (err != null) {
return done(err)
}
expect(this.AnalyticsManager.recordEvent.callCount).to.equal(1)
expect(
this.AnalyticsManager.recordEvent.calledWith(
ownerId,
'project-imported'
)
).to.equal(true)
done()
}
)
})
})
describe('with an error', function () {
beforeEach(function () {
this.ProjectModel.prototype.save = sinon
.stub()
.callsArgWith(0, new Error('something went wrong'))
this.handler.createBlankProject(ownerId, projectName, this.callback)
})
it('should return the error to the callback', function () {
expect(this.callback.args[0][0]).to.exist
})
})
describe('with an invalid name', function () {
beforeEach(function () {
this.ProjectDetailsHandler.validateProjectName = sinon
.stub()
.yields(new Error('bad name'))
this.handler.createBlankProject(ownerId, projectName, this.callback)
})
it('should return the error to the callback', function () {
expect(this.callback.args[0][0]).to.exist
})
it('should not try to create the project', function () {
this.ProjectModel.prototype.save.called.should.equal(false)
})
})
})
describe('Creating a basic project', function () {
beforeEach(function () {
this.project = new this.ProjectModel()
this.handler._buildTemplate = function (
templateName,
user,
projectName,
callback
) {
if (templateName === 'mainbasic.tex') {
return callback(null, ['mainbasic.tex', 'lines'])
}
throw new Error(`unknown template: ${templateName}`)
}
sinon.spy(this.handler, '_buildTemplate')
this.handler.createBlankProject = sinon
.stub()
.callsArgWith(2, null, this.project)
this.handler._createRootDoc = sinon
.stub()
.callsArgWith(3, null, this.project)
this.handler.createBasicProject(ownerId, projectName, this.callback)
})
it('should create a blank project first', function () {
this.handler.createBlankProject
.calledWith(ownerId, projectName)
.should.equal(true)
})
it('should create the root document', function () {
this.handler._createRootDoc
.calledWith(this.project, ownerId, ['mainbasic.tex', 'lines'])
.should.equal(true)
})
it('should build the mainbasic.tex template', function () {
this.handler._buildTemplate
.calledWith('mainbasic.tex', ownerId, projectName)
.should.equal(true)
})
})
describe('Creating a project from a snippet', function () {
beforeEach(function () {
this.project = new this.ProjectModel()
this.handler.createBlankProject = sinon
.stub()
.callsArgWith(2, null, this.project)
this.handler._createRootDoc = sinon
.stub()
.callsArgWith(3, null, this.project)
this.handler.createProjectFromSnippet(
ownerId,
projectName,
['snippet line 1', 'snippet line 2'],
this.callback
)
})
it('should create a blank project first', function () {
this.handler.createBlankProject
.calledWith(ownerId, projectName)
.should.equal(true)
})
it('should create the root document', function () {
this.handler._createRootDoc
.calledWith(this.project, ownerId, ['snippet line 1', 'snippet line 2'])
.should.equal(true)
})
})
describe('Creating an example project', function () {
beforeEach(function () {
this.project = new this.ProjectModel()
this.handler._buildTemplate = function (
templateName,
user,
projectName,
callback
) {
if (templateName === 'main.tex') {
return callback(null, ['main.tex', 'lines'])
}
if (templateName === 'references.bib') {
return callback(null, ['references.bib', 'lines'])
}
throw new Error(`unknown template: ${templateName}`)
}
sinon.spy(this.handler, '_buildTemplate')
this.handler.createBlankProject = sinon
.stub()
.callsArgWith(2, null, this.project)
this.handler._createRootDoc = sinon
.stub()
.callsArgWith(3, null, this.project)
this.handler.createExampleProject(ownerId, projectName, this.callback)
})
it('should create a blank project first', function () {
this.handler.createBlankProject
.calledWith(ownerId, projectName)
.should.equal(true)
})
it('should create the root document', function () {
this.handler._createRootDoc
.calledWith(this.project, ownerId, ['main.tex', 'lines'])
.should.equal(true)
})
it('should insert references.bib', function () {
this.ProjectEntityUpdateHandler.addDoc
.calledWith(
projectId,
rootFolderId,
'references.bib',
['references.bib', 'lines'],
ownerId
)
.should.equal(true)
})
it('should insert universe.jpg', function () {
this.ProjectEntityUpdateHandler.addFile
.calledWith(
projectId,
rootFolderId,
'universe.jpg',
Path.resolve(
Path.join(
__dirname,
'../../../../app/templates/project_files/universe.jpg'
)
),
null,
ownerId
)
.should.equal(true)
})
it('should build the main.tex template', function () {
this.handler._buildTemplate
.calledWith('main.tex', ownerId, projectName)
.should.equal(true)
})
it('should build the references.bib template', function () {
this.handler._buildTemplate
.calledWith('references.bib', ownerId, projectName)
.should.equal(true)
})
})
describe('_buildTemplate', function () {
beforeEach(function (done) {
this.handler._buildTemplate(
'main.tex',
this.user_id,
projectName,
(err, templateLines) => {
if (err != null) {
return done(err)
}
this.template = templateLines.reduce(
(singleLine, line) => `${singleLine}\n${line}`
)
done()
}
)
})
it('should insert the project name into the template', function (done) {
this.template.indexOf(projectName).should.not.equal(-1)
done()
})
it('should insert the users name into the template', function (done) {
this.template.indexOf(this.user.first_name).should.not.equal(-1)
this.template.indexOf(this.user.last_name).should.not.equal(-1)
done()
})
it('should not have undefined in the template', function (done) {
this.template.indexOf('undefined').should.equal(-1)
done()
})
it('should not have any underscore brackets in the output', function (done) {
this.template.indexOf('{{').should.equal(-1)
this.template.indexOf('<%=').should.equal(-1)
done()
})
it('should put the year in', function (done) {
this.template.indexOf(new Date().getUTCFullYear()).should.not.equal(-1)
done()
})
})
describe('_createRootDoc', function () {
beforeEach(function (done) {
this.project = new this.ProjectModel()
this.handler._createRootDoc(
this.project,
ownerId,
['line 1', 'line 2'],
done
)
})
it('should insert main.tex', function () {
this.ProjectEntityUpdateHandler.addDoc
.calledWith(
projectId,
rootFolderId,
'main.tex',
['line 1', 'line 2'],
ownerId
)
.should.equal(true)
})
it('should set the main doc id', function () {
this.ProjectEntityUpdateHandler.setRootDoc
.calledWith(projectId, docId)
.should.equal(true)
})
})
})