This commit is contained in:
Henry Oswald 2015-11-10 10:36:06 +00:00
commit c8ba7b72ab
38 changed files with 2119 additions and 292 deletions

View file

@ -1,6 +1,10 @@
ProjectGetter = require "../Project/ProjectGetter"
CollaboratorsHandler = require "./CollaboratorsHandler"
EditorController = require "../Editor/EditorController"
ProjectEditorHandler = require "../Project/ProjectEditorHandler"
EditorRealTimeController = require "../Editor/EditorRealTimeController"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
mimelib = require("mimelib")
module.exports = CollaboratorsController =
getCollaborators: (req, res, next = (error) ->) ->
@ -11,28 +15,49 @@ module.exports = CollaboratorsController =
CollaboratorsController._formatCollaborators project, (error, collaborators) ->
return next(error) if error?
res.send(JSON.stringify(collaborators))
removeSelfFromProject: (req, res, next = (error) ->) ->
user_id = req.session?.user?._id
if !user_id?
return next(new Error("User should be logged in"))
CollaboratorsHandler.removeUserFromProject req.params.project_id, user_id, (error) ->
return next(error) if error?
res.sendStatus 204
addUserToProject: (req, res, next) ->
project_id = req.params.Project_id
{email, privileges} = req.body
EditorController.addUserToProject project_id, email, privileges, (error, user) ->
LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) =>
return next(error) if error?
res.json user: user
if !allowed
return res.json { user: false }
else
{email, privileges} = req.body
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
if !email? or email == ""
return res.status(400).send("invalid email address")
adding_user_id = req.session?.user?._id
CollaboratorsHandler.addEmailToProject project_id, adding_user_id, email, privileges, (error, user_id) =>
return next(error) if error?
UserGetter.getUser user_id, (error, raw_user) ->
return next(error) if error?
user = ProjectEditorHandler.buildUserModelView(raw_user, privileges)
EditorRealTimeController.emitToRoom(project_id, 'userAddedToProject', user, privileges)
return res.json { user: user }
removeUserFromProject: (req, res, next) ->
project_id = req.params.Project_id
user_id = req.params.user_id
EditorController.removeUserFromProject project_id, user_id, (error)->
CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) ->
return next(error) if error?
res.sendStatus 204
removeSelfFromProject: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
user_id = req.session?.user?._id
CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) ->
return next(error) if error?
res.sendStatus 204
_removeUserIdFromProject: (project_id, user_id, callback = (error) ->) ->
CollaboratorsHandler.removeUserFromProject project_id, user_id, (error)->
return callback(error) if error?
EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
callback()
_formatCollaborators: (project, callback = (error, collaborators) ->) ->
collaborators = []

View file

@ -0,0 +1,25 @@
Project = require("../../models/Project").Project
EmailHandler = require("../Email/EmailHandler")
Settings = require "settings-sharelatex"
module.exports =
notifyUserOfProjectShare: (project_id, email, callback)->
Project
.findOne(_id: project_id )
.select("name owner_ref")
.populate('owner_ref')
.exec (err, project)->
emailOptions =
to: email
replyTo: project.owner_ref.email
project:
name: project.name
url: "#{Settings.siteUrl}/project/#{project._id}?" + [
"project_name=#{encodeURIComponent(project.name)}"
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
"new_email=#{encodeURIComponent(email)}"
"r=#{project.owner_ref.referal_id}" # Referal
"rs=ci" # referral source = collaborator invite
].join("&")
owner: project.owner_ref
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback

View file

@ -1,78 +1,63 @@
User = require('../../models/User').User
UserCreator = require('../User/UserCreator')
Project = require("../../models/Project").Project
Settings = require('settings-sharelatex')
EmailHandler = require("../Email/EmailHandler")
ProjectEntityHandler = require("../Project/ProjectEntityHandler")
mimelib = require("mimelib")
logger = require('logger-sharelatex')
async = require("async")
UserGetter = require "../User/UserGetter"
ContactManager = require "../Contacts/ContactManager"
CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
module.exports =
removeUserFromProject: (project_id, user_id, callback = ->)->
module.exports = CollaboratorsHandler =
removeUserFromProject: (project_id, user_id, callback = (error) ->)->
logger.log user_id: user_id, project_id: project_id, "removing user"
conditions = _id:project_id
update = $pull:{}
update["$pull"] = collaberator_refs:user_id, readOnly_refs:user_id
Project.update conditions, update, (err)->
if err?
logger.err err: err, "problem removing user from project collaberators"
logger.error err: err, "problem removing user from project collaberators"
callback(err)
addUserToProject: (project_id, email, privilegeLevel, callback)->
emails = mimelib.parseAddresses(email)
addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
emails = mimelib.parseAddresses(unparsed_email)
email = emails[0]?.address?.toLowerCase()
return callback(new Error("no valid email provided")) if !email?
self = @
User.findOne {'email':email}, (err, user)->
async.waterfall [
(cb)->
if user?
return cb(null, user)
else
self._createHoldingAccount email, cb
(@user, cb)=>
self._updateProjectWithUserPrivileges project_id, user, privilegeLevel, cb
(cb)->
self._notifyUserViaEmail project_id, email, cb
], (err)=>
callback(err, @user)
_createHoldingAccount: (email, callback)->
user = new User 'email':email, holdingAccount:true
user.save (err)->
callback(err, user)
_updateProjectWithUserPrivileges: (project_id, user, privilegeLevel, callback)->
if privilegeLevel == 'readAndWrite'
level = {"collaberator_refs":user}
logger.log privileges: "readAndWrite", user: user, project_id: project_id, "adding user"
else if privilegeLevel == 'readOnly'
level = {"readOnly_refs":user}
logger.log privileges: "readOnly", user: user, project_id: project_id, "adding user"
Project.update {_id: project_id}, {$push:level}, (err)->
callback(err)
_notifyUserViaEmail: (project_id, email, callback)->
Project.findOne(_id: project_id )
.select("name owner_ref")
.populate('owner_ref')
.exec (err, project)->
emailOptions =
to : email
replyTo : project.owner_ref.email
project:
name: project.name
url: "#{Settings.siteUrl}/project/#{project._id}?" + [
"project_name=#{encodeURIComponent(project.name)}"
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
"new_email=#{encodeURIComponent(email)}"
"r=#{project.owner_ref.referal_id}" # Referal
"rs=ci" # referral source = collaborator invite
].join("&")
owner: project.owner_ref
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
if !email? or email == ""
return callback(new Error("no valid email provided: '#{unparsed_email}'"))
UserCreator.getUserOrCreateHoldingAccount email, (error, user) ->
return callback(error) if error?
CollaboratorsHandler.addUserIdToProject project_id, adding_user_id, user._id, privilegeLevel, (error) ->
return callback(error) if error?
return callback null, user._id
addUserIdToProject: (project_id, adding_user_id, user_id, privilegeLevel, callback = (error) ->)->
Project.findOne { _id: project_id }, { collaberator_refs: 1, readOnly_refs: 1 }, (error, project) ->
return callback(error) if error?
existing_users = (project.collaberator_refs or [])
existing_users = existing_users.concat(project.readOnly_refs or [])
existing_users = existing_users.map (u) -> u.toString()
if existing_users.indexOf(user_id.toString()) > -1
return callback null # User already in Project
if privilegeLevel == 'readAndWrite'
level = {"collaberator_refs":user_id}
logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user"
else if privilegeLevel == 'readOnly'
level = {"readOnly_refs":user_id}
logger.log {privileges: "readOnly", user_id, project_id}, "adding user"
else
return callback(new Error("unknown privilegeLevel: #{privilegeLevel}"))
# Do these in the background
UserGetter.getUser user_id, {email: 1}, (error, user) ->
if error?
logger.error {err: error, project_id, user_id}, "error getting user while adding to project"
CollaboratorsEmailHandler.notifyUserOfProjectShare project_id, user.email
ContactManager.addContact adding_user_id, user_id
Project.update { _id: project_id }, { $addToSet: level }, (error) ->
return callback(error) if error?
# Flush to TPDS in background to add files to collaborator's Dropbox
ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (error) ->
if error?
logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator"
callback()

