Merge remote-tracking branch 'origin/master' into afc-metrics-spike

This commit is contained in:
Alberto Fernández Capel 2018-05-23 09:38:22 +01:00
commit a5b608a502
49 changed files with 1737 additions and 681 deletions

View file

@ -101,7 +101,7 @@ pipeline {
}
}
steps {
sh 'make minify'
sh 'WEBPACK_ENV=production make minify'
}
}

View file

@ -9,7 +9,7 @@ COFFEE := node_modules/.bin/coffee $(COFFEE_OPTIONS)
GRUNT := node_modules/.bin/grunt
APP_COFFEE_FILES := $(shell find app/coffee -name '*.coffee')
FRONT_END_COFFEE_FILES := $(shell find public/coffee -name '*.coffee')
TEST_COFFEE_FILES := $(shell find test -name '*.coffee')
TEST_COFFEE_FILES := $(shell find test/*/coffee -name '*.coffee')
MODULE_MAIN_COFFEE_FILES := $(shell find modules -type f -wholename '*main/index.coffee')
MODULE_IDE_COFFEE_FILES := $(shell find modules -type f -wholename '*ide/index.coffee')
COFFEE_FILES := app.coffee $(APP_COFFEE_FILES) $(FRONT_END_COFFEE_FILES) $(TEST_COFFEE_FILES)

View file

@ -3,11 +3,11 @@ web-sharelatex
web-sharelatex is the front-end web service of the open-source web-based collaborative LaTeX editor,
[ShareLaTeX](https://www.sharelatex.com).
It serves all the HTML pages, CSS and javascript to the client. web-sharelatex also contains
It serves all the HTML pages, CSS and javascript to the client. web-sharelatex also contains
a lot of logic around creating and editing projects, and account management.
The rest of the ShareLaTeX stack, along with information about contributing can be found in the
The rest of the ShareLaTeX stack, along with information about contributing can be found in the
[sharelatex/sharelatex](https://github.com/sharelatex/sharelatex) repository.
Build process
@ -20,7 +20,7 @@ Image processing tasks are commented out in the gruntfile and the needed package
New Docker-based build process
------------------------------
Note that the Grunt workflow from above should still work, but we are transitioning to a
Note that the Grunt workflow from above should still work, but we are transitioning to a
Docker based testing workflow, which is documented below:
### Running the app
@ -59,19 +59,18 @@ Acceptance tests are run against a live service, which runs in the `acceptance_t
To run the tests out-of-the-box, the makefile defines:
```
make install # Only needs running once, or when npm packages are updated
make acceptance_test
make test_acceptance
```
However, during development it is often useful to leave the service running for rapid iteration on the acceptance tests. This can be done with:
```
make acceptance_test_start_service
make acceptance_test_run # Run as many times as needed during development
make acceptance_test_stop_service
make test_acceptance_app_start_service
make test_acceptance_app_run # Run as many times as needed during development
make test_acceptance_app_stop_service
```
`make acceptance_test` just runs these three commands in sequence.
`make test_acceptance` just runs these three commands in sequence and then runs `make test_acceptance_modules` which performs the tests for each module in the `modules` directory. (Note that there is not currently an equivalent to the `-start` / `-run` x _n_ / `-stop` series for modules.)
During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI:
@ -111,12 +110,3 @@ We gratefully acknowledge [IconShock](http://www.iconshock.com) for use of the i
in the `public/img/iconshock` directory found via
[findicons.com](http://findicons.com/icon/498089/height?id=526085#)
## Acceptance Tests
To run the Acceptance tests:
- set `allowPublicAccess` to true, either in the configuration file,
or by setting the environment variable `SHARELATEX_ALLOW_PUBLIC_ACCESS` to `true`
- start the server (`grunt`)
- in a separate terminal, run `grunt test:acceptance`

View file

@ -0,0 +1,18 @@
ExportsHandler = require("./ExportsHandler")
AuthenticationController = require("../Authentication/AuthenticationController")
logger = require("logger-sharelatex")
module.exports =
exportProject: (req, res) ->
{project_id, brand_variation_id} = req.params
user_id = AuthenticationController.getLoggedInUserId(req)
ExportsHandler.exportProject project_id, user_id, brand_variation_id, (err, export_data) ->
logger.log
user_id:user_id
project_id: project_id
brand_variation_id:brand_variation_id
export_v1_id:export_data.v1_id
"exported project"
res.send export_v1_id: export_data.v1_id

View file

@ -0,0 +1,93 @@
ProjectGetter = require('../Project/ProjectGetter')
ProjectLocator = require('../Project/ProjectLocator')
UserGetter = require('../User/UserGetter')
logger = require('logger-sharelatex')
settings = require 'settings-sharelatex'
async = require 'async'
request = require 'request'
request = request.defaults()
settings = require 'settings-sharelatex'
module.exports = ExportsHandler = self =
exportProject: (project_id, user_id, brand_variation_id, callback=(error, export_data) ->) ->
self._buildExport project_id, user_id, brand_variation_id, (err, export_data) ->
return callback(err) if err?
self._requestExport export_data, (err, export_v1_id) ->
return callback(err) if err?
export_data.v1_id = export_v1_id
# TODO: possibly store the export data in Mongo
callback null, export_data
_buildExport: (project_id, user_id, brand_variation_id, callback=(err, export_data) ->) ->
jobs =
project: (cb) ->
ProjectGetter.getProject project_id, cb
# TODO: when we update async, signature will change from (cb, results) to (results, cb)
rootDoc: [ 'project', (cb, results) ->
ProjectLocator.findRootDoc {project: results.project, project_id: project_id}, cb
]
user: (cb) ->
UserGetter.getUser user_id, {first_name: 1, last_name: 1, email: 1}, cb
historyVersion: (cb) ->
self._requestVersion project_id, cb
async.auto jobs, (err, results) ->
if err?
logger.err err:err, project_id:project_id, user_id:user_id, brand_variation_id:brand_variation_id, "error building project export"
return callback(err)
{project, rootDoc, user, historyVersion} = results
if !rootDoc[1]?
err = new Error("cannot export project without root doc")
logger.err err:err, project_id: project_id
return callback(err)
export_data =
project:
id: project_id
rootDocPath: rootDoc[1]?.fileSystem
historyId: project.overleaf?.history?.id
historyVersion: historyVersion
user:
id: user_id
firstName: user.first_name
lastName: user.last_name
email: user.email
orcidId: null # until v2 gets ORCID
destination:
brandVariationId: brand_variation_id
options:
callbackUrl: null # for now, until we want v1 to call us back
callback null, export_data
_requestExport: (export_data, callback=(err, export_v1_id) ->) ->
request.post {
url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports"
auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass }
json: export_data
}, (err, res, body) ->
if err?
logger.err err:err, export:export_data, "error making request to v1 export"
callback err
else if 200 <= res.statusCode < 300
callback null, body.exportId
else
err = new Error("v1 export returned a failure status code: #{res.statusCode}")
logger.err err:err, export:export_data, "v1 export returned failure status code: #{res.statusCode}"
callback err
_requestVersion: (project_id, callback=(err, export_v1_id) ->) ->
request.get {
url: "#{settings.apis.project_history.url}/project/#{project_id}/version"
json: true
}, (err, res, body) ->
if err?
logger.err err:err, project_id:project_id, "error making request to project history"
callback err
else if res.statusCode >= 200 and res.statusCode < 300
callback null, body.version
else
err = new Error("project history version returned a failure status code: #{res.statusCode}")
logger.err err:err, project_id:project_id, "project history version returned failure status code: #{res.statusCode}"
callback err

View file

@ -1,8 +1,8 @@
_ = require("underscore")
logger = require('logger-sharelatex')
User = require('../../models/User').User
SubscriptionLocator = require "../Subscription/SubscriptionLocator"
Settings = require "settings-sharelatex"
FeaturesUpdater = require "../Subscription/FeaturesUpdater"
module.exports = ReferalAllocator =
allocate: (referal_id, new_user_id, referal_source, referal_medium, callback = ->)->
@ -25,50 +25,6 @@ module.exports = ReferalAllocator =
if err?
logger.err err:err, referal_id:referal_id, new_user_id:new_user_id, "something went wrong allocating referal"
return callback(err)
ReferalAllocator.assignBonus user._id, callback
FeaturesUpdater.refreshFeatures user._id, callback
else
callback()
assignBonus: (user_id, callback = (error) ->) ->
query = _id: user_id
User.findOne query, (error, user) ->
return callback(error) if error
return callback(new Error("user not found #{user_id} for assignBonus")) if !user?
logger.log user_id: user_id, refered_user_count: user.refered_user_count, "assigning bonus"
if user.refered_user_count? and user.refered_user_count > 0
newFeatures = ReferalAllocator._calculateFeatures(user)
if _.isEqual newFeatures, user.features
return callback()
User.update query, { $set: features: newFeatures }, callback
else
callback()
_calculateFeatures : (user)->
bonusLevel = ReferalAllocator._getBonusLevel(user)
currentFeatures = _.clone(user.features) #need to clone because we exend with underscore later
betterBonusFeatures = {}
_.each Settings.bonus_features["#{bonusLevel}"], (bonusLevel, key)->
currentLevel = user?.features?[key]
if _.isBoolean(currentLevel) and currentLevel == false
betterBonusFeatures[key] = bonusLevel
if _.isNumber(currentLevel)
if currentLevel == -1
return
bonusIsGreaterThanCurrent = currentLevel < bonusLevel
if bonusIsGreaterThanCurrent or bonusLevel == -1
betterBonusFeatures[key] = bonusLevel
newFeatures = _.extend(currentFeatures, betterBonusFeatures)
return newFeatures
_getBonusLevel: (user)->
highestBonusLevel = 0
_.each _.keys(Settings.bonus_features), (level)->
levelIsLessThanUser = level <= user.refered_user_count
levelIsMoreThanCurrentHighest = level >= highestBonusLevel
if levelIsLessThanUser and levelIsMoreThanCurrentHighest
highestBonusLevel = level
return highestBonusLevel

View file

@ -0,0 +1,44 @@
_ = require("underscore")
logger = require('logger-sharelatex')
User = require('../../models/User').User
Settings = require "settings-sharelatex"
module.exports = ReferalFeatures =
getBonusFeatures: (user_id, callback = (error) ->) ->
query = _id: user_id
User.findOne query, (error, user) ->
return callback(error) if error
return callback(new Error("user not found #{user_id} for assignBonus")) if !user?
logger.log user_id: user_id, refered_user_count: user.refered_user_count, "assigning bonus"
if user.refered_user_count? and user.refered_user_count > 0
newFeatures = ReferalFeatures._calculateFeatures(user)
callback null, newFeatures
else
callback null, {}
_calculateFeatures : (user)->
bonusLevel = ReferalFeatures._getBonusLevel(user)
currentFeatures = _.clone(user.features) #need to clone because we exend with underscore later
betterBonusFeatures = {}
_.each Settings.bonus_features["#{bonusLevel}"], (bonusLevel, key)->
currentLevel = user?.features?[key]
if _.isBoolean(currentLevel) and currentLevel == false
betterBonusFeatures[key] = bonusLevel
if _.isNumber(currentLevel)
if currentLevel == -1
return
bonusIsGreaterThanCurrent = currentLevel < bonusLevel
if bonusIsGreaterThanCurrent or bonusLevel == -1
betterBonusFeatures[key] = bonusLevel
newFeatures = _.extend(currentFeatures, betterBonusFeatures)
return newFeatures
_getBonusLevel: (user)->
highestBonusLevel = 0
_.each _.keys(Settings.bonus_features), (level)->
levelIsLessThanUser = level <= user.refered_user_count
levelIsMoreThanCurrentHighest = level >= highestBonusLevel
if levelIsLessThanUser and levelIsMoreThanCurrentHighest
highestBonusLevel = level
return highestBonusLevel

View file

@ -0,0 +1,83 @@
async = require("async")
PlansLocator = require("./PlansLocator")
_ = require("underscore")
SubscriptionLocator = require("./SubscriptionLocator")
UserFeaturesUpdater = require("./UserFeaturesUpdater")
Settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
ReferalFeatures = require("../Referal/ReferalFeatures")
V1SubscriptionManager = require("./V1SubscriptionManager")
oneMonthInSeconds = 60 * 60 * 24 * 30
module.exports = FeaturesUpdater =
refreshFeatures: (user_id, callback)->
jobs =
individualFeatures: (cb) -> FeaturesUpdater._getIndividualFeatures user_id, cb
groupFeatureSets: (cb) -> FeaturesUpdater._getGroupFeatureSets user_id, cb
v1Features: (cb) -> FeaturesUpdater._getV1Features user_id, cb
bonusFeatures: (cb) -> ReferalFeatures.getBonusFeatures user_id, cb
async.series jobs, (err, results)->
if err?
logger.err err:err, user_id:user_id,
"error getting subscription or group for refreshFeatures"
return callback(err)
{individualFeatures, groupFeatureSets, v1Features, bonusFeatures} = results
logger.log {user_id, individualFeatures, groupFeatureSets, v1Features, bonusFeatures}, 'merging user features'
featureSets = groupFeatureSets.concat [individualFeatures, v1Features, bonusFeatures]
features = _.reduce(featureSets, FeaturesUpdater._mergeFeatures, Settings.defaultFeatures)
logger.log {user_id, features}, 'updating user features'
UserFeaturesUpdater.updateFeatures user_id, features, callback
_getIndividualFeatures: (user_id, callback = (error, features = {}) ->) ->
SubscriptionLocator.getUsersSubscription user_id, (err, sub)->
callback err, FeaturesUpdater._subscriptionToFeatures(sub)
_getGroupFeatureSets: (user_id, callback = (error, featureSets = []) ->) ->
SubscriptionLocator.getGroupSubscriptionsMemberOf user_id, (err, subs) ->
callback err, (subs or []).map FeaturesUpdater._subscriptionToFeatures
_getV1Features: (user_id, callback = (error, features = {}) ->) ->
V1SubscriptionManager.getPlanCodeFromV1 user_id, (err, planCode) ->
callback err, FeaturesUpdater._planCodeToFeatures(planCode)
_mergeFeatures: (featuresA, featuresB) ->
features = Object.assign({}, featuresA)
for key, value of featuresB
# Special merging logic for non-boolean features
if key == 'compileGroup'
if features['compileGroup'] == 'priority' or featuresB['compileGroup'] == 'priority'
features['compileGroup'] = 'priority'
else
features['compileGroup'] = 'standard'
else if key == 'collaborators'
if features['collaborators'] == -1 or featuresB['collaborators'] == -1
features['collaborators'] = -1
else
features['collaborators'] = Math.max(
features['collaborators'] or 0,
featuresB['collaborators'] or 0
)
else if key == 'compileTimeout'
features['compileTimeout'] = Math.max(
features['compileTimeout'] or 0,
featuresB['compileTimeout'] or 0
)
else
# Boolean keys, true is better
features[key] = features[key] or featuresB[key]
return features
_subscriptionToFeatures: (subscription) ->
FeaturesUpdater._planCodeToFeatures(subscription?.planCode)
_planCodeToFeatures: (planCode) ->
if !planCode?
return {}
plan = PlansLocator.findLocalPlanInSettings planCode
if !plan?
return {}
else
return plan.features

View file

@ -9,6 +9,7 @@ logger = require('logger-sharelatex')
GeoIpLookup = require("../../infrastructure/GeoIpLookup")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
UserGetter = require "../User/UserGetter"
FeaturesUpdater = require './FeaturesUpdater'
module.exports = SubscriptionController =
@ -237,3 +238,9 @@ module.exports = SubscriptionController =
return next(error) if error?
req.body = body
next()
refreshUserFeatures: (req, res, next) ->
{user_id} = req.params
FeaturesUpdater.refreshFeatures user_id, (error) ->
return next(error) if error?
res.sendStatus 200

View file

@ -28,8 +28,8 @@ module.exports =
getSubscriptionByMemberIdAndId: (user_id, subscription_id, callback)->
Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback
getGroupSubscriptionMemberOf: (user_id, callback)->
Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
getGroupSubscriptionsMemberOf: (user_id, callback)->
Subscription.find {member_ids: user_id}, {_id:1, planCode:1}, callback
getGroupsWithEmailInvite: (email, callback) ->
Subscription.find { invited_emails: email }, callback

View file

@ -46,4 +46,6 @@ module.exports =
webRouter.get "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage
webRouter.post "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan
# Currently used in acceptance tests only, as a way to trigger the syncing logic
publicApiRouter.post "/user/:user_id/features/sync", AuthenticationController.httpAuth, SubscriptionController.refreshUserFeatures

View file

@ -2,26 +2,26 @@ async = require("async")
_ = require("underscore")
Subscription = require('../../models/Subscription').Subscription
SubscriptionLocator = require("./SubscriptionLocator")
UserFeaturesUpdater = require("./UserFeaturesUpdater")
PlansLocator = require("./PlansLocator")
Settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
ObjectId = require('mongoose').Types.ObjectId
ReferalAllocator = require("../Referal/ReferalAllocator")
ObjectId = require('mongoose').Types.ObjectId
FeaturesUpdater = require('./FeaturesUpdater')
oneMonthInSeconds = 60 * 60 * 24 * 30
module.exports = SubscriptionUpdater =
syncSubscription: (recurlySubscription, adminUser_id, callback) ->
logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "syncSubscription, creating new if subscription does not exist"
SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)->
return callback(err) if err?
if subscription?
logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does exist"
SubscriptionUpdater._updateSubscriptionFromRecurly recurlySubscription, subscription, callback
else
logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does not exist, creating a new one"
SubscriptionUpdater._createNewSubscription adminUser_id, (err, subscription)->
return callback(err) if err?
SubscriptionUpdater._updateSubscriptionFromRecurly recurlySubscription, subscription, callback
addUserToGroup: (adminUser_id, user_id, callback)->
@ -34,7 +34,7 @@ module.exports = SubscriptionUpdater =
if err?
logger.err err:err, searchOps:searchOps, insertOperation:insertOperation, "error findy and modify add user to group"
return callback(err)
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
FeaturesUpdater.refreshFeatures user_id, callback
addEmailInviteToGroup: (adminUser_id, email, callback) ->
logger.log {adminUser_id, email}, "adding email into mongo subscription"
@ -53,7 +53,7 @@ module.exports = SubscriptionUpdater =
if err?
logger.err err:err, searchOps:searchOps, removeOperation:removeOperation, "error removing user from group"
return callback(err)
SubscriptionUpdater._setUsersMinimumFeatures user_id, callback
FeaturesUpdater.refreshFeatures user_id, callback
removeEmailInviteFromGroup: (adminUser_id, email, callback)->
Subscription.update {
@ -62,9 +62,6 @@ module.exports = SubscriptionUpdater =
invited_emails: email
}, callback
refreshSubscription: (user_id, callback=(err)->) ->
SubscriptionUpdater._setUsersMinimumFeatures user_id, callback
deleteSubscription: (subscription_id, callback = (error) ->) ->
SubscriptionLocator.getSubscription subscription_id, (err, subscription) ->
return callback(err) if err?
@ -72,7 +69,7 @@ module.exports = SubscriptionUpdater =
logger.log {subscription_id, affected_user_ids}, "deleting subscription and downgrading users"
Subscription.remove {_id: ObjectId(subscription_id)}, (err) ->
return callback(err) if err?
async.mapSeries affected_user_ids, SubscriptionUpdater._setUsersMinimumFeatures, callback
async.mapSeries affected_user_ids, FeaturesUpdater.refreshFeatures, callback
_createNewSubscription: (adminUser_id, callback)->
logger.log adminUser_id:adminUser_id, "creating new subscription"
@ -100,43 +97,5 @@ module.exports = SubscriptionUpdater =
allIds = _.union subscription.member_ids, [subscription.admin_id]
jobs = allIds.map (user_id)->
return (cb)->
SubscriptionUpdater._setUsersMinimumFeatures user_id, cb
FeaturesUpdater.refreshFeatures user_id, cb
async.series jobs, callback
_setUsersMinimumFeatures: (user_id, callback)->
jobs =
subscription: (cb)->
SubscriptionLocator.getUsersSubscription user_id, cb
groupSubscription: (cb)->
SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb
v1PlanCode: (cb) ->
Modules = require '../../infrastructure/Modules'
Modules.hooks.fire 'getV1PlanCode', user_id, (err, results) ->
cb(err, results?[0] || null)
async.series jobs, (err, results)->
if err?
logger.err err:err, user_id:user_id,
"error getting subscription or group for _setUsersMinimumFeatures"
return callback(err)
{subscription, groupSubscription, v1PlanCode} = results
# Group Subscription
if groupSubscription? and groupSubscription.planCode?
logger.log user_id:user_id, "using group which user is memor of for features"
UserFeaturesUpdater.updateFeatures user_id, groupSubscription.planCode, callback
# Personal Subscription
else if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode
logger.log user_id:user_id, "using users subscription plan code for features"
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
# V1 Subscription
else if v1PlanCode?
logger.log user_id: user_id, "using the V1 plan for features"
UserFeaturesUpdater.updateFeatures user_id, v1PlanCode, callback
# Default
else
logger.log user_id:user_id, "using default features for user with no subscription or group"
UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, (err)->
if err?
logger.err err:err, user_id:user_id, "Error setting minimum user feature"
return callback(err)
ReferalAllocator.assignBonus user_id, callback

View file

@ -1,15 +1,12 @@
logger = require("logger-sharelatex")
User = require('../../models/User').User
PlansLocator = require("./PlansLocator")
module.exports =
updateFeatures: (user_id, plan_code, callback = (err, features)->)->
updateFeatures: (user_id, features, callback = (err, features)->)->
conditions = _id:user_id
update = {}
plan = PlansLocator.findLocalPlanInSettings(plan_code)
logger.log user_id:user_id, features:plan.features, plan_code:plan_code, "updating users features"
update["features.#{key}"] = value for key, value of plan.features
logger.log user_id:user_id, features:features, "updating users features"
update["features.#{key}"] = value for key, value of features
User.update conditions, update, (err)->
callback err, plan.features
callback err, features

View file

@ -0,0 +1,50 @@
UserGetter = require "../User/UserGetter"
request = require "request"
settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
module.exports = V1SubscriptionManager =
# Returned planCode = 'v1_pro' | 'v1_pro_plus' | 'v1_student' | 'v1_free' | null
# For this to work, we need plans in settings with plan-codes:
# - 'v1_pro'
# - 'v1_pro_plus'
# - 'v1_student'
# - 'v1_free'
getPlanCodeFromV1: (userId, callback=(err, planCode)->) ->
logger.log {userId}, "[V1SubscriptionManager] fetching v1 plan for user"
UserGetter.getUser userId, {'overleaf.id': 1}, (err, user) ->
return callback(err) if err?
v1Id = user?.overleaf?.id
if !v1Id?
logger.log {userId}, "[V1SubscriptionManager] no v1 id found for user"
return callback(null, null)
V1SubscriptionManager._v1PlanRequest v1Id, (err, body) ->
return callback(err) if err?
planName = body?.plan_name
logger.log {userId, planName, body}, "[V1SubscriptionManager] fetched v1 plan for user"
if planName in ['pro', 'pro_plus', 'student', 'free']
planName = "v1_#{planName}"
else
# Throw away 'anonymous', etc as being equivalent to null
planName = null
return callback(null, planName)
_v1PlanRequest: (v1Id, callback=(err, body)->) ->
if !settings?.apis?.v1
return callback null, null
request {
method: 'GET',
url: settings.apis.v1.url +
"/api/v1/sharelatex/users/#{v1Id}/plan_code"
auth:
user: settings.apis.v1.user
pass: settings.apis.v1.pass
sendImmediately: true
json: true,
timeout: 5 * 1000
}, (error, response, body) ->
return callback(error) if error?
if 200 <= response.statusCode < 300
return callback null, body
else
return callback new Error("non-success code from v1: #{response.statusCode}")

View file

@ -3,7 +3,7 @@ Path = require("path")
module.exports = FileTypeManager =
TEXT_EXTENSIONS : [
"tex", "latex", "sty", "cls", "bst", "bib", "bibtex", "txt", "tikz", "rtex", "md", "asy"
"tex", "latex", "sty", "cls", "bst", "bib", "bibtex", "txt", "tikz", "rtex", "md", "asy", "latexmkrc"
]
IGNORE_EXTENSIONS : [
@ -34,7 +34,7 @@ module.exports = FileTypeManager =
extension = parts.slice(-1)[0]
if extension?
extension = extension.toLowerCase()
binaryFile = (@TEXT_EXTENSIONS.indexOf(extension) == -1 or parts.length <= 1)
binaryFile = (@TEXT_EXTENSIONS.indexOf(extension) == -1 or parts.length <= 1) and parts[0] != 'latexmkrc'
if binaryFile
return callback null, true
@ -52,13 +52,10 @@ module.exports = FileTypeManager =
if extension?
extension = extension.toLowerCase()
ignore = false
if name[0] == "."
if name[0] == "." and extension != 'latexmkrc'
ignore = true
if @IGNORE_EXTENSIONS.indexOf(extension) != -1
ignore = true
if @IGNORE_FILENAMES.indexOf(name) != -1
ignore = true
callback null, ignore

View file

@ -14,6 +14,8 @@ module.exports = Features =
return Settings.enableGithubSync
when 'v1-return-message'
return Settings.accountMerge? and Settings.overleaf?
when 'v2-banner'
return Settings.showV2Banner
when 'custom-togglers'
return Settings.overleaf?
when 'templates'

View file

@ -26,6 +26,7 @@ HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
FileStoreController = require("./Features/FileStore/FileStoreController")
HistoryController = require("./Features/History/HistoryController")
ExportsController = require("./Features/Exports/ExportsController")
PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
ChatController = require("./Features/Chat/ChatController")
@ -205,6 +206,7 @@ module.exports = class Router
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
webRouter.post '/project/:project_id/export/:brand_variation_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportProject
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects

View file

@ -22,7 +22,7 @@ script(type='text/ng-template', id='supportModalTemplate')
tabindex='1',
onkeyup='')
.contact-suggestions(ng-show="suggestions.length")
p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>__kb__</a>", kb: translate("knowledge_base") })}
p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>" + translate("knowledge_base") + "</a>" })}
ul.contact-suggestion-list
li(ng-repeat="suggestion in suggestions")
a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")

View file

@ -371,40 +371,40 @@ div.full-size.pdf(ng-controller="PdfController")
a.text-info(href="https://www.sharelatex.com/learn/Debugging_Compilation_timeout_errors", target="_blank")
| #{translate("learn_how_to_make_documents_compile_quickly")}
.alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile")
p(ng-if="project.owner._id == user.id")
strong #{translate("upgrade_for_faster_compiles")}
p(ng-if="project.owner._id != user.id")
strong #{translate("ask_proj_owner_to_upgrade_for_faster_compiles")}
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
p Plus:
div
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id")
a.btn.btn-success.row-spaced-small(
href
ng-class="buttonClass"
ng-click="startFreeTrial('compile-timeout')"
) #{translate("start_free_trial")}
if settings.enableSubscriptions
.alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile")
p(ng-if="project.owner._id == user.id")
strong #{translate("upgrade_for_faster_compiles")}
p(ng-if="project.owner._id != user.id")
strong #{translate("ask_proj_owner_to_upgrade_for_faster_compiles")}
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
p Plus:
div
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id")
a.btn.btn-success.row-spaced-small(
href
ng-class="buttonClass"
ng-click="startFreeTrial('compile-timeout')"
) #{translate("start_free_trial")}
.alert.alert-danger(ng-show="pdf.autoCompileDisabled")
p

View file

@ -1,4 +1,4 @@
if (user.awareOfV2 && !settings.overleaf)
if hasFeature('v2-banner')
.userNotifications
ul.list-unstyled.notifications-list(ng-controller="OverleafV2NotificationController", ng-show="visible")
li.notification_entry

View file

@ -156,6 +156,10 @@ module.exports = settings =
url: process.env['LINKED_URL_PROXY']
thirdpartyreferences:
url: "http://#{process.env['THIRD_PARTY_REFERENCES_HOST'] or 'localhost'}:3046"
v1:
url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000"
user: 'overleaf'
pass: 'password'
templates:
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
@ -363,7 +367,7 @@ module.exports = settings =
appName: "ShareLaTeX (Community Edition)"
adminEmail: "placeholder@example.com"
brandPrefix: "" # Set to 'ol-' for overleaf styles
nav:

View file

@ -17,6 +17,7 @@ services:
PROJECT_HISTORY_ENABLED: 'true'
ENABLED_LINKED_FILE_TYPES: 'url'
LINKED_URL_PROXY: 'http://localhost:6543'
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
depends_on:
- redis
- mongo

View file

@ -1,5 +1,6 @@
define [
"ide/editor/Document"
"ide/editor/components/spellMenu"
"ide/editor/directives/aceEditor"
"ide/editor/directives/toggleSwitch"
"ide/editor/controllers/SavingNotificationController"

View file

@ -0,0 +1,34 @@
define ["base"], (App) ->
App.component "spellMenu", {
bindings: {
open: "<"
top: "<"
left: "<"
highlight: "<"
replaceWord: "&"
learnWord: "&"
}
template: """
<div
class="dropdown context-menu spell-check-menu"
ng-show="$ctrl.open"
ng-style="{top: $ctrl.top, left: $ctrl.left}"
ng-class="{open: $ctrl.open}"
>
<ul class="dropdown-menu">
<li ng-repeat="suggestion in $ctrl.highlight.suggestions | limitTo:8">
<a
href
ng-click="$ctrl.replaceWord({ highlight: $ctrl.highlight, suggestion: suggestion })"
>
{{ suggestion }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="$ctrl.learnWord({ highlight: $ctrl.highlight })">Add to Dictionary</a>
</li>
</ul>
</div>
"""
}

View file

@ -7,6 +7,7 @@ define [
"ide/editor/directives/aceEditor/undo/UndoManager"
"ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager"
"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
"ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter"
"ide/editor/directives/aceEditor/highlights/HighlightsManager"
"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
@ -15,7 +16,7 @@ define [
"ide/graphics/services/graphics"
"ide/preamble/services/preamble"
"ide/files/services/files"
], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, SpellCheckAdapter, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
EditSession = ace.require('ace/edit_session').EditSession
ModeList = ace.require('ace/ext/modelist')
Vim = ace.require('ace/keyboard/vim').Vim
@ -103,7 +104,8 @@ define [
if scope.spellCheck # only enable spellcheck when explicitly required
spellCheckCache = $cacheFactory.get("spellCheck-#{scope.name}") || $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000})
spellCheckManager = new SpellCheckManager(scope, editor, element, spellCheckCache, $http, $q)
spellCheckManager = new SpellCheckManager(scope, spellCheckCache, $http, $q, new SpellCheckAdapter(editor))
undoManager = new UndoManager(scope, editor, element)
highlightsManager = new HighlightsManager(scope, editor, element)
cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
@ -361,6 +363,23 @@ define [
session.setScrollTop(session.getScrollTop() + 1)
session.setScrollTop(session.getScrollTop() - 1)
onSessionChangeForSpellCheck = (e) ->
spellCheckManager.onSessionChange()
e.oldSession?.getDocument().off "change", spellCheckManager.onChange
e.session.getDocument().on "change", spellCheckManager.onChange
e.oldSession?.off "changeScrollTop", spellCheckManager.onScroll
e.session.on "changeScrollTop", spellCheckManager.onScroll
initSpellCheck = () ->
spellCheckManager.init()
editor.on 'changeSession', onSessionChangeForSpellCheck
onSessionChangeForSpellCheck({ session: editor.getSession() }) # Force initial setup
editor.on 'nativecontextmenu', spellCheckManager.onContextMenu
tearDownSpellCheck = () ->
editor.off 'changeSession', onSessionChangeForSpellCheck
editor.off 'nativecontextmenu', spellCheckManager.onContextMenu
attachToAce = (sharejs_doc) ->
lines = sharejs_doc.getSnapshot().split("\n")
session = editor.getSession()
@ -406,6 +425,7 @@ define [
editor.initing = false
# now ready to edit document
editor.setReadOnly(scope.readOnly) # respect the readOnly setting, normally false
initSpellCheck()
resetScrollMargins()
@ -467,6 +487,7 @@ define [
scope.$on '$destroy', () ->
if scope.sharejsDoc?
tearDownSpellCheck()
detachFromAce(scope.sharejsDoc)
session = editor.getSession()
session?.destroy()
@ -488,22 +509,14 @@ define [
>Dismiss</a>
</div>
<div class="ace-editor-body"></div>
<div
class="dropdown context-menu spell-check-menu"
ng-show="spellingMenu.open"
ng-style="{top: spellingMenu.top, left: spellingMenu.left}"
ng-class="{open: spellingMenu.open}"
>
<ul class="dropdown-menu">
<li ng-repeat="suggestion in spellingMenu.highlight.suggestions | limitTo:8">
<a href ng-click="replaceWord(spellingMenu.highlight, suggestion)">{{ suggestion }}</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="learnWord(spellingMenu.highlight)">Add to Dictionary</a>
</li>
</ul>
</div>
<spell-menu
open="spellMenu.open"
top="spellMenu.top"
left="spellMenu.left"
highlight="spellMenu.highlight"
replace-word="replaceWord(highlight, suggestion)"
learn-word="learnWord(highlight)"
></spell-menu>
<div
class="annotation-label"
ng-show="annotationLabel.show"

View file

@ -4,144 +4,93 @@ define [
Range = ace.require("ace/range").Range
class Highlight
constructor: (options) ->
@row = options.row
@column = options.column
constructor: (@markerId, @range, options) ->
@word = options.word
@suggestions = options.suggestions
class HighlightedWordManager
constructor: (@editor) ->
@reset()
reset: () ->
@highlights = rows: []
addHighlight: (highlight) ->
unless highlight instanceof Highlight
highlight = new Highlight(highlight)
range = new Range(
highlight.row, highlight.column,
highlight.row, highlight.column + highlight.word.length
)
highlight.markerId = @editor.getSession().addMarker range, "spelling-highlight", 'text', false
@highlights.rows[highlight.row] ||= []
@highlights.rows[highlight.row].push highlight
reset: () ->
@highlights?.forEach (highlight) =>
@editor.getSession().removeMarker(highlight.markerId)
@highlights = []
addHighlight: (options) ->
session = @editor.getSession()
doc = session.getDocument()
# Set up Range that will automatically update it's positions when the
# document changes
range = new Range()
range.start = doc.createAnchor({
row: options.row,
column: options.column
})
range.end = doc.createAnchor({
row: options.row,
column: options.column + options.word.length
})
# Prevent range from adding newly typed characters to the end of the word.
# This makes it appear as if the spelling error continues to the next word
# even after a space
range.end.$insertRight = true
markerId = session.addMarker range, "spelling-highlight", 'text', false
@highlights.push new Highlight(markerId, range, options)
removeHighlight: (highlight) ->
@editor.getSession().removeMarker(highlight.markerId)
for h, i in @highlights.rows[highlight.row]
if h == highlight
@highlights.rows[highlight.row].splice(i, 1)
@highlights = @highlights.filter (hl) ->
hl != highlight
removeWord: (word) ->
toRemove = []
for row in @highlights.rows
for highlight in (row || [])
if highlight.word == word
toRemove.push(highlight)
for highlight in toRemove
@removeHighlight highlight
@highlights.filter (highlight) ->
highlight.word == word
.forEach (highlight) =>
@removeHighlight(highlight)
moveHighlight: (highlight, position) ->
@removeHighlight highlight
highlight.row = position.row
highlight.column = position.column
@addHighlight highlight
clearRows: (from, to) ->
from ||= 0
to ||= @highlights.rows.length - 1
for row in @highlights.rows.slice(from, to + 1)
for highlight in (row || []).slice(0)
@removeHighlight highlight
insertRows: (offset, number) ->
# rows are inserted after offset. i.e. offset row is not modified
affectedHighlights = []
for row in @highlights.rows.slice(offset)
affectedHighlights.push(highlight) for highlight in (row || [])
for highlight in affectedHighlights
@moveHighlight highlight,
row: highlight.row + number
column: highlight.column
removeRows: (offset, number) ->
# offset is the first row to delete
affectedHighlights = []
for row in @highlights.rows.slice(offset)
affectedHighlights.push(highlight) for highlight in (row || [])
for highlight in affectedHighlights
if highlight.row >= offset + number
@moveHighlight highlight,
row: highlight.row - number
column: highlight.column
else
@removeHighlight highlight
clearRow: (row) ->
@highlights.filter (highlight) ->
highlight.range.start.row == row
.forEach (highlight) =>
@removeHighlight(highlight)
findHighlightWithinRange: (range) ->
rows = @highlights.rows.slice(range.start.row, range.end.row + 1)
for row in rows
for highlight in (row || [])
if @_doesHighlightOverlapRange(highlight, range.start, range.end)
return highlight
return null
applyChange: (change) ->
start = change.start
end = change.end
if change.action == "insert"
if start.row != end.row
rowsAdded = end.row - start.row
@insertRows start.row + 1, rowsAdded
# make a copy since we're going to modify in place
oldHighlights = (@highlights.rows[start.row] || []).slice(0)
for highlight in oldHighlights
if highlight.column > start.column
# insertion was fully before this highlight
@moveHighlight highlight,
row: end.row
column: highlight.column + (end.column - start.column)
else if highlight.column + highlight.word.length >= start.column
# insertion was inside this highlight
@removeHighlight highlight
else if change.action == "remove"
if start.row == end.row
oldHighlights = (@highlights.rows[start.row] || []).slice(0)
else
rowsRemoved = end.row - start.row
oldHighlights =
(@highlights.rows[start.row] || []).concat(
(@highlights.rows[end.row] || [])
)
@removeRows start.row + 1, rowsRemoved
for highlight in oldHighlights
if @_doesHighlightOverlapRange highlight, start, end
@removeHighlight highlight
else if @_isHighlightAfterRange highlight, start, end
@moveHighlight highlight,
row: start.row
column: highlight.column - (end.column - start.column)
_.find @highlights, (highlight) =>
@_doesHighlightOverlapRange highlight, range.start, range.end
_doesHighlightOverlapRange: (highlight, start, end) ->
highlightRow = highlight.range.start.row
highlightStartColumn = highlight.range.start.column
highlightEndColumn = highlight.range.end.column
highlightIsAllBeforeRange =
highlight.row < start.row or
(highlight.row == start.row and highlight.column + highlight.word.length <= start.column)
highlightRow < start.row or
(highlightRow == start.row and highlightEndColumn <= start.column)
highlightIsAllAfterRange =
highlight.row > end.row or
(highlight.row == end.row and highlight.column >= end.column)
highlightRow > end.row or
(highlightRow == end.row and highlightStartColumn >= end.column)
!(highlightIsAllBeforeRange or highlightIsAllAfterRange)
_isHighlightAfterRange: (highlight, start, end) ->
return true if highlight.row > end.row
return false if highlight.row < end.row
highlight.column >= end.column
clearHighlightTouchingRange: (range) ->
highlight = _.find @highlights, (hl) =>
@_doesHighlightTouchRange hl, range.start, range.end
if highlight
@removeHighlight highlight
_doesHighlightTouchRange: (highlight, start, end) ->
highlightRow = highlight.range.start.row
highlightStartColumn = highlight.range.start.column
highlightEndColumn = highlight.range.end.column
rangeStartIsWithinHighlight =
highlightStartColumn <= start.column and
highlightEndColumn >= start.column
rangeEndIsWithinHighlight =
highlightStartColumn <= end.column and
highlightEndColumn >= end.column
highlightRow == start.row and
(rangeStartIsWithinHighlight or rangeEndIsWithinHighlight)

View file

@ -0,0 +1,56 @@
define [
"ace/ace"
"ide/editor/directives/aceEditor/spell-check/HighlightedWordManager"
], (Ace, HighlightedWordManager) ->
Range = ace.require('ace/range').Range
class SpellCheckAdapter
constructor: (@editor) ->
@highlightedWordManager = new HighlightedWordManager(@editor)
getLines: () ->
@editor.getValue().split('\n')
normalizeChangeEvent: (e) -> e
getCoordsFromContextMenuEvent: (e) ->
e.domEvent.stopPropagation()
return {
x: e.domEvent.clientX,
y: e.domEvent.clientY
}
preventContextMenuEventDefault: (e) ->
e.domEvent.preventDefault()
getHighlightFromCoords: (coords) ->
position = @editor.renderer.screenToTextCoordinates(coords.x, coords.y)
@highlightedWordManager.findHighlightWithinRange({
start: position
end: position
})
selectHighlightedWord: (highlight) ->
row = highlight.range.start.row
startColumn = highlight.range.start.column
endColumn = highlight.range.end.column
@editor.getSession().getSelection().setSelectionRange(
new Range(
row, startColumn,
row, endColumn
)
)
replaceWord: (highlight, newWord) =>
row = highlight.range.start.row
startColumn = highlight.range.start.column
endColumn = highlight.range.end.column
@editor.getSession().replace(new Range(
row, startColumn,
row, endColumn
), newWord)
# Bring editor back into focus after clicking on suggestion
@editor.focus()

View file

@ -1,129 +1,88 @@
define [
"ide/editor/directives/aceEditor/spell-check/HighlightedWordManager"
"ace/ace"
], (HighlightedWordManager) ->
Range = ace.require("ace/range").Range
define [], () ->
class SpellCheckManager
constructor: (@$scope, @editor, @element, @cache, @$http, @$q) ->
$(document.body).append @element.find(".spell-check-menu")
constructor: (@$scope, @cache, @$http, @$q, @adapter) ->
@$scope.spellMenu = {
open: false
top: '0px'
left: '0px'
suggestions: []
}
@inProgressRequest = null
@updatedLines = []
@highlightedWordManager = new HighlightedWordManager(@editor)
@$scope.$watch "spellCheckLanguage", (language, oldLanguage) =>
@$scope.$watch 'spellCheckLanguage', (language, oldLanguage) =>
if language != oldLanguage and oldLanguage?
@runFullCheck()
onChange = (e) =>
@runCheckOnChange(e)
onScroll = () =>
@closeContextMenu()
@$scope.replaceWord = @adapter.replaceWord
@$scope.learnWord = @learnWord
@editor.on "changeSession", (e) =>
@highlightedWordManager.reset()
if @inProgressRequest?
@inProgressRequest.abort()
if @$scope.spellCheckEnabled and @$scope.spellCheckLanguage and @$scope.spellCheckLanguage != ""
@runSpellCheckSoon(200)
e.oldSession?.getDocument().off "change", onChange
e.session.getDocument().on "change", onChange
e.oldSession?.off "changeScrollTop", onScroll
e.session.on "changeScrollTop", onScroll
@$scope.spellingMenu = {left: '0px', top: '0px'}
@editor.on "nativecontextmenu", (e) =>
e.domEvent.stopPropagation();
@closeContextMenu(e.domEvent)
@openContextMenu(e.domEvent)
$(document).on "click", (e) =>
if e.which != 3 # Ignore if this was a right click
@closeContextMenu(e)
$(document).on 'click', (e) =>
@closeContextMenu() if e.which != 3 # Ignore if right click
return true
@$scope.replaceWord = (highlight, suggestion) =>
@replaceWord(highlight, suggestion)
init: () ->
@updatedLines = Array(@adapter.getLines().length).fill(true)
@runSpellCheckSoon(200) if @isSpellCheckEnabled()
@$scope.learnWord = (highlight) =>
@learnWord(highlight)
isSpellCheckEnabled: () ->
return !!(
@$scope.spellCheck and
@$scope.spellCheckLanguage and
@$scope.spellCheckLanguage != ''
)
runFullCheck: () ->
@highlightedWordManager.clearRows()
if @$scope.spellCheckLanguage and @$scope.spellCheckLanguage != ""
@runSpellCheck()
onChange: (e) =>
if @isSpellCheckEnabled()
@markLinesAsUpdated(@adapter.normalizeChangeEvent(e))
@adapter.highlightedWordManager.clearHighlightTouchingRange(e)
runCheckOnChange: (e) ->
if @$scope.spellCheckLanguage and @$scope.spellCheckLanguage != ""
@highlightedWordManager.applyChange(e)
@markLinesAsUpdated(e)
@runSpellCheckSoon()
onSessionChange: () =>
@adapter.highlightedWordManager.reset()
@inProgressRequest.abort() if @inProgressRequest?
@runSpellCheckSoon(200) if @isSpellCheckEnabled()
onContextMenu: (e) =>
@closeContextMenu()
@openContextMenu(e)
onScroll: () => @closeContextMenu()
openContextMenu: (e) ->
position = @editor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
highlight = @highlightedWordManager.findHighlightWithinRange
start: position
end: position
@$scope.$apply () =>
@$scope.spellingMenu.highlight = highlight
coords = @adapter.getCoordsFromContextMenuEvent(e)
highlight = @adapter.getHighlightFromCoords(coords)
if highlight
e.stopPropagation()
e.preventDefault()
@editor.getSession().getSelection().setSelectionRange(
new Range(
highlight.row, highlight.column
highlight.row, highlight.column + highlight.word.length
)
)
@adapter.preventContextMenuEventDefault(e)
@adapter.selectHighlightedWord(highlight)
@$scope.$apply () =>
@$scope.spellingMenu.open = true
@$scope.spellingMenu.left = e.clientX + 'px'
@$scope.spellingMenu.top = e.clientY + 'px'
@$scope.spellMenu = {
open: true
top: coords.y + 'px'
left: coords.x + 'px'
highlight: highlight
}
return false
closeContextMenu: (e) ->
# this is triggered on scroll, so for performance only apply
# setting when it changes
if @$scope?.spellingMenu?.open != false
closeContextMenu: () ->
# This is triggered on scroll, so for performance only apply setting when
# it changes
if @$scope?.spellMenu and @$scope.spellMenu.open != false
@$scope.$apply () =>
@$scope.spellingMenu.open = false
@$scope.spellMenu.open = false
replaceWord: (highlight, text) ->
@editor.getSession().replace(new Range(
highlight.row, highlight.column,
highlight.row, highlight.column + highlight.word.length
), text)
learnWord: (highlight) ->
learnWord: (highlight) =>
@apiRequest "/learn", word: highlight.word
@highlightedWordManager.removeWord highlight.word
@adapter.highlightedWordManager.removeWord highlight.word
language = @$scope.spellCheckLanguage
@cache?.put("#{language}:#{highlight.word}", true)
getHighlightedWordAtCursor: () ->
cursor = @editor.getCursorPosition()
highlight = @highlightedWordManager.findHighlightWithinRange
start: cursor
end: cursor
return highlight
runSpellCheckSoon: (delay = 1000) ->
run = () =>
delete @timeoutId
@runSpellCheck(@updatedLines)
@updatedLines = []
if @timeoutId?
clearTimeout @timeoutId
@timeoutId = setTimeout run, delay
runFullCheck: () ->
@adapter.highlightedWordManager.reset()
@runSpellCheck() if @isSpellCheckEnabled()
markLinesAsUpdated: (change) ->
start = change.start
@ -146,6 +105,15 @@ define [
@updatedLines[start.row] = true
removeLines()
runSpellCheckSoon: (delay = 1000) ->
run = () =>
delete @timeoutId
@runSpellCheck(@updatedLines)
@updatedLines = []
if @timeoutId?
clearTimeout @timeoutId
@timeoutId = setTimeout run, delay
runSpellCheck: (linesToProcess) ->
{words, positions} = @getWords(linesToProcess)
language = @$scope.spellCheckLanguage
@ -178,11 +146,11 @@ define [
displayResult = (highlights) =>
if linesToProcess?
for shouldProcess, row in linesToProcess
@highlightedWordManager.clearRows(row, row) if shouldProcess
@adapter.highlightedWordManager.clearRow(row) if shouldProcess
else
@highlightedWordManager.clearRows()
@adapter.highlightedWordManager.reset()
for highlight in highlights
@highlightedWordManager.addHighlight highlight
@adapter.highlightedWordManager.addHighlight highlight
if not words.length
displayResult highlights
@ -212,8 +180,24 @@ define [
seen[key] = true
displayResult highlights
apiRequest: (endpoint, data, callback = (error, result) ->)->
data.token = window.user.id
data._csrf = window.csrfToken
# use angular timeout option to cancel request if doc is changed
requestHandler = @$q.defer()
options = {timeout: requestHandler.promise}
httpRequest = @$http.post("/spelling" + endpoint, data, options)
.then (response) =>
callback(null, response.data)
.catch (response) =>
callback(new Error('api failure'))
# provide a method to cancel the request
abortRequest = () ->
requestHandler.resolve()
return { abort: abortRequest }
getWords: (linesToProcess) ->
lines = @editor.getValue().split("\n")
lines = @adapter.getLines()
words = []
positions = []
for line, row in lines
@ -232,22 +216,6 @@ define [
words.push(word)
return words: words, positions: positions
apiRequest: (endpoint, data, callback = (error, result) ->)->
data.token = window.user.id
data._csrf = window.csrfToken
# use angular timeout option to cancel request if doc is changed
requestHandler = @$q.defer()
options = {timeout: requestHandler.promise}
httpRequest = @$http.post("/spelling" + endpoint, data, options)
.then (response) =>
callback(null, response.data)
.catch (response) =>
callback(new Error('api failure'))
# provide a method to cancel the request
abortRequest = () ->
requestHandler.resolve()
return { abort: abortRequest }
blacklistedCommandRegex: ///
\\ # initial backslash
(label # any of these commands

View file

@ -55,32 +55,4 @@ define [
hits = _.map response.hits, buildHitViewModel
updateHits hits
$scope.showMissingTemplateModal = () ->
modalInstance = $modal.open(
templateUrl: "missingWikiPageModal"
controller: "MissingWikiPageController"
)
App.controller 'MissingWikiPageController', ($scope, $modalInstance) ->
$scope.form = {}
$scope.sent = false
$scope.sending = false
$scope.contactUs = ->
if !$scope.form.message?
console.log "message not set"
return
$scope.sending = true
ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32)
params =
email: $scope.form.email or "support@sharelatex.com"
message: $scope.form.message or ""
subject: "new wiki page sujection - [#{ticketNumber}]"
labels: "support wiki"
Groove.createTicket params, (err, json)->
$scope.sent = true
$scope.$apply()
$scope.close = () ->
$modalInstance.close()

View file

@ -219,4 +219,11 @@
font-style: italic;
color: #999;
}
.spelling-error {
background-image: url(/img/spellcheck-underline.png);
background-repeat: repeat-x;
background-position: bottom;
}
}

View file

@ -0,0 +1,56 @@
expect = require('chai').expect
request = require './helpers/request'
_ = require 'underscore'
User = require './helpers/User'
ProjectGetter = require '../../../app/js/Features/Project/ProjectGetter.js'
ExportsHandler = require '../../../app/js/Features/Exports/ExportsHandler.js'
MockProjectHistoryApi = require './helpers/MockProjectHistoryApi'
MockV1Api = require './helpers/MockV1Api'
describe 'Exports', ->
before (done) ->
@brand_variation_id = '18'
@owner = new User()
@owner.login (error) =>
throw error if error?
@owner.createProject 'example-project', {template: 'example'}, (error, @project_id) =>
throw error if error?
done()
describe 'exporting a project', ->
beforeEach (done) ->
@version = Math.floor(Math.random() * 10000)
MockProjectHistoryApi.setProjectVersion(@project_id, @version)
@export_id = Math.floor(Math.random() * 10000)
MockV1Api.setExportId(@export_id)
MockV1Api.clearExportParams()
@owner.request {
method: 'POST',
url: "/project/#{@project_id}/export/#{@brand_variation_id}",
json: {},
}, (error, response, body) =>
throw error if error?
expect(response.statusCode).to.equal 200
@exportResponseBody = body
done()
it 'should have sent correct data to v1', (done) ->
{project, user, destination, options} = MockV1Api.getLastExportParams()
# project details should match
expect(project.id).to.equal @project_id
expect(project.rootDocPath).to.equal '/main.tex'
# version should match what was retrieved from project-history
expect(project.historyVersion).to.equal @version
# user details should match
expect(user.id).to.equal @owner.id
expect(user.email).to.equal @owner.email
# brand-variation should match
expect(destination.brandVariationId).to.equal @brand_variation_id
done()
it 'should have returned the export ID provided by v1', (done) ->
expect(@exportResponseBody.export_v1_id).to.equal @export_id
done()

View file

@ -16,6 +16,7 @@ createInvite = (sendingUser, projectId, email, callback=(err, invite)->) ->
privileges: 'readAndWrite'
}, (err, response, body) ->
return callback(err) if err
expect(response.statusCode).to.equal 200
callback(null, body.invite)
createProject = (owner, projectName, callback=(err, projectId, project)->) ->
@ -207,9 +208,9 @@ describe "ProjectInviteTests", ->
@email = 'smoketestuser@example.com'
@projectName = 'sharing test'
Async.series [
(cb) => @user.login cb
(cb) => @user.logout cb
(cb) => @user.ensureUserExists cb
(cb) => @sendingUser.login cb
(cb) => @sendingUser.setFeatures { collaborators: 10 }, cb
], done
describe 'creating invites', ->
@ -266,7 +267,7 @@ describe "ProjectInviteTests", ->
(cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 0, cb
], done
it 'should allow the project owner to many invites at once', (done) ->
it 'should allow the project owner to create many invites at once', (done) ->
@inviteOne = null
@inviteTwo = null
Async.series [

View file

@ -0,0 +1,151 @@
expect = require("chai").expect
async = require("async")
UserClient = require "./helpers/User"
request = require "./helpers/request"
settings = require "settings-sharelatex"
{ObjectId} = require("../../../app/js/infrastructure/mongojs")
Subscription = require("../../../app/js/models/Subscription").Subscription
User = require("../../../app/js/models/User").User
MockV1Api = require "./helpers/MockV1Api"
syncUserAndGetFeatures = (user, callback = (error, features) ->) ->
request {
method: 'POST',
url: "/user/#{user._id}/features/sync",
auth:
user: 'sharelatex'
pass: 'password'
sendImmediately: true
}, (error, response, body) ->
throw error if error?
expect(response.statusCode).to.equal 200
User.findById user._id, (error, user) ->
return callback(error) if error?
features = user.toObject().features
delete features.$init # mongoose internals
return callback null, features
describe "Subscriptions", ->
beforeEach (done) ->
@user = new UserClient()
@user.ensureUserExists (error) ->
throw error if error?
done()
describe "when user has no subscriptions", ->
it "should set their features to the basic set", (done) ->
syncUserAndGetFeatures @user, (error, features) =>
throw error if error?
expect(features).to.deep.equal(settings.defaultFeatures)
done()
describe "when the user has an individual subscription", ->
beforeEach ->
Subscription.create {
admin_id: @user._id
planCode: 'collaborator'
customAccount: true
} # returns a promise
it "should set their features to the upgraded set", (done) ->
syncUserAndGetFeatures @user, (error, features) =>
throw error if error?
plan = settings.plans.find (plan) -> plan.planCode == 'collaborator'
expect(features).to.deep.equal(plan.features)
done()
describe "when the user is in a group subscription", ->
beforeEach ->
Subscription.create {
admin_id: ObjectId()
member_ids: [@user._id]
groupAccount: true
planCode: 'collaborator'
customAccount: true
} # returns a promise
it "should set their features to the upgraded set", (done) ->
syncUserAndGetFeatures @user, (error, features) =>
throw error if error?
plan = settings.plans.find (plan) -> plan.planCode == 'collaborator'
expect(features).to.deep.equal(plan.features)
done()
describe "when the user has bonus features", ->
beforeEach ->
User.update {
_id: @user._id
}, {
refered_user_count: 10
} # returns a promise
it "should set their features to the bonus set", (done) ->
syncUserAndGetFeatures @user, (error, features) =>
throw error if error?
expect(features).to.deep.equal(Object.assign(
{}, settings.defaultFeatures, settings.bonus_features[9]
))
done()
describe "when the user has a v1 plan", ->
beforeEach ->
MockV1Api.setUser 42, plan_name: 'free'
User.update {
_id: @user._id
}, {
overleaf:
id: 42
} # returns a promise
it "should set their features to the v1 plan", (done) ->
syncUserAndGetFeatures @user, (error, features) =>
throw error if error?
plan = settings.plans.find (plan) -> plan.planCode == 'v1_free'
expect(features).to.deep.equal(plan.features)
done()
describe "when the user has a v1 plan and bonus features", ->
beforeEach ->
MockV1Api.setUser 42, plan_name: 'free'
User.update {
_id: @user._id
}, {
overleaf:
id: 42
refered_user_count: 10
} # returns a promise
it "should set their features to the best of the v1 plan and bonus features", (done) ->
syncUserAndGetFeatures @user, (error, features) =>
throw error if error?
v1plan = settings.plans.find (plan) -> plan.planCode == 'v1_free'
expectedFeatures = Object.assign(
{}, v1plan.features, settings.bonus_features[9]
)
expect(features).to.deep.equal(expectedFeatures)
done()
describe "when the user has a group and personal subscription", ->
beforeEach (done) ->
Subscription.create {
admin_id: @user._id
planCode: 'professional'
customAccount: true
}, (error) =>
throw error if error?
Subscription.create {
admin_id: ObjectId()
member_ids: [@user._id]
groupAccount: true
planCode: 'collaborator'
customAccount: true
}, done
return
it "should set their features to the best set", (done) ->
syncUserAndGetFeatures @user, (error, features) =>
throw error if error?
plan = settings.plans.find (plan) -> plan.planCode == 'professional'
expect(features).to.deep.equal(plan.features)
done()

View file

@ -6,9 +6,14 @@ module.exports = MockProjectHistoryApi =
oldFiles: {}
projectVersions: {}
addOldFile: (project_id, version, pathname, content) ->
@oldFiles["#{project_id}:#{version}:#{pathname}"] = content
setProjectVersion: (project_id, version) ->
@projectVersions[project_id] = version
run: () ->
app.post "/project", (req, res, next) =>
res.json project: id: 1
@ -21,6 +26,13 @@ module.exports = MockProjectHistoryApi =
else
res.send 404
app.get "/project/:project_id/version", (req, res, next) =>
{project_id} = req.params
if @projectVersions[project_id]?
res.json version: @projectVersions[project_id]
else
res.send 404
app.listen 3054, (error) ->
throw error if error?
.on "error", (error) ->

View file

@ -0,0 +1,45 @@
express = require("express")
app = express()
bodyParser = require('body-parser')
app.use(bodyParser.json())
module.exports = MockV1Api =
users: { }
setUser: (id, user) ->
@users[id] = user
exportId: null
exportParams: null
setExportId: (id) ->
@exportId = id
getLastExportParams: () ->
@exportParams
clearExportParams: () ->
@exportParams = null
run: () ->
app.get "/api/v1/sharelatex/users/:ol_user_id/plan_code", (req, res, next) =>
user = @users[req.params.ol_user_id]
if user
res.json user
else
res.sendStatus 404
app.post "/api/v1/sharelatex/exports", (req, res, next) =>
#{project, version, pathname}
@exportParams = Object.assign({}, req.body)
res.json exportId: @exportId
app.listen 5000, (error) ->
throw error if error?
.on "error", (error) ->
console.error "error starting MockV1Api:", error.message
process.exit(1)
MockV1Api.run()

View file

@ -40,6 +40,12 @@ class User
@referal_id = user?.referal_id
callback(null, @password)
setFeatures: (features, callback = (error) ->) ->
update = {}
for key, value of features
update["features.#{key}"] = value
UserModel.update { _id: @id }, update, callback
logout: (callback = (error) ->) ->
@getCsrfToken (error) =>
return callback(error) if error?

View file

@ -0,0 +1,95 @@
module.exports =
enableSubscriptions: true
features: features =
v1_free:
collaborators: 1
dropbox: false
versioning: false
github: true
templates: false
references: false
referencesSearch: false
mendeley: true
compileTimeout: 60
compileGroup: "standard"
trackChanges: false
personal:
collaborators: 1
dropbox: false
versioning: false
github: false
templates: false
references: false
referencesSearch: false
mendeley: false
compileTimeout: 60
compileGroup: "standard"
trackChanges: false
collaborator:
collaborators: 10
dropbox: true
versioning: true
github: true
templates: true
references: true
referencesSearch: true
mendeley: true
compileTimeout: 180
compileGroup: "priority"
trackChanges: true
professional:
collaborators: -1
dropbox: true
versioning: true
github: true
templates: true
references: true
referencesSearch: true
mendeley: true
compileTimeout: 180
compileGroup: "priority"
trackChanges: true
defaultFeatures: features.personal
defaultPlanCode: 'personal'
plans: plans = [{
planCode: "v1_free"
name: "V1 Free"
price: 0
features: features.v1_free
},{
planCode: "personal"
name: "Personal"
price: 0
features: features.personal
},{
planCode: "collaborator"
name: "Collaborator"
price: 1500
features: features.collaborator
},{
planCode: "professional"
name: "Professional"
price: 3000
features: features.professional
}]
bonus_features:
1:
collaborators: 2
dropbox: false
versioning: false
3:
collaborators: 4
dropbox: false
versioning: false
6:
collaborators: 4
dropbox: true
versioning: true
9:
collaborators: -1
dropbox: true
versioning: true

View file

@ -0,0 +1,39 @@
SandboxedModule = require('sandboxed-module')
assert = require('assert')
chai = require('chai')
expect = chai.expect
sinon = require('sinon')
modulePath = require('path').join __dirname, '../../../../app/js/Features/Exports/ExportsController.js'
describe 'ExportsController', ->
project_id = "123njdskj9jlk"
user_id = "123nd3ijdks"
brand_variation_id = 22
beforeEach ->
@handler =
getUserNotifications: sinon.stub().callsArgWith(1)
@req =
params:
project_id: project_id
brand_variation_id: brand_variation_id
session:
user:
_id:user_id
i18n:
translate:->
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@req.session.user._id)
@controller = SandboxedModule.require modulePath, requires:
"./ExportsHandler":@handler
'logger-sharelatex':
log:->
err:->
'../Authentication/AuthenticationController': @AuthenticationController
it 'should ask the handler to perform the export', (done) ->
@handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897})
@controller.exportProject @req, send:(body) =>
expect(body).to.deep.equal {export_v1_id: 897}
done()

View file

@ -0,0 +1,202 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = '../../../../app/js/Features/Exports/ExportsHandler.js'
SandboxedModule = require('sandboxed-module')
describe 'ExportsHandler', ->
beforeEach ->
@ProjectGetter = {}
@ProjectLocator = {}
@UserGetter = {}
@settings = {}
@stubRequest = {}
@request = defaults: => return @stubRequest
@ExportsHandler = SandboxedModule.require modulePath, requires:
'logger-sharelatex':
log: ->
err: ->
'../Project/ProjectGetter': @ProjectGetter
'../Project/ProjectLocator': @ProjectLocator
'../User/UserGetter': @UserGetter
'settings-sharelatex': @settings
'request': @request
@project_id = "project-id-123"
@project_history_id = 987
@user_id = "user-id-456"
@brand_variation_id = 789
@callback = sinon.stub()
describe 'exportProject', ->
beforeEach (done) ->
@export_data = {iAmAnExport: true}
@response_body = {iAmAResponseBody: true}
@ExportsHandler._buildExport = sinon.stub().yields(null, @export_data)
@ExportsHandler._requestExport = sinon.stub().yields(null, @response_body)
@ExportsHandler.exportProject @project_id, @user_id, @brand_variation_id, (error, export_data) =>
@callback(error, export_data)
done()
it "should build the export", ->
@ExportsHandler._buildExport
.calledWith(@project_id, @user_id, @brand_variation_id)
.should.equal true
it "should request the export", ->
@ExportsHandler._requestExport
.calledWith(@export_data)
.should.equal true
it "should return the export", ->
@callback
.calledWith(null, @export_data)
.should.equal true
describe '_buildExport', ->
beforeEach (done) ->
@project =
id: @project_id
overleaf:
history:
id: @project_history_id
@user =
id: @user_id
first_name: 'Arthur'
last_name: 'Author'
email: 'arthur.author@arthurauthoring.org'
@rootDocPath = 'main.tex'
@historyVersion = 777
@ProjectGetter.getProject = sinon.stub().yields(null, @project)
@ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'main.tex'}])
@UserGetter.getUser = sinon.stub().yields(null, @user)
@ExportsHandler._requestVersion = sinon.stub().yields(null, @historyVersion)
done()
describe "when all goes well", ->
beforeEach (done) ->
@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
@callback(error, export_data)
done()
it "should request the project history version", ->
@ExportsHandler._requestVersion.called
.should.equal true
it "should return export data", ->
expected_export_data =
project:
id: @project_id
rootDocPath: @rootDocPath
historyId: @project_history_id
historyVersion: @historyVersion
user:
id: @user_id
firstName: @user.first_name
lastName: @user.last_name
email: @user.email
orcidId: null
destination:
brandVariationId: @brand_variation_id
options:
callbackUrl: null
@callback.calledWith(null, expected_export_data)
.should.equal true
describe "when project is not found", ->
beforeEach (done) ->
@ProjectGetter.getProject = sinon.stub().yields(new Error("project not found"))
@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
@callback(error, export_data)
done()
it "should return an error", ->
(@callback.args[0][0] instanceof Error)
.should.equal true
describe "when project has no root doc", ->
beforeEach (done) ->
@ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, null])
@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
@callback(error, export_data)
done()
it "should return an error", ->
(@callback.args[0][0] instanceof Error)
.should.equal true
describe "when user is not found", ->
beforeEach (done) ->
@UserGetter.getUser = sinon.stub().yields(new Error("user not found"))
@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
@callback(error, export_data)
done()
it "should return an error", ->
(@callback.args[0][0] instanceof Error)
.should.equal true
describe "when project history request fails", ->
beforeEach (done) ->
@ExportsHandler._requestVersion = sinon.stub().yields(new Error("project history call failed"))
@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
@callback(error, export_data)
done()
it "should return an error", ->
(@callback.args[0][0] instanceof Error)
.should.equal true
describe '_requestExport', ->
beforeEach (done) ->
@settings.apis =
v1:
url: 'http://localhost:5000'
user: 'overleaf'
pass: 'pass'
@export_data = {iAmAnExport: true}
@export_id = 4096
@stubPost = sinon.stub().yields(null, {statusCode: 200}, { exportId: @export_id })
done()
describe "when all goes well", ->
beforeEach (done) ->
@stubRequest.post = @stubPost
@ExportsHandler._requestExport @export_data, (error, export_v1_id) =>
@callback(error, export_v1_id)
done()
it 'should issue the request', ->
expect(@stubPost.getCall(0).args[0]).to.deep.equal
url: @settings.apis.v1.url + '/api/v1/sharelatex/exports'
auth:
user: @settings.apis.v1.user
pass: @settings.apis.v1.pass
json: @export_data
it 'should return the v1 export id', ->
@callback.calledWith(null, @export_id)
.should.equal true
describe "when the request fails", ->
beforeEach (done) ->
@stubRequest.post = sinon.stub().yields(new Error("export request failed"))
@ExportsHandler._requestExport @export_data, (error, export_v1_id) =>
@callback(error, export_v1_id)
done()
it "should return an error", ->
(@callback.args[0][0] instanceof Error)
.should.equal true
describe "when the request returns an error code", ->
beforeEach (done) ->
@stubRequest.post = sinon.stub().yields(null, {statusCode: 401}, { })
@ExportsHandler._requestExport @export_data, (error, export_v1_id) =>
@callback(error, export_v1_id)
done()
it "should return the error", ->
(@callback.args[0][0] instanceof Error)
.should.equal true

View file

@ -4,12 +4,12 @@ require('chai').should()
sinon = require('sinon')
modulePath = require('path').join __dirname, '../../../../app/js/Features/Referal/ReferalAllocator.js'
describe 'Referalallocator', ->
describe 'ReferalAllocator', ->
beforeEach ->
@ReferalAllocator = SandboxedModule.require modulePath, requires:
'../../models/User': User: @User = {}
"../Subscription/SubscriptionLocator": @SubscriptionLocator = {}
"../Subscription/FeaturesUpdater": @FeaturesUpdater = {}
"settings-sharelatex": @Settings = {}
'logger-sharelatex':
log:->
@ -26,7 +26,7 @@ describe 'Referalallocator', ->
@referal_source = "bonus"
@User.update = sinon.stub().callsArgWith 3, null
@User.findOne = sinon.stub().callsArgWith 1, null, { _id: @user_id }
@ReferalAllocator.assignBonus = sinon.stub().callsArg 1
@FeaturesUpdater.refreshFeatures = sinon.stub().yields()
@ReferalAllocator.allocate @referal_id, @new_user_id, @referal_source, @referal_medium, @callback
it 'should update the referring user with the refered users id', ->
@ -44,8 +44,8 @@ describe 'Referalallocator', ->
.calledWith( referal_id: @referal_id )
.should.equal true
it "shoudl assign the user their bonus", ->
@ReferalAllocator.assignBonus
it "should refresh the user's subscription", ->
@FeaturesUpdater.refreshFeatures
.calledWith(@user_id)
.should.equal true
@ -57,7 +57,7 @@ describe 'Referalallocator', ->
@referal_source = "public_share"
@User.update = sinon.stub().callsArgWith 3, null
@User.findOne = sinon.stub().callsArgWith 1, null, { _id: @user_id }
@ReferalAllocator.assignBonus = sinon.stub().callsArg 1
@FeaturesUpdater.refreshFeatures = sinon.stub().yields()
@ReferalAllocator.allocate @referal_id, @new_user_id, @referal_source, @referal_medium, @callback
it 'should not update the referring user with the refered users id', ->
@ -69,118 +69,7 @@ describe 'Referalallocator', ->
.should.equal true
it "should not assign the user a bonus", ->
@ReferalAllocator.assignBonus.called.should.equal false
@FeaturesUpdater.refreshFeatures.called.should.equal false
it "should call the callback", ->
@callback.called.should.equal true
describe "assignBonus", ->
beforeEach ->
@refered_user_count = 3
@Settings.bonus_features =
"3":
collaborators: 3
dropbox: false
versioning: false
stubbedUser = {
refered_user_count: @refered_user_count,
features:{collaborators:1, dropbox:false, versioning:false}
}
@User.findOne = sinon.stub().callsArgWith 1, null, stubbedUser
@User.update = sinon.stub().callsArgWith 2, null
@ReferalAllocator.assignBonus @user_id, @callback
it "should get the users number of refered user", ->
@User.findOne
.calledWith(_id: @user_id)
.should.equal true
it "should update the user to bonus features", ->
@User.update
.calledWith({
_id: @user_id
}, {
$set:
features:
@Settings.bonus_features[@refered_user_count.toString()]
})
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
describe "when there is nothing to assign", ->
beforeEach ->
@ReferalAllocator._calculateBonuses = sinon.stub().returns({})
@stubbedUser =
refered_user_count:4
features:{collaborators:3, versioning:true, dropbox:false}
@Settings.bonus_features =
"4":
collaborators:3
versioning:true
dropbox:false
@User.findOne = sinon.stub().callsArgWith 1, null, @stubbedUser
@User.update = sinon.stub().callsArgWith 2, null
it "should not call update if there are no bonuses to apply", (done)->
@ReferalAllocator.assignBonus @user_id, (err)=>
@User.update.called.should.equal false
done()
describe "when the user has better features already", ->
beforeEach ->
@refered_user_count = 3
@stubbedUser =
refered_user_count:4
features:
collaborators:3
dropbox:false
versioning:false
@Settings.bonus_features =
"4":
collaborators: 10
dropbox: true
versioning: false
@User.findOne = sinon.stub().callsArgWith 1, null, @stubbedUser
@User.update = sinon.stub().callsArgWith 2, null
it "should not set in in mongo when the feature is better", (done)->
@ReferalAllocator.assignBonus @user_id, =>
@User.update.calledWith({_id: @user_id }, {$set: features:{dropbox:true, versioning:false, collaborators:10} }).should.equal true
done()
it "should not overright if the user has -1 users", (done)->
@stubbedUser.features.collaborators = -1
@ReferalAllocator.assignBonus @user_id, =>
@User.update.calledWith({_id: @user_id }, {$set: features:{dropbox:true, versioning:false, collaborators:-1} }).should.equal true
done()
describe "when the user is not at a bonus level", ->
beforeEach ->
@refered_user_count = 0
@Settings.bonus_features =
"1":
collaborators: 3
dropbox: false
versioning: false
@User.findOne = sinon.stub().callsArgWith 1, null, { refered_user_count: @refered_user_count }
@User.update = sinon.stub().callsArgWith 2, null
@ReferalAllocator.assignBonus @user_id, @callback
it "should get the users number of refered user", ->
@User.findOne
.calledWith(_id: @user_id)
.should.equal true
it "should not update the user to bonus features", ->
@User.update.called.should.equal false
it "should call the callback", ->
@callback.called.should.equal true

View file

@ -0,0 +1,65 @@
SandboxedModule = require('sandboxed-module')
assert = require('assert')
require('chai').should()
sinon = require('sinon')
modulePath = require('path').join __dirname, '../../../../app/js/Features/Referal/ReferalFeatures.js'
describe 'ReferalFeatures', ->
beforeEach ->
@ReferalFeatures = SandboxedModule.require modulePath, requires:
'../../models/User': User: @User = {}
"settings-sharelatex": @Settings = {}
'logger-sharelatex':
log:->
err:->
@callback = sinon.stub()
@referal_id = "referal-id-123"
@referal_medium = "twitter"
@user_id = "user-id-123"
@new_user_id = "new-user-id-123"
describe "getBonusFeatures", ->
beforeEach ->
@refered_user_count = 3
@Settings.bonus_features =
"3":
collaborators: 3
dropbox: false
versioning: false
stubbedUser = {
refered_user_count: @refered_user_count,
features:{collaborators:1, dropbox:false, versioning:false}
}
@User.findOne = sinon.stub().callsArgWith 1, null, stubbedUser
@ReferalFeatures.getBonusFeatures @user_id, @callback
it "should get the users number of refered user", ->
@User.findOne
.calledWith(_id: @user_id)
.should.equal true
it "should call the callback with the features", ->
@callback.calledWith(null, @Settings.bonus_features[3]).should.equal true
describe "when the user is not at a bonus level", ->
beforeEach ->
@refered_user_count = 0
@Settings.bonus_features =
"1":
collaborators: 3
dropbox: false
versioning: false
@User.findOne = sinon.stub().callsArgWith 1, null, { refered_user_count: @refered_user_count }
@ReferalFeatures.getBonusFeatures @user_id, @callback
it "should get the users number of refered user", ->
@User.findOne
.calledWith(_id: @user_id)
.should.equal true
it "should call the callback with no features", ->
@callback.calledWith(null, {}).should.equal true

View file

@ -0,0 +1,173 @@
SandboxedModule = require('sandboxed-module')
should = require('chai').should()
expect = require('chai').expect
sinon = require 'sinon'
modulePath = "../../../../app/js/Features/Subscription/FeaturesUpdater"
assert = require("chai").assert
ObjectId = require('mongoose').Types.ObjectId
describe "FeaturesUpdater", ->
beforeEach ->
@user_id = ObjectId().toString()
@FeaturesUpdater = SandboxedModule.require modulePath, requires:
'./UserFeaturesUpdater': @UserFeaturesUpdater = {}
'./SubscriptionLocator': @SubscriptionLocator = {}
'./PlansLocator': @PlansLocator = {}
"logger-sharelatex": log:->
'settings-sharelatex': @Settings = {}
"../Referal/ReferalFeatures" : @ReferalFeatures = {}
"./V1SubscriptionManager": @V1SubscriptionManager = {}
describe "refreshFeatures", ->
beforeEach ->
@UserFeaturesUpdater.updateFeatures = sinon.stub().yields()
@FeaturesUpdater._getIndividualFeatures = sinon.stub().yields(null, { 'individual': 'features' })
@FeaturesUpdater._getGroupFeatureSets = sinon.stub().yields(null, [{ 'group': 'features' }, { 'group': 'features2' }])
@FeaturesUpdater._getV1Features = sinon.stub().yields(null, { 'v1': 'features' })
@ReferalFeatures.getBonusFeatures = sinon.stub().yields(null, { 'bonus': 'features' })
@FeaturesUpdater._mergeFeatures = sinon.stub().returns({'merged': 'features'})
@callback = sinon.stub()
@FeaturesUpdater.refreshFeatures @user_id, @callback
it "should get the individual features", ->
@FeaturesUpdater._getIndividualFeatures
.calledWith(@user_id)
.should.equal true
it "should get the group features", ->
@FeaturesUpdater._getGroupFeatureSets
.calledWith(@user_id)
.should.equal true
it "should get the v1 features", ->
@FeaturesUpdater._getV1Features
.calledWith(@user_id)
.should.equal true
it "should get the bonus features", ->
@ReferalFeatures.getBonusFeatures
.calledWith(@user_id)
.should.equal true
it "should merge from the default features", ->
@FeaturesUpdater._mergeFeatures.calledWith(@Settings.defaultFeatures).should.equal true
it "should merge the individual features", ->
@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'individual': 'features' }).should.equal true
it "should merge the group features", ->
@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features' }).should.equal true
@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features2' }).should.equal true
it "should merge the v1 features", ->
@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'v1': 'features' }).should.equal true
it "should merge the bonus features", ->
@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'bonus': 'features' }).should.equal true
it "should update the user with the merged features", ->
@UserFeaturesUpdater.updateFeatures
.calledWith(@user_id, {'merged': 'features'})
.should.equal true
describe "_mergeFeatures", ->
it "should prefer priority over standard for compileGroup", ->
expect(@FeaturesUpdater._mergeFeatures({
compileGroup: 'priority'
}, {
compileGroup: 'standard'
})).to.deep.equal({
compileGroup: 'priority'
})
expect(@FeaturesUpdater._mergeFeatures({
compileGroup: 'standard'
}, {
compileGroup: 'priority'
})).to.deep.equal({
compileGroup: 'priority'
})
expect(@FeaturesUpdater._mergeFeatures({
compileGroup: 'priority'
}, {
compileGroup: 'priority'
})).to.deep.equal({
compileGroup: 'priority'
})
expect(@FeaturesUpdater._mergeFeatures({
compileGroup: 'standard'
}, {
compileGroup: 'standard'
})).to.deep.equal({
compileGroup: 'standard'
})
it "should prefer -1 over any other for collaborators", ->
expect(@FeaturesUpdater._mergeFeatures({
collaborators: -1
}, {
collaborators: 10
})).to.deep.equal({
collaborators: -1
})
expect(@FeaturesUpdater._mergeFeatures({
collaborators: 10
}, {
collaborators: -1
})).to.deep.equal({
collaborators: -1
})
expect(@FeaturesUpdater._mergeFeatures({
collaborators: 4
}, {
collaborators: 10
})).to.deep.equal({
collaborators: 10
})
it "should prefer the higher of compileTimeout", ->
expect(@FeaturesUpdater._mergeFeatures({
compileTimeout: 20
}, {
compileTimeout: 10
})).to.deep.equal({
compileTimeout: 20
})
expect(@FeaturesUpdater._mergeFeatures({
compileTimeout: 10
}, {
compileTimeout: 20
})).to.deep.equal({
compileTimeout: 20
})
it "should prefer the true over false for other keys", ->
expect(@FeaturesUpdater._mergeFeatures({
github: true
}, {
github: false
})).to.deep.equal({
github: true
})
expect(@FeaturesUpdater._mergeFeatures({
github: false
}, {
github: true
})).to.deep.equal({
github: true
})
expect(@FeaturesUpdater._mergeFeatures({
github: true
}, {
github: true
})).to.deep.equal({
github: true
})
expect(@FeaturesUpdater._mergeFeatures({
github: false
}, {
github: false
})).to.deep.equal({
github: false
})

View file

@ -75,6 +75,7 @@ describe "SubscriptionController", ->
"./SubscriptionDomainHandler":@SubscriptionDomainHandler
"../User/UserGetter": @UserGetter
"./RecurlyWrapper": @RecurlyWrapper = {}
"./FeaturesUpdater": @FeaturesUpdater = {}
@res = new MockResponse()

View file

@ -1,5 +1,6 @@
SandboxedModule = require('sandboxed-module')
should = require('chai').should()
expect = require('chai').expect
sinon = require 'sinon'
modulePath = "../../../../app/js/Features/Subscription/SubscriptionUpdater"
assert = require("chai").assert
@ -22,6 +23,7 @@ describe "SubscriptionUpdater", ->
save: sinon.stub().callsArgWith(0)
freeTrial:{}
planCode:"student_or_something"
@user_id = @adminuser_id
@groupSubscription =
admin_id: @adminUser._id
@ -48,15 +50,15 @@ describe "SubscriptionUpdater", ->
@Settings =
freeTrialPlanCode: "collaborator"
defaultPlanCode: "personal"
defaultFeatures: { "default": "features" }
@UserFeaturesUpdater =
updateFeatures : sinon.stub().callsArgWith(2)
updateFeatures : sinon.stub().yields()
@PlansLocator =
findLocalPlanInSettings: sinon.stub().returns({})
@ReferalAllocator = assignBonus:sinon.stub().callsArgWith(1)
@ReferalAllocator.cock = true
@ReferalFeatures = getBonusFeatures: sinon.stub().callsArgWith(1)
@Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, null)}}
@SubscriptionUpdater = SandboxedModule.require modulePath, requires:
'../../models/Subscription': Subscription:@SubscriptionModel
@ -65,8 +67,7 @@ describe "SubscriptionUpdater", ->
'./PlansLocator': @PlansLocator
"logger-sharelatex": log:->
'settings-sharelatex': @Settings
"../Referal/ReferalAllocator" : @ReferalAllocator
'../../infrastructure/Modules': @Modules
"./FeaturesUpdater": @FeaturesUpdater = {}
describe "syncSubscription", ->
@ -97,7 +98,7 @@ describe "SubscriptionUpdater", ->
describe "_updateSubscriptionFromRecurly", ->
beforeEach ->
@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub().callsArgWith(1)
@FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1)
it "should update the subscription with token etc when not expired", (done)->
@SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=>
@ -108,7 +109,7 @@ describe "SubscriptionUpdater", ->
assert.equal(@subscription.freeTrial.expiresAt, undefined)
assert.equal(@subscription.freeTrial.planCode, undefined)
@subscription.save.called.should.equal true
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true
@FeaturesUpdater.refreshFeatures.calledWith(@adminUser._id).should.equal true
done()
it "should remove the recurlySubscription_id when expired", (done)->
@ -117,15 +118,15 @@ describe "SubscriptionUpdater", ->
@SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=>
assert.equal(@subscription.recurlySubscription_id, undefined)
@subscription.save.called.should.equal true
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true
@FeaturesUpdater.refreshFeatures.calledWith(@adminUser._id).should.equal true
done()
it "should update all the users features", (done)->
@SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=>
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@allUserIds[0]).should.equal true
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@allUserIds[1]).should.equal true
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@allUserIds[2]).should.equal true
@FeaturesUpdater.refreshFeatures.calledWith(@adminUser._id).should.equal true
@FeaturesUpdater.refreshFeatures.calledWith(@allUserIds[0]).should.equal true
@FeaturesUpdater.refreshFeatures.calledWith(@allUserIds[1]).should.equal true
@FeaturesUpdater.refreshFeatures.calledWith(@allUserIds[2]).should.equal true
done()
it "should set group to true and save how many members can be added to group", (done)->
@ -152,6 +153,9 @@ describe "SubscriptionUpdater", ->
done()
describe "addUserToGroup", ->
beforeEach ->
@FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1)
it "should add the users id to the group as a set", (done)->
@SubscriptionUpdater.addUserToGroup @adminUser._id, @otherUserId, =>
searchOps =
@ -163,12 +167,12 @@ describe "SubscriptionUpdater", ->
it "should update the users features", (done)->
@SubscriptionUpdater.addUserToGroup @adminUser._id, @otherUserId, =>
@UserFeaturesUpdater.updateFeatures.calledWith(@otherUserId, @subscription.planCode).should.equal true
@FeaturesUpdater.refreshFeatures.calledWith(@otherUserId).should.equal true
done()
describe "removeUserFromGroup", ->
beforeEach ->
@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub().callsArgWith(1)
@FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1)
it "should pull the users id from the group", (done)->
@SubscriptionUpdater.removeUserFromGroup @adminUser._id, @otherUserId, =>
@ -181,69 +185,7 @@ describe "SubscriptionUpdater", ->
it "should update the users features", (done)->
@SubscriptionUpdater.removeUserFromGroup @adminUser._id, @otherUserId, =>
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@otherUserId).should.equal true
done()
describe "_setUsersMinimumFeatures", ->
it "should call updateFeatures with the subscription if set", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
assert.equal args[1], @subscription.planCode
done()
it "should call updateFeatures with the group subscription if set", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription)
@SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
assert.equal args[1], @groupSubscription.planCode
done()
it "should call updateFeatures with the overleaf subscription if set", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, null)
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, ['ol_pro'])
@SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
assert.equal args[1], 'ol_pro'
done()
it "should call not call updateFeatures with users subscription if the subscription plan code is the default one (downgraded)", (done)->
@subscription.planCode = @Settings.defaultPlanCode
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription)
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
assert.equal args[1], @groupSubscription.planCode
done()
it "should call updateFeatures with default if there are no subscriptions for user", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null)
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
assert.equal args[1], @Settings.defaultPlanCode
done()
it "should call assignBonus", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
@ReferalAllocator.assignBonus.calledWith(@adminuser_id).should.equal true
@FeaturesUpdater.refreshFeatures.calledWith(@otherUserId).should.equal true
done()
describe "deleteSubscription", ->
@ -255,7 +197,7 @@ describe "SubscriptionUpdater", ->
member_ids: [ ObjectId(), ObjectId(), ObjectId() ]
}
@SubscriptionLocator.getSubscription = sinon.stub().yields(null, @subscription)
@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub().yields()
@FeaturesUpdater.refreshFeatures = sinon.stub().yields()
@SubscriptionUpdater.deleteSubscription @subscription_id, done
it "should look up the subscription", ->
@ -269,22 +211,12 @@ describe "SubscriptionUpdater", ->
.should.equal true
it "should downgrade the admin_id", ->
@SubscriptionUpdater._setUsersMinimumFeatures
@FeaturesUpdater.refreshFeatures
.calledWith(@subscription.admin_id)
.should.equal true
it "should downgrade all of the members", ->
for user_id in @subscription.member_ids
@SubscriptionUpdater._setUsersMinimumFeatures
@FeaturesUpdater.refreshFeatures
.calledWith(user_id)
.should.equal true
describe 'refreshSubscription', ->
beforeEach ->
@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub()
.callsArgWith(1, null)
it 'should call to _setUsersMinimumFeatures', ->
@SubscriptionUpdater.refreshSubscription(@adminUser._id, ()->)
@SubscriptionUpdater._setUsersMinimumFeatures.callCount.should.equal 1
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true

View file

@ -4,31 +4,20 @@ sinon = require 'sinon'
modulePath = "../../../../app/js/Features/Subscription/UserFeaturesUpdater"
assert = require("chai").assert
describe "UserFeaturesUpdater", ->
beforeEach ->
@User =
update: sinon.stub().callsArgWith(2)
@PlansLocator =
findLocalPlanInSettings : sinon.stub()
@UserFeaturesUpdater = SandboxedModule.require modulePath, requires:
'../../models/User': User:@User
"logger-sharelatex": log:->
'./PlansLocator': @PlansLocator
describe "updateFeatures", ->
it "should send the users features", (done)->
user_id = "5208dd34438842e2db000005"
plan_code = "student"
@features = features:{versioning:true, collaborators:10}
@PlansLocator.findLocalPlanInSettings = sinon.stub().returns(@features)
@UserFeaturesUpdater.updateFeatures user_id, plan_code, (err, features)=>
@features = {versioning:true, collaborators:10}
@UserFeaturesUpdater.updateFeatures user_id, @features, (err, features)=>
update = {"features.versioning":true, "features.collaborators":10}
@User.update.calledWith({"_id":user_id}, update).should.equal true
features.should.deep.equal @features.features
features.should.deep.equal @features
done()

View file

@ -0,0 +1,128 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
modulePath = path.join __dirname, '../../../../app/js/Features/Subscription/V1SubscriptionManager'
sinon = require("sinon")
expect = require("chai").expect
describe 'V1SubscriptionManager', ->
beforeEach ->
@V1SubscriptionManager = SandboxedModule.require modulePath, requires:
"../User/UserGetter": @UserGetter = {}
"logger-sharelatex":
log: sinon.stub()
err: sinon.stub()
warn: sinon.stub()
"settings-sharelatex":
overleaf:
host: @host = "http://overleaf.example.com"
"request": @request = sinon.stub()
@V1SubscriptionManager._v1PlanRequest = sinon.stub()
@userId = 'abcd'
@v1UserId = 42
@user =
_id: @userId
email: 'user@example.com'
overleaf:
id: @v1UserId
describe 'getPlanCodeFromV1', ->
beforeEach ->
@responseBody =
id: 32,
plan_name: 'pro'
@UserGetter.getUser = sinon.stub()
.yields(null, @user)
@V1SubscriptionManager._v1PlanRequest = sinon.stub()
.yields(null, @responseBody)
@call = (cb) =>
@V1SubscriptionManager.getPlanCodeFromV1 @userId, cb
describe 'when all goes well', ->
it 'should call getUser', (done) ->
@call (err, planCode) =>
expect(
@UserGetter.getUser.callCount
).to.equal 1
expect(
@UserGetter.getUser.calledWith(@userId)
).to.equal true
done()
it 'should call _v1PlanRequest', (done) ->
@call (err, planCode) =>
expect(
@V1SubscriptionManager._v1PlanRequest.callCount
).to.equal 1
expect(
@V1SubscriptionManager._v1PlanRequest.calledWith(
@v1UserId
)
).to.equal true
done()
it 'should produce a plan-code without error', (done) ->
@call (err, planCode) =>
expect(err).to.not.exist
expect(planCode).to.equal 'v1_pro'
done()
describe 'when the plan_name from v1 is null', ->
beforeEach ->
@responseBody.plan_name = null
it 'should produce a null plan-code without error', (done) ->
@call (err, planCode) =>
expect(err).to.not.exist
expect(planCode).to.equal null
done()
describe 'when getUser produces an error', ->
beforeEach ->
@UserGetter.getUser = sinon.stub()
.yields(new Error('woops'))
it 'should not call _v1PlanRequest', (done) ->
@call (err, planCode) =>
expect(
@V1SubscriptionManager._v1PlanRequest.callCount
).to.equal 0
done()
it 'should produce an error', (done) ->
@call (err, planCode) =>
expect(err).to.exist
expect(planCode).to.not.exist
done()
describe 'when getUser does not find a user', ->
beforeEach ->
@UserGetter.getUser = sinon.stub()
.yields(null, null)
it 'should not call _v1PlanRequest', (done) ->
@call (err, planCode) =>
expect(
@V1SubscriptionManager._v1PlanRequest.callCount
).to.equal 0
done()
it 'should produce a null plan-code, without error', (done) ->
@call (err, planCode) =>
expect(err).to.not.exist
expect(planCode).to.not.exist
done()
describe 'when the request to v1 fails', ->
beforeEach ->
@V1SubscriptionManager._v1PlanRequest = sinon.stub()
.yields(new Error('woops'))
it 'should produce an error', (done) ->
@call (err, planCode) =>
expect(err).to.exist
expect(planCode).to.not.exist
done()

View file

@ -39,7 +39,7 @@ describe "FileTypeManager", ->
beforeEach ->
@stat = { size: 100 }
@fs.stat = sinon.stub().callsArgWith(1, null, @stat)
it "should return .tex files as not binary", ->
@FileTypeManager.isBinary "file.tex", "/path/on/disk", (error, binary) ->
binary.should.equal false
@ -80,10 +80,18 @@ describe "FileTypeManager", ->
@FileTypeManager.isBinary "tex", "/path/on/disk", (error, binary) ->
binary.should.equal true
it "should return .latexmkrc file as not binary", ->
@FileTypeManager.isBinary ".latexmkrc", "/path/on/disk", (error, binary) ->
binary.should.equal false
it "should return latexmkrc file as not binary", ->
@FileTypeManager.isBinary "latexmkrc", "/path/on/disk", (error, binary) ->
binary.should.equal false
it "should ignore the case of an extension", ->
@FileTypeManager.isBinary "file.TEX", "/path/on/disk", (error, binary) ->
binary.should.equal false
it "should return large text files as binary", ->
@stat.size = 2 * 1024 * 1024 # 2Mb
@FileTypeManager.isBinary "file.tex", "/path/on/disk", (error, binary) ->
@ -98,6 +106,10 @@ describe "FileTypeManager", ->
@FileTypeManager.shouldIgnore "path/.git", (error, ignore) ->
ignore.should.equal true
it "should not ignore .latexmkrc dotfile", ->
@FileTypeManager.shouldIgnore "path/.latexmkrc", (error, ignore) ->
ignore.should.equal false
it "should ignore __MACOSX", ->
@FileTypeManager.shouldIgnore "path/__MACOSX", (error, ignore) ->
ignore.should.equal true
@ -109,5 +121,3 @@ describe "FileTypeManager", ->
it "should ignore the case of the extension", ->
@FileTypeManager.shouldIgnore "file.AUX", (error, ignore) ->
ignore.should.equal true

View file

@ -0,0 +1,46 @@
define [
'ide/editor/directives/aceEditor/spell-check/SpellCheckManager'
], (SpellCheckManager) ->
describe 'SpellCheckManager', ->
beforeEach (done) ->
@timelord = sinon.useFakeTimers()
window.user = { id: 1 }
window.csrfToken = 'token'
@scope = {
$watch: sinon.stub()
spellCheck: true
spellCheckLanguage: 'en'
}
@highlightedWordManager = {
reset: sinon.stub()
clearRow: sinon.stub()
addHighlight: sinon.stub()
}
@adapter = {
getLines: sinon.stub()
highlightedWordManager: @highlightedWordManager
}
inject ($q, $http, $httpBackend, $cacheFactory) =>
@$http = $http
@$q = $q
@$httpBackend = $httpBackend
cache = $cacheFactory('spellCheckTest', {capacity: 1000})
@spellCheckManager = new SpellCheckManager(@scope, cache, $http, $q, @adapter)
done()
afterEach ->
@timelord.restore()
it 'runs a full check soon after init', () ->
@$httpBackend.when('POST', '/spelling/check').respond({
misspellings: [{
index: 0
suggestions: ['opposition']
}]
})
@adapter.getLines.returns(['oppozition'])
@spellCheckManager.init()
@timelord.tick(200)
@$httpBackend.flush()
expect(@highlightedWordManager.addHighlight).to.have.been.called

View file

@ -1,7 +1,9 @@
const fs = require('fs')
const path = require('path')
const webpack = require('webpack')
const MODULES_PATH = path.join(__dirname, '/modules')
const webpackENV = process.env.WEBPACK_ENV || 'development'
// Generate a hash of entry points, including modules
const entryPoints = {}
@ -80,6 +82,15 @@ module.exports = {
jquery: path.join(__dirname, 'node_modules/jquery/dist/jquery'),
}
},
// TODO
// plugins: {}
plugins: [
new webpack.DefinePlugin({
// Swaps out checks for NODE_ENV with the env. This is used by various
// libs to enable dev-only features. These checks then become something
// like `if ('production' == 'production')`. Minification will then strip
// the dev-only code from the bundle
'process.env': {
NODE_ENV: JSON.stringify(webpackENV)
},
})
]
}