View file

@ -4,7 +4,7 @@ AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
apply: (webRouter, apiRouter) ->
webRouter.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
webRouter.post '/project/:Project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
apiRouter.get '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
webRouter.post '/project/:Project_id/users', SecurityManager.requestIsOwner, CollaboratorsController.addUserToProject

View file

@ -0,0 +1,43 @@
AuthenticationController = require "../Authentication/AuthenticationController"
ContactManager = require "./ContactManager"
UserGetter = require "../User/UserGetter"
logger = require "logger-sharelatex"
Modules = require "../../infrastructure/Modules"
module.exports = ContactsController =
getContacts: (req, res, next) ->
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
return next(error) if error?
ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
return next(error) if error?
UserGetter.getUsers contact_ids, {
email: 1, first_name: 1, last_name: 1, holdingAccount: 1
}, (error, contacts) ->
return next(error) if error?
# UserGetter.getUsers may not preserve order so put them back in order
positions = {}
for contact_id, i in contact_ids
positions[contact_id] = i
contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()]
# Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
contacts = contacts.filter (c) -> !c.holdingAccount
contacts = contacts.map(ContactsController._formatContact)
Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) ->
return next(error) if error?
contacts = contacts.concat(additional_contacts...)
res.send({
contacts: contacts
})
_formatContact: (contact) ->
return {
id: contact._id?.toString()
email: contact.email
first_name: contact.first_name
last_name: contact.last_name
type: "user"
}

View file

@ -0,0 +1,39 @@
request = require "request"
settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
module.exports = ContactManager =
getContactIds: (user_id, options = { limits: 50 }, callback = (error, contacts) ->) ->
logger.log {user_id}, "getting user contacts"
url = "#{settings.apis.contacts.url}/user/#{user_id}/contacts"
request.get {
url: url
qs: options
json: true
jar: false
}, (error, res, data) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null, data?.contact_ids or [])
else
error = new Error("contacts api responded with non-success code: #{res.statusCode}")
logger.error {err: error, user_id}, "error getting contacts for user"
callback(error)
addContact: (user_id, contact_id, callback = (error) ->) ->
logger.log {user_id, contact_id}, "add user contact"
url = "#{settings.apis.contacts.url}/user/#{user_id}/contacts"
request.post {
url: url
json: {
contact_id: contact_id
}
jar: false
}, (error, res, data) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null, data?.contact_ids or [])
else
error = new Error("contacts api responded with non-success code: #{res.statusCode}")
logger.error {err: error, user_id, contact_id}, "error adding contact for user"
callback(error)

View file

@ -0,0 +1,9 @@
AuthenticationController = require('../Authentication/AuthenticationController')
ContactController = require "./ContactController"
module.exports =
apply: (webRouter, apiRouter) ->
webRouter.get '/user/contacts',
AuthenticationController.requireLogin(),
ContactController.getContacts

View file

@ -1,48 +1,18 @@
logger = require('logger-sharelatex')
Metrics = require('../../infrastructure/Metrics')
sanitize = require('sanitizer')
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
ProjectEntityHandler = require('../Project/ProjectEntityHandler')
ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')
ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
ProjectDeleter = require("../Project/ProjectDeleter")
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
LimitationsManager = require("../Subscription/LimitationsManager")
EditorRealTimeController = require("./EditorRealTimeController")
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
Settings = require('settings-sharelatex')
async = require('async')
LockManager = require("../../infrastructure/LockManager")
_ = require('underscore')
redis = require("redis-sharelatex")
rclientPub = redis.createClient(Settings.redis.web)
rclientSub = redis.createClient(Settings.redis.web)
module.exports = EditorController =
addUserToProject: (project_id, email, privileges, callback = (error, collaborator_added)->)->
email = email.toLowerCase()
LimitationsManager.isCollaboratorLimitReached project_id, (error, limit_reached) =>
if error?
logger.error err:error, "error adding user to to project when checking if collaborator limit has been reached"
return callback(new Error("Something went wrong"))
if limit_reached
callback null, false
else
CollaboratorsHandler.addUserToProject project_id, email, privileges, (err, user)=>
return callback(err) if error?
# Flush to TPDS to add files to collaborator's Dropbox
ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, ->
EditorRealTimeController.emitToRoom(project_id, 'userAddedToProject', user, privileges)
callback null, ProjectEditorHandler.buildUserModelView(user, privileges)
removeUserFromProject: (project_id, user_id, callback = (error) ->)->
CollaboratorsHandler.removeUserFromProject project_id, user_id, (error) =>
return callback(error) if error?
EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
callback()
setDoc: (project_id, doc_id, docLines, source, callback = (err)->)->
DocumentUpdaterHandler.setDocument project_id, doc_id, docLines, source, (err)=>
logger.log project_id:project_id, doc_id:doc_id, "notifying users that the document has been updated"

View file

@ -243,7 +243,8 @@ module.exports = ProjectController =
anonymous: anonymous
languages: Settings.languages
themes: THEME_LIST
timer.done()
maxDocLength: Settings.max_doc_length
timer.done()
_buildProjectList: (ownedProjects, sharedProjects, readOnlyProjects)->
projects = []

View file

@ -19,15 +19,15 @@ module.exports =
return callback(error) if error?
callback null, (project.collaberator_refs.length + project.readOnly_refs.length)
isCollaboratorLimitReached: (project_id, callback = (error, limit_reached)->) ->
canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) ->
@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
return callback(error) if error?
@currentNumberOfCollaboratorsInProject project_id, (error, current_number) =>
return callback(error) if error?
if current_number < allowed_number or allowed_number < 0
callback null, false
else
if current_number + x_collaborators <= allowed_number or allowed_number < 0
callback null, true
else
callback null, false
userHasSubscriptionOrIsGroupMember: (user, callback = (err, hasSubscriptionOrIsMember)->) ->
@userHasSubscription user, (err, hasSubscription, subscription)=>

View file

@ -16,3 +16,11 @@ module.exports = UserGetter =
query = _id: query
db.users.findOne query, projection, callback
getUsers: (user_ids, projection, callback = (error, users) ->) ->
try
user_ids = user_ids.map (u) -> ObjectId(u.toString())
catch error
return callback error
db.users.find { _id: { $in: user_ids} }, projection, callback

View file

@ -146,7 +146,10 @@ module.exports = (app, webRouter, apiRouter)->
next()
webRouter.use (req, res, next) ->
res.locals.nav = Settings.nav
# Clone the nav settings so they can be modified for each request
res.locals.nav = {}
for key, value of Settings.nav
res.locals.nav[key] = _.clone(Settings.nav[key])
res.locals.templates = Settings.templateLinks
next()

View file

@ -1,6 +1,7 @@
fs = require "fs"
Path = require "path"
jade = require "jade"
async = require "async"
MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules")
@ -12,6 +13,7 @@ module.exports = Modules =
loadedModule = require(Path.join(MODULE_BASE_PATH, moduleName, "index"))
loadedModule.name = moduleName
@modules.push loadedModule
Modules.attachHooks()
applyRouter: (webRouter, apiRouter) ->
for module in @modules
@ -35,5 +37,25 @@ module.exports = Modules =
moduleIncludesAvailable: (view) ->
return (Modules.viewIncludes[view] or []).length > 0
attachHooks: () ->
for module in @modules
if module.hooks?
for hook, method of module.hooks
Modules.hooks.attach hook, method
hooks:
_hooks: {}
attach: (name, method) ->
@_hooks[name] ?= []
@_hooks[name].push method
fire: (name, args..., callback) ->
methods = @_hooks[name] or []
call_methods = methods.map (method) ->
return (cb) -> method(args..., cb)
async.series call_methods, (error, results) ->
return callback(error) if error?
return callback null, results
Modules.loadModules()

View file

@ -58,7 +58,8 @@ Modules.loadViewIncludes app
app.use bodyParser.urlencoded({ extended: true, limit: "2mb"})
app.use bodyParser.json({limit: "2mb"})
# Make sure we can process the max doc length plus some overhead for JSON encoding
app.use bodyParser.json({limit: Settings.max_doc_length + 16 * 1024}) # 16kb overhead
app.use multer(dest: Settings.path.uploadFolder)
app.use methodOverride()

View file

@ -36,6 +36,7 @@ Modules = require "./infrastructure/Modules"
RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
InactiveProjectController = require("./Features/InactiveData/InactiveProjectController")
ContactRouter = require("./Features/Contacts/ContactRouter")
logger = require("logger-sharelatex")
_ = require("underscore")
@ -65,6 +66,7 @@ module.exports = class Router
PasswordResetRouter.apply(webRouter, apiRouter)
StaticPagesRouter.apply(webRouter, apiRouter)
RealTimeProxyRouter.apply(webRouter, apiRouter)
ContactRouter.apply(webRouter, apiRouter)
Modules.applyRouter(webRouter, apiRouter)

View file

@ -90,6 +90,7 @@ block content
window.user = !{JSON.stringify(user).replace(/\//g, '\\/')};
window.csrfToken = "!{csrfToken}";
window.anonymous = #{anonymous};
window.maxDocLength = #{maxDocLength};
window.requirejs = {
"paths" : {
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML",

View file

@ -47,12 +47,22 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
form(ng-show="canAddCollaborators")
.small #{translate("share_with_your_collabs")}
.form-group
input.form-control(
type="text"
tags-input(
template="shareTagTemplate"
placeholder="joe@example.com, sue@example.com, ..."
ng-model="inputs.email"
ng-model="inputs.contacts"
focus-on="open"
display-property="display"
add-on-paste="true"
replace-spaces-with-dashes="false"
type="email"
)
auto-complete(
source="filterAutocompleteUsers($query)"
template="shareAutocompleteTemplate"
display-property="email"
min-length="0"
)
.form-group
.pull-right
select.privileges.form-control(
@ -62,9 +72,12 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
option(value="readAndWrite") #{translate("can_edit")}
option(value="readOnly") #{translate("read_only")}
| &nbsp;&nbsp;
//- We have to use mousedown here since click has issues with the
//- blur handler in tags-input sometimes changing its height and
//- moving this button, preventing the click registering.
button.btn.btn-info(
type="submit"
ng-click="addMembers()"
ng-mousedown="addMembers()"
) #{translate("share")}
div.text-center(ng-hide="canAddCollaborators")
p #{translate("need_to_upgrade_for_more_collabs")}.
@ -76,7 +89,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.modal-footer
.modal-footer-left
i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
span.text-danger.error(ng-show="state.error") {{ state.error }}
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
button.btn.btn-primary(
ng-click="done()"
) #{translate("done")}
@ -123,3 +136,25 @@ script(type="text/ng-template", id="makePrivateModalTemplate")
button.btn.btn-info(
ng-click="makePrivate()"
) #{translate("make_private")}
script(type="text/ng-template", id="shareTagTemplate")
.tag-template
span(ng-if="data.type")
i.fa.fa-fw(ng-class="{'fa-user': data.type != 'group', 'fa-group': data.type == 'group'}")
|
span {{$getDisplayText()}}
|
a(href, ng-click="$removeTag()").remove-button
i.fa.fa-fw.fa-close
script(type="text/ng-template", id="shareAutocompleteTemplate")
.autocomplete-template
div(ng-if="data.type == 'user'")
i.fa.fa-fw.fa-user
|
span(ng-bind-html="$highlight(data.display)")
div(ng-if="data.type == 'group'")
i.fa.fa-fw.fa-group
|
span(ng-bind-html="$highlight(data.name)")
span.subdued.small(ng-show="data.member_count") ({{ data.member_count }} members)

View file

@ -102,6 +102,8 @@ module.exports =
url: "http://localhost:8080/json"
realTime:
url: "http://localhost:3026"
contacts:
url: "http://localhost:3036"
templates:
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
@ -247,7 +249,10 @@ module.exports =
# Should we allow access to any page without logging in? This includes
# public projects, /learn, /templates, about pages, etc.
allowPublicAccess: false
# Maximum size of text documents in the real-time editing system.
max_doc_length: 2 * 1024 * 1024 # 2mb
# Internal configs
# ----------------
path:

View file

@ -16,6 +16,7 @@ define [
"mvdSixpack"
"ErrorCatcher"
"localStorage"
"ngTagsInput"
]).config (sixpackProvider)->
sixpackProvider.setOptions({
debug: false

View file

@ -23,6 +23,7 @@ define [], () ->
@userIsLeavingPage = false
window.addEventListener 'beforeunload', =>
@userIsLeavingPage = true
return # Don't return true or it will show a pop up
@connected = false
@userIsInactive = false

View file

@ -250,11 +250,9 @@ define [
v: version
_onError: (error, meta = {}) ->
meta.doc_id = @doc_id
console.error "ShareJS error", error, meta
ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{@ide.socket.socket.transport.name}" )
@ide.socket.disconnect()
meta.doc_id = @doc_id
@ide.reportError(error, meta)
@doc?.clearInflightAndPendingOps()
@_cleanUp()
@trigger "error", error
@trigger "error", error, meta

View file

@ -87,12 +87,20 @@ define [
callback null, new_sharejs_doc
_bindToDocumentEvents: (doc, sharejs_doc) ->
sharejs_doc.on "error", (error) =>
sharejs_doc.on "error", (error, meta) =>
if error?.message?.match "maxDocLength"
@ide.showGenericMessageModal(
"Document Too Long"
"Sorry, this file is too long to be edited manually. Please upload it directly."
)
else
@ide.socket.disconnect()
@ide.reportError(error, meta)
@ide.showGenericMessageModal(
"Out of sync"
"Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
)
@openDoc(doc, forceReopen: true)
@ide.showGenericMessageModal(
"Out of sync"
"Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
)
sharejs_doc.on "externalUpdate", (update) =>
return if @_ignoreExternalUpdates

View file

@ -45,6 +45,8 @@ define [
# ops as quickly as possible for low latency.
@_doc.setFlushDelay(0)
@trigger "remoteop"
@_doc.on "error", (e) =>
@_handleError(e)
@_bindToDocChanges(@_doc)
@ -98,7 +100,7 @@ define [
getInflightOp: () -> @_doc.inflightOp
getPendingOp: () -> @_doc.pendingOp
attachToAce: (ace) -> @_doc.attach_ace(ace)
attachToAce: (ace) -> @_doc.attach_ace(ace, false, window.maxDocLength)
detachFromAce: () -> @_doc.detach_ace?()
INFLIGHT_OP_TIMEOUT: 10000

View file

@ -40,7 +40,7 @@ applyToShareJS = (editorDoc, delta, doc) ->
# Attach an ace editor to the document. The editor's contents are replaced
# with the document's contents unless keepEditorContents is true. (In which case the document's
# contents are nuked and replaced with the editor's).
window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents) ->
window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength) ->
throw new Error 'Only text documents can be attached to ace' unless @provides['text']
doc = this
@ -74,6 +74,11 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents) ->
# Listen for edits in ace
editorListener = (change) ->
return if suppress
if maxDocLength? and editorDoc.getValue().length > maxDocLength
doc.emit "error", new Error("document length is greater than maxDocLength")
return
applyToShareJS editorDoc, change, doc
check()

View file

@ -11,6 +11,8 @@ define [
$scope.onDrop = (events, ui) ->
source = $(ui.draggable).scope().entity
return if !source?
# clear highlight explicitely
$('.file-tree-inner .droppable-hover').removeClass('droppable-hover')
ide.fileTreeManager.moveEntity(source, $scope.entity)
$scope.orderByFoldersFirst = (entity) ->

View file

@ -1,10 +1,10 @@
define [
"base"
], (App) ->
App.controller "ShareProjectModalController", ["$scope", "$modalInstance", "$timeout", "projectMembers", "$modal", ($scope, $modalInstance, $timeout, projectMembers, $modal) ->
App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, $modal, $http) ->
$scope.inputs = {
privileges: "readAndWrite"
email: ""
contacts: []
}
$scope.state = {
error: null
@ -22,35 +22,85 @@ define [
allowedNoOfMembers = $scope.project.features.collaborators
$scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS
$scope.autocompleteContacts = []
do loadAutocompleteUsers = () ->
$http.get "/user/contacts"
.success (data) ->
$scope.autocompleteContacts = data.contacts or []
for contact in $scope.autocompleteContacts
if contact.type == "user"
if contact.last_name == "" and contact.first_name = contact.email.split("@")[0]
# User has not set their proper name so use email as canonical display property
contact.display = contact.email
else
contact.name = "#{contact.first_name} #{contact.last_name}"
contact.display = "#{contact.name} <#{contact.email}>"
else
# Must be a group
contact.display = contact.name
getCurrentMemberEmails = () ->
$scope.project.members.map (u) -> u.email
$scope.filterAutocompleteUsers = ($query) ->
currentMemberEmails = getCurrentMemberEmails()
return $scope.autocompleteContacts.filter (contact) ->
if contact.email? and contact.email in currentMemberEmails
return false
for text in [contact.name, contact.email]
if text?.toLowerCase().indexOf($query.toLowerCase()) > -1
return true
return false
$scope.addMembers = () ->
return if !$scope.inputs.email? or $scope.inputs.email == ""
addMembers = () ->
return if $scope.inputs.contacts.length == 0
emails = $scope.inputs.email.split(/,\s*/)
$scope.inputs.email = ""
$scope.state.error = null
$scope.state.inflight = true
do addNextMember = () ->
if emails.length == 0 or !$scope.canAddCollaborators
$scope.state.inflight = false
$scope.$apply()
return
members = $scope.inputs.contacts
$scope.inputs.contacts = []
$scope.state.error = null
$scope.state.inflight = true
email = emails.shift()
projectMembers
.addMember(email, $scope.inputs.privileges)
.success (data) ->
if data?.user # data.user is false if collaborator limit is hit.
$scope.project.members.push data.user
currentMemberEmails = getCurrentMemberEmails()
do addNextMember = () ->
if members.length == 0 or !$scope.canAddCollaborators
$scope.state.inflight = false
$scope.$apply()
return
member = members.shift()
if !member.type? and member.display in currentMemberEmails
# Skip this existing member
return addNextMember()
if member.type == "user"
request = projectMembers.addMember(member.email, $scope.inputs.privileges)
else if member.type == "group"
request = projectMembers.addGroup(member.id, $scope.inputs.privileges)
else # Not an auto-complete object, so email == display
request = projectMembers.addMember(member.display, $scope.inputs.privileges)
request
.success (data) ->
if data.users?
users = data.users
else if data.user?
users = [data.user]
else
users = []
$scope.project.members.push users...
setTimeout () ->
# Give $scope a chance to update $scope.canAddCollaborators
# with new collaborator information.
addNextMember()
, 0
.error () ->
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong :("
.error () ->
$scope.state.inflight = false
$scope.state.error = true
$timeout addMembers, 50 # Give email list a chance to update
$scope.removeMember = (member) ->
$scope.state.error = null
@ -85,7 +135,6 @@ define [
$scope.cancel = () ->
$modalInstance.dismiss()
]
App.controller "MakePublicModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) ->
$scope.inputs = {

View file

@ -17,5 +17,13 @@ define [
privileges: privileges
_csrf: window.csrfToken
})
addGroup: (group_id, privileges) ->
$http.post("/project/#{ide.project_id}/group", {
group_id: group_id
privileges: privileges
_csrf: window.csrfToken
})
}
]

View file

@ -13,4 +13,5 @@ define [
"libs/passfield"
"libs/sixpack"
"libs/angular-sixpack"
"libs/ng-tags-input-3.0.0"
], () ->

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
.hover-container {
.show-on-hover {
display: none;
}
&:hover {
.show-on-hover {
display: initial;
}
}
}

View file

@ -0,0 +1,142 @@
tags-input {
display: block;
}
tags-input *, tags-input *:before, tags-input *:after {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
tags-input .host {
position: relative;
height: 100%;
}
tags-input .host:active {
outline: none;
}
tags-input .tags {
.form-control;
-moz-appearance: textfield;
-webkit-appearance: textfield;
padding: 2px 5px;
overflow: hidden;
word-wrap: break-word;
cursor: text;
background-color: #fff;
height: 100%;
}
tags-input .tags.focused {
outline: none;
-webkit-box-shadow: 0 0 3px 1px rgba(5, 139, 242, 0.6);
-moz-box-shadow: 0 0 3px 1px rgba(5, 139, 242, 0.6);
box-shadow: 0 0 3px 1px rgba(5, 139, 242, 0.6);
}
tags-input .tags .tag-list {
margin: 0;
padding: 0;
list-style-type: none;
}
tags-input .tags .tag-item {
margin: 2px;
padding: 0 7px;
display: inline-block;
float: left;
height: 26px;
line-height: 25px;
border: 1px solid @gray-light;
background-color: @gray-lightest;
border-radius: 3px;
}
tags-input .tags .tag-item.selected {
background-color: @gray-lighter;
}
tags-input .tags .tag-item .remove-button {
color: @gray-light;
text-decoration: none;
}
tags-input .tags .tag-item .remove-button:active {
color: @brand-primary;
}
tags-input .tags .input {
border: 0;
outline: none;
margin: 2px;
padding: 0;
padding-left: 5px;
float: left;
height: 26px;
}
tags-input .tags .input.invalid-tag {
color: @brand-danger;
}
tags-input .tags .input::-ms-clear {
display: none;
}
tags-input.ng-invalid .tags {
-webkit-box-shadow: 0 0 3px 1px rgba(255, 0, 0, 0.6);
-moz-box-shadow: 0 0 3px 1px rgba(255, 0, 0, 0.6);
box-shadow: 0 0 3px 1px rgba(255, 0, 0, 0.6);
}
tags-input[disabled] .host:focus {
outline: none;
}
tags-input[disabled] .tags {
background-color: #eee;
cursor: default;
}
tags-input[disabled] .tags .tag-item {
opacity: 0.65;
background: -webkit-linear-gradient(top, #f0f9ff 0%, rgba(203, 235, 255, 0.75) 47%, rgba(161, 219, 255, 0.62) 100%);
background: linear-gradient(to bottom, #f0f9ff 0%, rgba(203, 235, 255, 0.75) 47%, rgba(161, 219, 255, 0.62) 100%);
}
tags-input[disabled] .tags .tag-item .remove-button {
cursor: default;
}
tags-input[disabled] .tags .tag-item .remove-button:active {
color: @brand-primary;
}
tags-input[disabled] .tags .input {
background-color: #eee;
cursor: default;
}
tags-input .autocomplete {
margin-top: 5px;
position: absolute;
padding: 5px 0;
z-index: 999;
width: 100%;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
}
tags-input .autocomplete .suggestion-list {
margin: 0;
padding: 0;
list-style-type: none;
max-height: 280px;
overflow-y: auto;
position: relative;
}
tags-input .autocomplete .suggestion-item {
padding: 5px 10px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
tags-input .autocomplete .suggestion-item.selected {
color: white;
background-color: @brand-primary;
.subdued {
color: white;
}
}
tags-input .autocomplete .suggestion-item em {
font-weight: bold;
font-style: normal;
}
/*# sourceMappingURL=ng-tags-input.css.map */

View file

@ -40,6 +40,7 @@
// @import "components/wells.less";
@import "components/close.less";
@import "components/fineupload.less";
@import "components/hover.less";
// Components w/ JavaScript
@import "components/modals.less";
@ -47,6 +48,9 @@
@import "components/popovers.less";
@import "components/carousel.less";
// ngTagsInput
@import "components/tags-input.less";
// Utility classes
@import "core/utilities.less";
@import "core/responsive-utilities.less";

View file

@ -11,15 +11,17 @@ ObjectId = require("mongojs").ObjectId
describe "CollaboratorsController", ->
beforeEach ->
@CollaboratorsHandler =
removeUserFromProject:sinon.stub()
@CollaboratorsController = SandboxedModule.require modulePath, requires:
"../Project/ProjectGetter": @ProjectGetter = {}
"./CollaboratorsHandler": @CollaboratorsHandler
"../Editor/EditorController": @EditorController = {}
"./CollaboratorsHandler": @CollaboratorsHandler = {}
"../Editor/EditorRealTimeController": @EditorRealTimeController = {}
'../Subscription/LimitationsManager' : @LimitationsManager = {}
'../Project/ProjectEditorHandler' : @ProjectEditorHandler = {}
'../User/UserGetter': @UserGetter = {}
@res = new MockResponse()
@req = new MockRequest()
@project_id = "project-id-123"
@callback = sinon.stub()
describe "getCollaborators", ->
@ -51,40 +53,85 @@ describe "CollaboratorsController", ->
it "should return the formatted collaborators", ->
@res.body.should.equal JSON.stringify(@collaborators)
describe "removeSelfFromProject", ->
beforeEach ->
@req.session =
user: _id: @user_id = "user-id-123"
destroy:->
@req.params = project_id: @project_id
@CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2)
@CollaboratorsController.removeSelfFromProject(@req, @res)
it "should remove the logged in user from the project", ->
@CollaboratorsHandler.removeUserFromProject.calledWith(@project_id, @user_id)
it "should return a success code", ->
@res.statusCode.should.equal 204
describe "addUserToProject", ->
beforeEach ->
@req.params =
Project_id: @project_id = "project-id-123"
Project_id: @project_id
@req.body =
email: @email = "joe@example.com"
privileges: @privileges = "readAndWrite"
email: @email = "Joe@example.com"
privileges: @privileges = "readOnly"
@req.session =
user: _id: @adding_user_id = "adding-user-id"
@res.json = sinon.stub()
@EditorController.addUserToProject = sinon.stub().callsArgWith(3, null, @user = {"mock": "user"})
@CollaboratorsController.addUserToProject @req, @res
it "should add the user to the project", ->
@EditorController.addUserToProject
.calledWith(@project_id, @email, @privileges)
.should.equal true
it "should send the back the added user", ->
@res.json.calledWith(user: @user).should.equal true
@user_id = "mock-user-id"
@raw_user = {
_id: @user_id, email: "joe@example.com", first_name: "Joe", last_name: "Example", unused: "foo"
}
@user_view = {
id: @user_id, first_name: "Joe", last_name: "Example", email: "joe@example.com"
}
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@ProjectEditorHandler.buildUserModelView = sinon.stub().returns(@user_view)
@CollaboratorsHandler.addEmailToProject = sinon.stub().callsArgWith(4, null, @user_id)
@UserGetter.getUser = sinon.stub().callsArgWith(1, null, @user)
@EditorRealTimeController.emitToRoom = sinon.stub()
@callback = sinon.stub()
describe "when the project can accept more collaborators", ->
beforeEach ->
@CollaboratorsController.addUserToProject @req, @res, @next
it "should add the user to the project", ->
@CollaboratorsHandler.addEmailToProject
.calledWith(@project_id, @adding_user_id, @email.toLowerCase(), @privileges)
.should.equal true
it "should emit a userAddedToProject event", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "userAddedToProject", @user_view, @privileges)
.should.equal true
it "should send the user as the response body", ->
@res.json
.calledWith({
user: @user_view
})
.should.equal true
describe "when the project cannot accept more collaborators", ->
beforeEach ->
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
@CollaboratorsController.addUserToProject @req, @res, @next
it "should not add the user to the project", ->
@CollaboratorsHandler.addEmailToProject.called.should.equal false
it "should not emit a userAddedToProject event", ->
@EditorRealTimeController.emitToRoom.called.should.equal false
it "should send user: false as the response body", ->
@res.json
.calledWith({
user: false
})
.should.equal true
describe "when the email is not valid", ->
beforeEach ->
@req.body.email = "not-valid"
@res.status = sinon.stub().returns @res
@res.send = sinon.stub()
@CollaboratorsController.addUserToProject @req, @res, @next
it "should not add the user to the project", ->
@CollaboratorsHandler.addEmailToProject.called.should.equal false
it "should not emit a userAddedToProject event", ->
@EditorRealTimeController.emitToRoom.called.should.equal false
it "should return a 400 response", ->
@res.status.calledWith(400).should.equal true
@res.send.calledWith("invalid email address").should.equal true
describe "removeUserFromProject", ->
beforeEach ->
@ -92,17 +139,45 @@ describe "CollaboratorsController", ->
Project_id: @project_id = "project-id-123"
user_id: @user_id = "user-id-123"
@res.sendStatus = sinon.stub()
@EditorController.removeUserFromProject = sinon.stub().callsArg(2)
@EditorRealTimeController.emitToRoom = sinon.stub()
@CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2)
@CollaboratorsController.removeUserFromProject @req, @res
it "should from the user from the project", ->
@EditorController.removeUserFromProject
@CollaboratorsHandler.removeUserFromProject
.calledWith(@project_id, @user_id)
.should.equal true
it "should emit a userRemovedFromProject event to the proejct", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, 'userRemovedFromProject', @user_id)
.should.equal true
it "should send the back a success response", ->
@res.sendStatus.calledWith(204).should.equal true
describe "removeSelfFromProject", ->
beforeEach ->
@req.session =
user: _id: @user_id = "user-id-123"
@req.params = Project_id: @project_id
@res.sendStatus = sinon.stub()
@EditorRealTimeController.emitToRoom = sinon.stub()
@CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2)
@CollaboratorsController.removeSelfFromProject(@req, @res)
it "should remove the logged in user from the project", ->
@CollaboratorsHandler.removeUserFromProject
.calledWith(@project_id, @user_id)
.should.equal true
it "should emit a userRemovedFromProject event to the proejct", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, 'userRemovedFromProject', @user_id)
.should.equal true
it "should return a success code", ->
@res.sendStatus.calledWith(204).should.equal true
describe "_formatCollaborators", ->
beforeEach ->

View file

@ -7,42 +7,138 @@ modulePath = path.join __dirname, "../../../../app/js/Features/Collaborators/Col
expect = require("chai").expect
describe "CollaboratorsHandler", ->
beforeEach ->
@user =
email:"bob@bob.com"
@UserModel =
findById:sinon.stub().callsArgWith(1, null, @user)
update: sinon.stub()
@settings = {}
@ProjectModel =
update: sinon.stub().callsArgWith(1)
@CollaboratorHandler = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
"logger-sharelatex":
log:->
err:->
'../../models/User': User:@UserModel
"../../models/Project": Project:@ProjectModel
"../Email/EmailHandler": {}
"logger-sharelatex": @logger = { log: sinon.stub(), err: sinon.stub() }
'../User/UserCreator': @UserCreator = {}
'../User/UserGetter': @UserGetter = {}
"../Contacts/ContactManager": @ContactManager = {}
"../../models/Project": Project: @Project = {}
"../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
"./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {}
@project_id = "123l2j13lkj"
@user_id = "132kj1lk2j"
@project_id = "mock-project-id"
@user_id = "mock-user-id"
@adding_user_id = "adding-user-id"
@email = "joe@sharelatex.com"
@callback = sinon.stub()
describe "removeUserFromProject", ->
beforeEach ->
@ProjectModel.update.callsArgWith(2)
@Project.update = sinon.stub().callsArg(2)
@CollaboratorHandler.removeUserFromProject @project_id, @user_id, @callback
it "should remove the user from mongo", (done)->
@CollaboratorHandler.removeUserFromProject @project_id, @user_id, =>
update = @ProjectModel.update.args[0][1]
assert.deepEqual update, "$pull":{collaberator_refs:@user_id, readOnly_refs:@user_id}
done()
it "should remove the user from mongo", ->
@Project.update
.calledWith({
_id: @project_id
}, {
"$pull":{collaberator_refs:@user_id, readOnly_refs:@user_id}
})
.should.equal true
describe "addUserToProject", ->
beforeEach ->
@Project.update = sinon.stub().callsArg(2)
@Project.findOne = sinon.stub().callsArgWith(2, null, @project = {})
@ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().callsArg(1)
@CollaboratorHandler.addEmailToProject = sinon.stub().callsArgWith(4, null, @user_id)
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { _id: @user_id, email: @email })
@CollaboratorsEmailHandler.notifyUserOfProjectShare = sinon.stub()
@ContactManager.addContact = sinon.stub()
describe "as readOnly", ->
beforeEach ->
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readOnly", @callback
it "should add the user to the readOnly_refs", ->
@Project.update
.calledWith({
_id: @project_id
}, {
"$addToSet":{ readOnly_refs: @user_id }
})
.should.equal true
it "should flush the project to the TPDS", ->
@ProjectEntityHandler.flushProjectToThirdPartyDataStore
.calledWith(@project_id)
.should.equal true
it "should send an email to the shared-with user", ->
@CollaboratorsEmailHandler.notifyUserOfProjectShare
.calledWith(@project_id, @email)
.should.equal true
it "should add the user as a contact for the adding user", ->
@ContactManager.addContact
.calledWith(@adding_user_id, @user_id)
.should.equal true
describe "as readAndWrite", ->
beforeEach ->
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readAndWrite", @callback
it "should add the user to the collaberator_refs", ->
@Project.update
.calledWith({
_id: @project_id
}, {
"$addToSet":{ collaberator_refs: @user_id }
})
.should.equal true
it "should flush the project to the TPDS", ->
@ProjectEntityHandler.flushProjectToThirdPartyDataStore
.calledWith(@project_id)
.should.equal true
describe "with invalid privilegeLevel", ->
beforeEach ->
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "notValid", @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true
describe "when user already exists as a collaborator", ->
beforeEach ->
@project.collaberator_refs = [@user_id]
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readAndWrite", @callback
it "should not add the user again", ->
@Project.update.called.should.equal false
describe "addEmailToProject", ->
beforeEach ->
@UserCreator.getUserOrCreateHoldingAccount = sinon.stub().callsArgWith(1, null, @user = {_id: @user_id})
@CollaboratorHandler.addUserIdToProject = sinon.stub().callsArg(4)
describe "with a valid email", ->
beforeEach ->
@CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, (@email = "Joe@example.com"), (@privilegeLevel = "readAndWrite"), @callback
it "should get the user with the lowercased email", ->
@UserCreator.getUserOrCreateHoldingAccount
.calledWith(@email.toLowerCase())
.should.equal true
it "should add the user to the project by id", ->
@CollaboratorHandler.addUserIdToProject
.calledWith(@project_id, @adding_user_id, @user_id, @privilegeLevel)
.should.equal true
it "should return the callback with the user_id", ->
@callback.calledWith(null, @user_id).should.equal true
describe "with an invalid email", ->
beforeEach ->
@CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, "not-and-email", (@privilegeLevel = "readAndWrite"), @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true
it "should not add any users to the proejct", ->
@CollaboratorHandler.addUserIdToProject.called.should.equal false

View file

@ -0,0 +1,64 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
assert = chai.assert
expect = chai.expect
modulePath = "../../../../app/js/Features/Contacts/ContactController.js"
SandboxedModule = require('sandboxed-module')
describe "ContactController", ->
beforeEach ->
@ContactController = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"../User/UserGetter": @UserGetter = {}
"./ContactManager": @ContactManager = {}
"../Authentication/AuthenticationController": @AuthenticationController = {}
"../../infrastructure/Modules": @Modules = { hooks: {} }
@next = sinon.stub()
@req = {}
@res = {}
@res.status = sinon.stub().returns @req
@res.send = sinon.stub()
describe "getContacts", ->
beforeEach ->
@user_id = "mock-user-id"
@contact_ids = ["contact-1", "contact-2", "contact-3"]
@contacts = [
{ _id: "contact-1", email: "joe@example.com", first_name: "Joe", last_name: "Example", unsued: "foo" }
{ _id: "contact-2", email: "jane@example.com", first_name: "Jane", last_name: "Example", unsued: "foo", holdingAccount: true }
{ _id: "contact-3", email: "jim@example.com", first_name: "Jim", last_name: "Example", unsued: "foo" }
]
@AuthenticationController.getLoggedInUserId = sinon.stub().callsArgWith(1, null, @user_id)
@ContactManager.getContactIds = sinon.stub().callsArgWith(2, null, @contact_ids)
@UserGetter.getUsers = sinon.stub().callsArgWith(2, null, @contacts)
@Modules.hooks.fire = sinon.stub().callsArg(3)
@ContactController.getContacts @req, @res, @next
it "should look up the logged in user id", ->
@AuthenticationController.getLoggedInUserId
.calledWith(@req)
.should.equal true
it "should get the users contact ids", ->
@ContactManager.getContactIds
.calledWith(@user_id, { limit: 50 })
.should.equal true
it "should populate the users contacts ids", ->
@UserGetter.getUsers
.calledWith(@contact_ids, { email: 1, first_name: 1, last_name: 1, holdingAccount: 1 })
.should.equal true
it "should fire the getContact module hook", ->
@Modules.hooks.fire
.calledWith("getContacts", @user_id)
.should.equal true
it "should return a formatted list of contacts in contact list order, without holding accounts", ->
@res.send.args[0][0].contacts.should.deep.equal [
{ id: "contact-1", email: "joe@example.com", first_name: "Joe", last_name: "Example", type: "user" }
{ id: "contact-3", email: "jim@example.com", first_name: "Jim", last_name: "Example", type: "user" }
]

View file

@ -0,0 +1,91 @@
chai = require('chai')
chai.should()
sinon = require("sinon")
modulePath = "../../../../app/js/Features/Contacts/ContactManager"
SandboxedModule = require('sandboxed-module')
describe "ContactManager", ->
beforeEach ->
@ContactManager = SandboxedModule.require modulePath, requires:
"request" : @request = sinon.stub()
"settings-sharelatex": @settings =
apis:
contacts:
url: "contacts.sharelatex.com"
"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub(), err:->}
@user_id = "user-id-123"
@contact_id = "contact-id-123"
@callback = sinon.stub()
describe "getContacts", ->
describe "with a successful response code", ->
beforeEach ->
@request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, { contact_ids: @contact_ids = ["mock", "contact_ids"]})
@ContactManager.getContactIds @user_id, { limit: 42 }, @callback
it "should get the contacts from the contacts api", ->
@request.get
.calledWith({
url: "#{@settings.apis.contacts.url}/user/#{@user_id}/contacts"
qs: { limit: 42 }
json: true
jar: false
})
.should.equal true
it "should call the callback with the contatcs", ->
@callback.calledWith(null, @contact_ids).should.equal true
describe "with a failed response code", ->
beforeEach ->
@request.get = sinon.stub().callsArgWith(1, null, statusCode: 500, null)
@ContactManager.getContactIds @user_id, { limit: 42 }, @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error("contacts api responded with non-success code: 500")).should.equal true
it "should log the error", ->
@logger.error
.calledWith({
err: new Error("contacts api responded with a non-success code: 500")
user_id: @user_id
}, "error getting contacts for user")
.should.equal true
describe "addContact", ->
describe "with a successful response code", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 200, null)
@ContactManager.addContact @user_id, @contact_id, @callback
it "should add the contacts for the user in the contacts api", ->
@request.post
.calledWith({
url: "#{@settings.apis.contacts.url}/user/#{@user_id}/contacts"
json: {
contact_id: @contact_id
}
jar: false
})
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
describe "with a failed response code", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 500, null)
@ContactManager.addContact @user_id, @contact_id, @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error("contacts api responded with non-success code: 500")).should.equal true
it "should log the error", ->
@logger.error
.calledWith({
err: new Error("contacts api responded with a non-success code: 500")
user_id: @user_id
contact_id: @contact_id
}, "error adding contact for user")
.should.equal true

View file

@ -33,10 +33,8 @@ describe "EditorController", ->
setSpellCheckLanguage: sinon.spy()
@ProjectEntityHandler =
flushProjectToThirdPartyDataStore:sinon.stub()
@ProjectEditorHandler = {}
@Project =
findPopulatedById: sinon.stub().callsArgWith(1, null, @project)
@LimitationsManager = {}
@client = new MockClient()
@settings =
@ -56,14 +54,12 @@ describe "EditorController", ->
releaseLock : sinon.stub()
@EditorController = SandboxedModule.require modulePath, requires:
"../../infrastructure/Server" : io : @io
'../Project/ProjectEditorHandler' : @ProjectEditorHandler
'../Project/ProjectEntityHandler' : @ProjectEntityHandler
'../Project/ProjectOptionsHandler' : @ProjectOptionsHandler
'../Project/ProjectDetailsHandler': @ProjectDetailsHandler
'../Project/ProjectDeleter' : @ProjectDeleter
'../Collaborators/CollaboratorsHandler': @CollaboratorsHandler
'../DocumentUpdater/DocumentUpdaterHandler' : @DocumentUpdaterHandler
'../Subscription/LimitationsManager' : @LimitationsManager
'../../models/Project' : Project: @Project
"settings-sharelatex":@settings
'../Dropbox/DropboxProjectLinker':@dropboxProjectLinker
@ -76,67 +72,6 @@ describe "EditorController", ->
log: sinon.stub()
err: sinon.stub()
describe "addUserToProject", ->
beforeEach ->
@email = "Jane.Doe@example.com"
@priveleges = "readOnly"
@addedUser = { _id: "added-user" }
@ProjectEditorHandler.buildUserModelView = sinon.stub().returns(@addedUser)
@CollaboratorsHandler.addUserToProject = sinon.stub().callsArgWith(3, null, @addedUser)
@EditorRealTimeController.emitToRoom = sinon.stub()
@callback = sinon.stub()
describe "when the project can accept more collaborators", ->
beforeEach ->
@LimitationsManager.isCollaboratorLimitReached = sinon.stub().callsArgWith(1, null, false)
it "should add the user to the project", (done)->
@EditorController.addUserToProject @project_id, @email, @priveleges, =>
@CollaboratorsHandler.addUserToProject.calledWith(@project_id, @email.toLowerCase(), @priveleges).should.equal true
done()
it "should emit a userAddedToProject event", (done)->
@EditorController.addUserToProject @project_id, @email, @priveleges, =>
@EditorRealTimeController.emitToRoom.calledWith(@project_id, "userAddedToProject", @addedUser).should.equal true
done()
it "should return the user to the callback", (done)->
@EditorController.addUserToProject @project_id, @email, @priveleges, (err, result)=>
result.should.equal @addedUser
done()
describe "when the project cannot accept more collaborators", ->
beforeEach ->
@LimitationsManager.isCollaboratorLimitReached = sinon.stub().callsArgWith(1, null, true)
@EditorController.addUserToProject(@project_id, @email, @priveleges, @callback)
it "should not add the user to the project", ->
@CollaboratorsHandler.addUserToProject.called.should.equal false
it "should not emit a userAddedToProject event", ->
@EditorRealTimeController.emitToRoom.called.should.equal false
it "should return false to the callback", ->
@callback.calledWith(null, false).should.equal true
describe "removeUserFromProject", ->
beforeEach ->
@removed_user_id = "removed-user-id"
@CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArgWith(2)
@EditorRealTimeController.emitToRoom = sinon.stub()
@EditorController.removeUserFromProject(@project_id, @removed_user_id)
it "remove the user from the project", ->
@CollaboratorsHandler.removeUserFromProject
.calledWith(@project_id, @removed_user_id)
.should.equal true
it "should emit a userRemovedFromProject event", ->
@EditorRealTimeController.emitToRoom.calledWith(@project_id, "userRemovedFromProject", @removed_user_id).should.equal true
describe "updating compiler used for project", ->
it "should send the new compiler and project id to the project options handler", (done)->
compiler = "latex"

View file

@ -62,7 +62,7 @@ describe "LimitationsManager", ->
it "should return the total number of collaborators", ->
@callback.calledWith(null, 3).should.equal true
describe "isCollaboratorLimitReached", ->
describe "canAddXCollaborators", ->
beforeEach ->
sinon.stub @LimitationsManager,
"currentNumberOfCollaboratorsInProject",
@ -76,7 +76,16 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 1
@allowed_number = 2
@LimitationsManager.isCollaboratorLimitReached(@project_id, @callback)
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return true", ->
@callback.calledWith(null, true).should.equal true
describe "when the project has fewer collaborators than allowed but I want to add more than allowed", ->
beforeEach ->
@current_number = 1
@allowed_number = 2
@LimitationsManager.canAddXCollaborators(@project_id, 2, @callback)
it "should return false", ->
@callback.calledWith(null, false).should.equal true
@ -85,19 +94,19 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 3
@allowed_number = 2
@LimitationsManager.isCollaboratorLimitReached(@project_id, @callback)
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return true", ->
@callback.calledWith(null, true).should.equal true
it "should return false", ->
@callback.calledWith(null, false).should.equal true
describe "when the project has infinite collaborators", ->
beforeEach ->
@current_number = 100
@allowed_number = -1
@LimitationsManager.isCollaboratorLimitReached(@project_id, @callback)
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return false", ->
@callback.calledWith(null, false).should.equal true
it "should return true", ->
@callback.calledWith(null, true).should.equal true
describe "userHasSubscription", ->