mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' into node-6.9
This commit is contained in:
commit
621a07aff2
179 changed files with 5083 additions and 5069 deletions
|
@ -1,5 +1,6 @@
|
||||||
fs = require "fs"
|
fs = require "fs"
|
||||||
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
|
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
|
||||||
|
require('es6-promise').polyfill()
|
||||||
|
|
||||||
module.exports = (grunt) ->
|
module.exports = (grunt) ->
|
||||||
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||||
|
@ -18,6 +19,7 @@ module.exports = (grunt) ->
|
||||||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||||
grunt.loadNpmTasks 'grunt-parallel'
|
grunt.loadNpmTasks 'grunt-parallel'
|
||||||
grunt.loadNpmTasks 'grunt-exec'
|
grunt.loadNpmTasks 'grunt-exec'
|
||||||
|
grunt.loadNpmTasks 'grunt-postcss'
|
||||||
# grunt.loadNpmTasks 'grunt-contrib-imagemin'
|
# grunt.loadNpmTasks 'grunt-contrib-imagemin'
|
||||||
# grunt.loadNpmTasks 'grunt-sprity'
|
# grunt.loadNpmTasks 'grunt-sprity'
|
||||||
|
|
||||||
|
@ -136,8 +138,14 @@ module.exports = (grunt) ->
|
||||||
files:
|
files:
|
||||||
"public/stylesheets/style.css": "public/stylesheets/style.less"
|
"public/stylesheets/style.css": "public/stylesheets/style.less"
|
||||||
|
|
||||||
|
postcss:
|
||||||
|
options:
|
||||||
|
map: true,
|
||||||
|
processors: [
|
||||||
|
require('autoprefixer')({browsers: [ 'last 2 versions', 'ie >= 10' ]})
|
||||||
|
]
|
||||||
|
dist:
|
||||||
|
src: 'public/stylesheets/style.css'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
run:
|
run:
|
||||||
|
@ -222,11 +230,11 @@ module.exports = (grunt) ->
|
||||||
|
|
||||||
sed:
|
sed:
|
||||||
version:
|
version:
|
||||||
path: "app/views/sentry.jade"
|
path: "app/views/sentry.pug"
|
||||||
pattern: '@@COMMIT@@',
|
pattern: '@@COMMIT@@',
|
||||||
replacement: '<%= commit %>',
|
replacement: '<%= commit %>',
|
||||||
release:
|
release:
|
||||||
path: "app/views/sentry.jade"
|
path: "app/views/sentry.pug"
|
||||||
pattern: "@@RELEASE@@"
|
pattern: "@@RELEASE@@"
|
||||||
replacement: process.env.BUILD_NUMBER || "(unknown build)"
|
replacement: process.env.BUILD_NUMBER || "(unknown build)"
|
||||||
|
|
||||||
|
@ -366,7 +374,7 @@ module.exports = (grunt) ->
|
||||||
|
|
||||||
grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server']
|
grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server']
|
||||||
grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes']
|
grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes']
|
||||||
grunt.registerTask 'compile:css', 'Compile the less files to css', ['less']
|
grunt.registerTask 'compile:css', 'Compile the less files to css', ['less', 'postcss:dist']
|
||||||
grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",]
|
grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",]
|
||||||
grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
|
grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
|
||||||
grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests']
|
grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests']
|
||||||
|
@ -389,5 +397,5 @@ module.exports = (grunt) ->
|
||||||
|
|
||||||
grunt.registerTask 'default', 'run'
|
grunt.registerTask 'default', 'run'
|
||||||
|
|
||||||
grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
|
grunt.registerTask 'version', "Write the version number into sentry.pug", ['git-rev-parse', 'sed']
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,11 @@ module.exports =
|
||||||
if !settings?.apis?.analytics?.url? or !settings.apis.blog.url?
|
if !settings?.apis?.analytics?.url? or !settings.apis.blog.url?
|
||||||
return res.json []
|
return res.json []
|
||||||
|
|
||||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
user = AuthenticationController.getSessionUser(req)
|
||||||
logger.log {user_id}, "getting unread announcements"
|
logger.log {user_id:user?._id}, "getting unread announcements"
|
||||||
AnnouncementsHandler.getUnreadAnnouncements user_id, (err, announcements)->
|
AnnouncementsHandler.getUnreadAnnouncements user, (err, announcements)->
|
||||||
if err?
|
if err?
|
||||||
logger.err {err, user_id}, "unable to get unread announcements"
|
logger.err {err:err, user_id:user._id}, "unable to get unread announcements"
|
||||||
next(err)
|
next(err)
|
||||||
else
|
else
|
||||||
res.json announcements
|
res.json announcements
|
||||||
|
|
|
@ -1,24 +1,46 @@
|
||||||
AnalyticsManager = require("../Analytics/AnalyticsManager")
|
AnalyticsManager = require("../Analytics/AnalyticsManager")
|
||||||
BlogHandler = require("../Blog/BlogHandler")
|
BlogHandler = require("../Blog/BlogHandler")
|
||||||
async = require("async")
|
|
||||||
_ = require("lodash")
|
|
||||||
logger = require("logger-sharelatex")
|
logger = require("logger-sharelatex")
|
||||||
settings = require("settings-sharelatex")
|
settings = require("settings-sharelatex")
|
||||||
|
async = require("async")
|
||||||
|
_ = require("lodash")
|
||||||
|
|
||||||
module.exports =
|
module.exports = AnnouncementsHandler =
|
||||||
|
|
||||||
|
_domainSpecificAnnouncements : (email)->
|
||||||
|
domainSpecific = _.filter settings?.domainAnnouncements, (domainAnnouncment)->
|
||||||
|
matches = _.filter domainAnnouncment.domains, (domain)->
|
||||||
|
return email.indexOf(domain) != -1
|
||||||
|
return matches.length > 0 and domainAnnouncment.id?
|
||||||
|
return domainSpecific or []
|
||||||
|
|
||||||
|
|
||||||
|
getUnreadAnnouncements : (user, callback = (err, announcements)->)->
|
||||||
|
if !user? and !user._id?
|
||||||
|
return callback("user not supplied")
|
||||||
|
|
||||||
getUnreadAnnouncements : (user_id, callback = (err, announcements)->)->
|
|
||||||
async.parallel {
|
async.parallel {
|
||||||
lastEvent: (cb)->
|
lastEvent: (cb)->
|
||||||
AnalyticsManager.getLastOccurance user_id, "announcement-alert-dismissed", cb
|
AnalyticsManager.getLastOccurance user._id, "announcement-alert-dismissed", cb
|
||||||
announcements: (cb)->
|
announcements: (cb)->
|
||||||
BlogHandler.getLatestAnnouncements cb
|
BlogHandler.getLatestAnnouncements cb
|
||||||
}, (err, results)->
|
}, (err, results)->
|
||||||
if err?
|
if err?
|
||||||
logger.err err:err, user_id:user_id, "error getting unread announcements"
|
logger.err err:err, user_id:user._id, "error getting unread announcements"
|
||||||
return callback(err)
|
return callback(err)
|
||||||
|
|
||||||
announcements = _.sortBy(results.announcements, "date").reverse()
|
domainSpecific = AnnouncementsHandler._domainSpecificAnnouncements(user?.email)
|
||||||
|
|
||||||
|
domainSpecific = _.map domainSpecific, (domainAnnouncment)->
|
||||||
|
try
|
||||||
|
domainAnnouncment.date = new Date(domainAnnouncment.date)
|
||||||
|
return domainAnnouncment
|
||||||
|
catch e
|
||||||
|
return callback(e)
|
||||||
|
|
||||||
|
announcements = results.announcements
|
||||||
|
announcements = _.union announcements, domainSpecific
|
||||||
|
announcements = _.sortBy(announcements, "date").reverse()
|
||||||
|
|
||||||
lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId
|
lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId
|
||||||
|
|
||||||
|
@ -35,6 +57,6 @@ module.exports =
|
||||||
announcement.read = read
|
announcement.read = read
|
||||||
return announcement
|
return announcement
|
||||||
|
|
||||||
logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements"
|
logger.log announcementsLength:announcements?.length, user_id:user?._id, "returning announcements"
|
||||||
|
|
||||||
callback null, announcements
|
callback null, announcements
|
||||||
|
|
|
@ -148,6 +148,7 @@ module.exports = AuthenticationController =
|
||||||
return next()
|
return next()
|
||||||
else
|
else
|
||||||
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
|
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
|
||||||
|
AuthenticationController._setRedirectInSession(req)
|
||||||
return res.redirect "/login"
|
return res.redirect "/login"
|
||||||
|
|
||||||
httpAuth: basicAuth (user, pass)->
|
httpAuth: basicAuth (user, pass)->
|
||||||
|
@ -193,8 +194,8 @@ module.exports = AuthenticationController =
|
||||||
|
|
||||||
_setRedirectInSession: (req, value) ->
|
_setRedirectInSession: (req, value) ->
|
||||||
if !value?
|
if !value?
|
||||||
value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else req.path
|
value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}"
|
||||||
if req.session?
|
if req.session? && !value.match(new RegExp('^\/(socket.io|js|stylesheets|img)\/.*$'))
|
||||||
req.session.postLoginRedirect = value
|
req.session.postLoginRedirect = value
|
||||||
|
|
||||||
_getRedirectFromSession: (req) ->
|
_getRedirectFromSession: (req) ->
|
||||||
|
|
82
services/web/app/coffee/Features/Chat/ChatApiHandler.coffee
Normal file
82
services/web/app/coffee/Features/Chat/ChatApiHandler.coffee
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
request = require("request")
|
||||||
|
settings = require("settings-sharelatex")
|
||||||
|
logger = require("logger-sharelatex")
|
||||||
|
|
||||||
|
module.exports = ChatApiHandler =
|
||||||
|
_apiRequest: (opts, callback = (error, data) ->) ->
|
||||||
|
request opts, (error, response, data) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
if 200 <= response.statusCode < 300
|
||||||
|
return callback null, data
|
||||||
|
else
|
||||||
|
error = new Error("chat api returned non-success code: #{response.statusCode}")
|
||||||
|
error.statusCode = response.statusCode
|
||||||
|
logger.error {err: error, opts}, "error sending request to chat api"
|
||||||
|
return callback error
|
||||||
|
|
||||||
|
sendGlobalMessage: (project_id, user_id, content, callback)->
|
||||||
|
ChatApiHandler._apiRequest {
|
||||||
|
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
|
||||||
|
method: "POST"
|
||||||
|
json: {user_id, content}
|
||||||
|
}, callback
|
||||||
|
|
||||||
|
getGlobalMessages: (project_id, limit, before, callback)->
|
||||||
|
qs = {}
|
||||||
|
qs.limit = limit if limit?
|
||||||
|
qs.before = before if before?
|
||||||
|
|
||||||
|
ChatApiHandler._apiRequest {
|
||||||
|
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
|
||||||
|
method: "GET"
|
||||||
|
qs: qs
|
||||||
|
json: true
|
||||||
|
}, callback
|
||||||
|
|
||||||
|
sendComment: (project_id, thread_id, user_id, content, callback = (error) ->) ->
|
||||||
|
ChatApiHandler._apiRequest {
|
||||||
|
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages"
|
||||||
|
method: "POST"
|
||||||
|
json: {user_id, content}
|
||||||
|
}, callback
|
||||||
|
|
||||||
|
getThreads: (project_id, callback = (error) ->) ->
|
||||||
|
ChatApiHandler._apiRequest {
|
||||||
|
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/threads"
|
||||||
|
method: "GET"
|
||||||
|
json: true
|
||||||
|
}, callback
|
||||||
|
|
||||||
|
resolveThread: (project_id, thread_id, user_id, callback = (error) ->) ->
|
||||||
|
ChatApiHandler._apiRequest {
|
||||||
|
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/resolve"
|
||||||
|
method: "POST"
|
||||||
|
json: {user_id}
|
||||||
|
}, callback
|
||||||
|
|
||||||
|
reopenThread: (project_id, thread_id, callback = (error) ->) ->
|
||||||
|
ChatApiHandler._apiRequest {
|
||||||
|
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/reopen"
|
||||||
|
method: "POST"
|
||||||
|
}, callback
|
||||||
|
|
||||||
|
deleteThread: (project_id, thread_id, callback = (error) ->) ->
|
||||||
|
ChatApiHandler._apiRequest {
|
||||||
|
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}"
|
||||||
|
method: "DELETE"
|
||||||
|
}, callback
|
||||||
|
|
||||||
|
editMessage: (project_id, thread_id, message_id, content, callback = (error) ->) ->
|
||||||
|
ChatApiHandler._apiRequest {
|
||||||
|
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}/edit"
|
||||||
|
method: "POST"
|
||||||
|
json:
|
||||||
|
content: content
|
||||||
|
}, callback
|
||||||
|
|
||||||
|
deleteMessage: (project_id, thread_id, message_id, callback = (error) ->) ->
|
||||||
|
ChatApiHandler._apiRequest {
|
||||||
|
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}"
|
||||||
|
method: "DELETE"
|
||||||
|
}, callback
|
||||||
|
|
|
@ -1,33 +1,34 @@
|
||||||
ChatHandler = require("./ChatHandler")
|
ChatApiHandler = require("./ChatApiHandler")
|
||||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||||
logger = require("logger-sharelatex")
|
logger = require("logger-sharelatex")
|
||||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||||
|
UserInfoManager = require('../User/UserInfoManager')
|
||||||
|
UserInfoController = require('../User/UserInfoController')
|
||||||
|
CommentsController = require('../Comments/CommentsController')
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
|
||||||
|
|
||||||
sendMessage: (req, res, next)->
|
sendMessage: (req, res, next)->
|
||||||
project_id = req.params.Project_id
|
project_id = req.params.project_id
|
||||||
messageContent = req.body.content
|
content = req.body.content
|
||||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||||
if !user_id?
|
if !user_id?
|
||||||
err = new Error('no logged-in user')
|
err = new Error('no logged-in user')
|
||||||
return next(err)
|
return next(err)
|
||||||
ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)->
|
ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) ->
|
||||||
if err?
|
return next(err) if err?
|
||||||
logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api"
|
UserInfoManager.getPersonalInfo message.user_id, (err, user) ->
|
||||||
return res.sendStatus(500)
|
return next(err) if err?
|
||||||
EditorRealTimeController.emitToRoom project_id, "new-chat-message", builtMessge, (err)->
|
message.user = UserInfoController.formatPersonalInfo(user)
|
||||||
res.send()
|
EditorRealTimeController.emitToRoom project_id, "new-chat-message", message, (err)->
|
||||||
|
res.send(204)
|
||||||
|
|
||||||
getMessages: (req, res)->
|
getMessages: (req, res, next)->
|
||||||
project_id = req.params.Project_id
|
project_id = req.params.project_id
|
||||||
query = req.query
|
query = req.query
|
||||||
logger.log project_id:project_id, query:query, "getting messages"
|
logger.log project_id:project_id, query:query, "getting messages"
|
||||||
ChatHandler.getMessages project_id, query, (err, messages)->
|
ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
|
||||||
if err?
|
return next(err) if err?
|
||||||
logger.err err:err, query:query, "problem getting messages from chat api"
|
CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
|
||||||
return res.sendStatus 500
|
return next(err) if err?
|
||||||
logger.log length:messages?.length, "sending messages to client"
|
logger.log length: messages?.length, "sending messages to client"
|
||||||
res.set 'Content-Type', 'application/json'
|
res.json messages
|
||||||
res.send messages
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
request = require("request")
|
|
||||||
settings = require("settings-sharelatex")
|
|
||||||
logger = require("logger-sharelatex")
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
|
|
||||||
sendMessage: (project_id, user_id, messageContent, callback)->
|
|
||||||
opts =
|
|
||||||
method:"post"
|
|
||||||
json:
|
|
||||||
content:messageContent
|
|
||||||
user_id:user_id
|
|
||||||
uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
|
|
||||||
request opts, (err, response, body)->
|
|
||||||
if err?
|
|
||||||
logger.err err:err, "problem sending new message to chat"
|
|
||||||
callback(err, body)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getMessages: (project_id, query, callback)->
|
|
||||||
qs = {}
|
|
||||||
qs.limit = query.limit if query?.limit?
|
|
||||||
qs.before = query.before if query?.before?
|
|
||||||
|
|
||||||
opts =
|
|
||||||
uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
|
|
||||||
method:"get"
|
|
||||||
qs: qs
|
|
||||||
|
|
||||||
request opts, (err, response, body)->
|
|
||||||
callback(err, body)
|
|
|
@ -11,7 +11,7 @@ module.exports = CollaboratorsEmailHandler =
|
||||||
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
|
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
|
||||||
].join("&")
|
].join("&")
|
||||||
|
|
||||||
notifyUserOfProjectInvite: (project_id, email, invite, callback)->
|
notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)->
|
||||||
Project
|
Project
|
||||||
.findOne(_id: project_id )
|
.findOne(_id: project_id )
|
||||||
.select("name owner_ref")
|
.select("name owner_ref")
|
||||||
|
@ -24,4 +24,5 @@ module.exports = CollaboratorsEmailHandler =
|
||||||
name: project.name
|
name: project.name
|
||||||
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
|
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
|
||||||
owner: project.owner_ref
|
owner: project.owner_ref
|
||||||
|
sendingUser_id: sendingUser._id
|
||||||
EmailHandler.sendEmail "projectInvite", emailOptions, callback
|
EmailHandler.sendEmail "projectInvite", emailOptions, callback
|
||||||
|
|
|
@ -10,6 +10,7 @@ EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||||
AnalyticsManger = require("../Analytics/AnalyticsManager")
|
AnalyticsManger = require("../Analytics/AnalyticsManager")
|
||||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||||
|
rateLimiter = require("../../infrastructure/RateLimiter")
|
||||||
|
|
||||||
module.exports = CollaboratorsInviteController =
|
module.exports = CollaboratorsInviteController =
|
||||||
|
|
||||||
|
@ -31,12 +32,28 @@ module.exports = CollaboratorsInviteController =
|
||||||
callback(null, userExists)
|
callback(null, userExists)
|
||||||
else
|
else
|
||||||
callback(null, true)
|
callback(null, true)
|
||||||
|
|
||||||
|
_checkRateLimit: (user_id, callback = (error) ->) ->
|
||||||
|
LimitationsManager.allowedNumberOfCollaboratorsForUser user_id, (err, collabLimit = 1)->
|
||||||
|
return callback(err) if err?
|
||||||
|
if collabLimit == -1
|
||||||
|
collabLimit = 20
|
||||||
|
collabLimit = collabLimit * 10
|
||||||
|
opts =
|
||||||
|
endpointName: "invite-to-project-by-user-id"
|
||||||
|
timeInterval: 60 * 30
|
||||||
|
subjectName: user_id
|
||||||
|
throttle: collabLimit
|
||||||
|
rateLimiter.addCount opts, callback
|
||||||
|
|
||||||
inviteToProject: (req, res, next) ->
|
inviteToProject: (req, res, next) ->
|
||||||
projectId = req.params.Project_id
|
projectId = req.params.Project_id
|
||||||
email = req.body.email
|
email = req.body.email
|
||||||
sendingUser = AuthenticationController.getSessionUser(req)
|
sendingUser = AuthenticationController.getSessionUser(req)
|
||||||
sendingUserId = sendingUser._id
|
sendingUserId = sendingUser._id
|
||||||
|
if email == sendingUser.email
|
||||||
|
logger.log {projectId, email, sendingUserId}, "cannot invite yourself to project"
|
||||||
|
return res.json {invite: null, error: 'cannot_invite_self'}
|
||||||
logger.log {projectId, email, sendingUserId}, "inviting to project"
|
logger.log {projectId, email, sendingUserId}, "inviting to project"
|
||||||
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
|
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
|
||||||
return next(error) if error?
|
return next(error) if error?
|
||||||
|
@ -48,20 +65,24 @@ module.exports = CollaboratorsInviteController =
|
||||||
if !email? or email == ""
|
if !email? or email == ""
|
||||||
logger.log {projectId, email, sendingUserId}, "invalid email address"
|
logger.log {projectId, email, sendingUserId}, "invalid email address"
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
|
CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) ->
|
||||||
if err?
|
return next(error) if error?
|
||||||
logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
|
if !underRateLimit
|
||||||
return next(err)
|
return res.sendStatus(429)
|
||||||
if !shouldAllowInvite
|
CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
|
||||||
logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
|
|
||||||
return res.json {invite: null, error: 'cannot_invite_non_user'}
|
|
||||||
CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
|
|
||||||
if err?
|
if err?
|
||||||
logger.err {projectId, email, sendingUserId}, "error creating project invite"
|
logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
|
||||||
return next(err)
|
return next(err)
|
||||||
logger.log {projectId, email, sendingUserId}, "invite created"
|
if !shouldAllowInvite
|
||||||
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
|
logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
|
||||||
return res.json {invite: invite}
|
return res.json {invite: null, error: 'cannot_invite_non_user'}
|
||||||
|
CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
|
||||||
|
if err?
|
||||||
|
logger.err {projectId, email, sendingUserId}, "error creating project invite"
|
||||||
|
return next(err)
|
||||||
|
logger.log {projectId, email, sendingUserId}, "invite created"
|
||||||
|
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
|
||||||
|
return res.json {invite: invite}
|
||||||
|
|
||||||
revokeInvite: (req, res, next) ->
|
revokeInvite: (req, res, next) ->
|
||||||
projectId = req.params.Project_id
|
projectId = req.params.Project_id
|
||||||
|
|
|
@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler =
|
||||||
|
|
||||||
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
|
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
|
||||||
logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite"
|
logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite"
|
||||||
CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, (err)->
|
CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, sendingUser, (err)->
|
||||||
return callback(err) if err?
|
return callback(err) if err?
|
||||||
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
|
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
|
||||||
return callback(err) if err?
|
return callback(err) if err?
|
||||||
|
@ -80,7 +80,7 @@ module.exports = CollaboratorsInviteHandler =
|
||||||
# Send email and notification in background
|
# Send email and notification in background
|
||||||
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
|
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
|
||||||
if err?
|
if err?
|
||||||
logger.err {projectId, email}, "error sending messages for invite"
|
logger.err {err, projectId, email}, "error sending messages for invite"
|
||||||
callback(null, invite)
|
callback(null, invite)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,15 @@ module.exports =
|
||||||
webRouter.post(
|
webRouter.post(
|
||||||
'/project/:Project_id/invite',
|
'/project/:Project_id/invite',
|
||||||
RateLimiterMiddlewear.rateLimit({
|
RateLimiterMiddlewear.rateLimit({
|
||||||
endpointName: "invite-to-project"
|
endpointName: "invite-to-project-by-project-id"
|
||||||
params: ["Project_id"]
|
params: ["Project_id"]
|
||||||
maxRequests: 200
|
maxRequests: 100
|
||||||
|
timeInterval: 60 * 10
|
||||||
|
}),
|
||||||
|
RateLimiterMiddlewear.rateLimit({
|
||||||
|
endpointName: "invite-to-project-by-ip"
|
||||||
|
ipOnly:true
|
||||||
|
maxRequests: 100
|
||||||
timeInterval: 60 * 10
|
timeInterval: 60 * 10
|
||||||
}),
|
}),
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
ChatApiHandler = require("../Chat/ChatApiHandler")
|
||||||
|
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||||
|
logger = require("logger-sharelatex")
|
||||||
|
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||||
|
UserInfoManager = require('../User/UserInfoManager')
|
||||||
|
UserInfoController = require('../User/UserInfoController')
|
||||||
|
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||||
|
async = require "async"
|
||||||
|
|
||||||
|
module.exports = CommentsController =
|
||||||
|
sendComment: (req, res, next) ->
|
||||||
|
{project_id, thread_id} = req.params
|
||||||
|
content = req.body.content
|
||||||
|
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||||
|
if !user_id?
|
||||||
|
err = new Error('no logged-in user')
|
||||||
|
return next(err)
|
||||||
|
logger.log {project_id, thread_id, user_id, content}, "sending comment"
|
||||||
|
ChatApiHandler.sendComment project_id, thread_id, user_id, content, (err, comment) ->
|
||||||
|
return next(err) if err?
|
||||||
|
UserInfoManager.getPersonalInfo comment.user_id, (err, user) ->
|
||||||
|
return next(err) if err?
|
||||||
|
comment.user = UserInfoController.formatPersonalInfo(user)
|
||||||
|
EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err) ->
|
||||||
|
res.send 204
|
||||||
|
|
||||||
|
getThreads: (req, res, next) ->
|
||||||
|
{project_id} = req.params
|
||||||
|
logger.log {project_id}, "getting comment threads for project"
|
||||||
|
ChatApiHandler.getThreads project_id, (err, threads) ->
|
||||||
|
return next(err) if err?
|
||||||
|
CommentsController._injectUserInfoIntoThreads threads, (error, threads) ->
|
||||||
|
return next(err) if err?
|
||||||
|
res.json threads
|
||||||
|
|
||||||
|
resolveThread: (req, res, next) ->
|
||||||
|
{project_id, thread_id} = req.params
|
||||||
|
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||||
|
logger.log {project_id, thread_id, user_id}, "resolving comment thread"
|
||||||
|
ChatApiHandler.resolveThread project_id, thread_id, user_id, (err) ->
|
||||||
|
return next(err) if err?
|
||||||
|
UserInfoManager.getPersonalInfo user_id, (err, user) ->
|
||||||
|
return next(err) if err?
|
||||||
|
EditorRealTimeController.emitToRoom project_id, "resolve-thread", thread_id, UserInfoController.formatPersonalInfo(user), (err)->
|
||||||
|
res.send 204
|
||||||
|
|
||||||
|
reopenThread: (req, res, next) ->
|
||||||
|
{project_id, thread_id} = req.params
|
||||||
|
logger.log {project_id, thread_id}, "reopening comment thread"
|
||||||
|
ChatApiHandler.reopenThread project_id, thread_id, (err, threads) ->
|
||||||
|
return next(err) if err?
|
||||||
|
EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)->
|
||||||
|
res.send 204
|
||||||
|
|
||||||
|
deleteThread: (req, res, next) ->
|
||||||
|
{project_id, doc_id, thread_id} = req.params
|
||||||
|
logger.log {project_id, doc_id, thread_id}, "deleting comment thread"
|
||||||
|
DocumentUpdaterHandler.deleteThread project_id, doc_id, thread_id, (err) ->
|
||||||
|
return next(err) if err?
|
||||||
|
ChatApiHandler.deleteThread project_id, thread_id, (err, threads) ->
|
||||||
|
return next(err) if err?
|
||||||
|
EditorRealTimeController.emitToRoom project_id, "delete-thread", thread_id, (err)->
|
||||||
|
res.send 204
|
||||||
|
|
||||||
|
editMessage: (req, res, next) ->
|
||||||
|
{project_id, thread_id, message_id} = req.params
|
||||||
|
{content} = req.body
|
||||||
|
logger.log {project_id, thread_id, message_id}, "editing message thread"
|
||||||
|
ChatApiHandler.editMessage project_id, thread_id, message_id, content, (err) ->
|
||||||
|
return next(err) if err?
|
||||||
|
EditorRealTimeController.emitToRoom project_id, "edit-message", thread_id, message_id, content, (err)->
|
||||||
|
res.send 204
|
||||||
|
|
||||||
|
deleteMessage: (req, res, next) ->
|
||||||
|
{project_id, thread_id, message_id} = req.params
|
||||||
|
logger.log {project_id, thread_id, message_id}, "deleting message"
|
||||||
|
ChatApiHandler.deleteMessage project_id, thread_id, message_id, (err, threads) ->
|
||||||
|
return next(err) if err?
|
||||||
|
EditorRealTimeController.emitToRoom project_id, "delete-message", thread_id, message_id, (err)->
|
||||||
|
res.send 204
|
||||||
|
|
||||||
|
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
|
||||||
|
userCache = {}
|
||||||
|
getUserDetails = (user_id, callback = (error, user) ->) ->
|
||||||
|
return callback(null, userCache[user_id]) if userCache[user_id]?
|
||||||
|
UserInfoManager.getPersonalInfo user_id, (err, user) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
user = UserInfoController.formatPersonalInfo user
|
||||||
|
userCache[user_id] = user
|
||||||
|
callback null, user
|
||||||
|
|
||||||
|
jobs = []
|
||||||
|
for thread_id, thread of threads
|
||||||
|
do (thread) ->
|
||||||
|
if thread.resolved
|
||||||
|
jobs.push (cb) ->
|
||||||
|
getUserDetails thread.resolved_by_user_id, (error, user) ->
|
||||||
|
cb(error) if error?
|
||||||
|
thread.resolved_by_user = user
|
||||||
|
cb()
|
||||||
|
for message in thread.messages
|
||||||
|
do (message) ->
|
||||||
|
jobs.push (cb) ->
|
||||||
|
getUserDetails message.user_id, (error, user) ->
|
||||||
|
cb(error) if error?
|
||||||
|
message.user = user
|
||||||
|
cb()
|
||||||
|
|
||||||
|
async.series jobs, (error) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
return callback null, threads
|
|
@ -29,6 +29,21 @@ module.exports = DocstoreManager =
|
||||||
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
||||||
logger.error err: error, project_id: project_id, "error getting all docs from docstore"
|
logger.error err: error, project_id: project_id, "error getting all docs from docstore"
|
||||||
callback(error)
|
callback(error)
|
||||||
|
|
||||||
|
getAllRanges: (project_id, callback = (error) ->) ->
|
||||||
|
logger.log { project_id }, "getting all doc ranges for project in docstore api"
|
||||||
|
url = "#{settings.apis.docstore.url}/project/#{project_id}/ranges"
|
||||||
|
request.get {
|
||||||
|
url: url
|
||||||
|
json: true
|
||||||
|
}, (error, res, docs) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
if 200 <= res.statusCode < 300
|
||||||
|
callback(null, docs)
|
||||||
|
else
|
||||||
|
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
||||||
|
logger.error err: error, project_id: project_id, "error getting all doc ranges from docstore"
|
||||||
|
callback(error)
|
||||||
|
|
||||||
getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) ->
|
getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) ->
|
||||||
if typeof(options) == "function"
|
if typeof(options) == "function"
|
||||||
|
@ -45,13 +60,13 @@ module.exports = DocstoreManager =
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
if 200 <= res.statusCode < 300
|
if 200 <= res.statusCode < 300
|
||||||
logger.log doc_id: doc_id, project_id: project_id, version: doc.version, rev: doc.rev, "got doc from docstore api"
|
logger.log doc_id: doc_id, project_id: project_id, version: doc.version, rev: doc.rev, "got doc from docstore api"
|
||||||
callback(null, doc.lines, doc.rev, doc.version)
|
callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
|
||||||
else
|
else
|
||||||
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
||||||
logger.error err: error, project_id: project_id, doc_id: doc_id, "error getting doc from docstore"
|
logger.error err: error, project_id: project_id, doc_id: doc_id, "error getting doc from docstore"
|
||||||
callback(error)
|
callback(error)
|
||||||
|
|
||||||
updateDoc: (project_id, doc_id, lines, version, callback = (error, modified, rev) ->) ->
|
updateDoc: (project_id, doc_id, lines, version, ranges, callback = (error, modified, rev) ->) ->
|
||||||
logger.log project_id: project_id, doc_id: doc_id, "updating doc in docstore api"
|
logger.log project_id: project_id, doc_id: doc_id, "updating doc in docstore api"
|
||||||
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
|
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
|
||||||
request.post {
|
request.post {
|
||||||
|
@ -59,6 +74,7 @@ module.exports = DocstoreManager =
|
||||||
json:
|
json:
|
||||||
lines: lines
|
lines: lines
|
||||||
version: version
|
version: version
|
||||||
|
ranges: ranges
|
||||||
}, (error, res, result) ->
|
}, (error, res, result) ->
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
if 200 <= res.statusCode < 300
|
if 200 <= res.statusCode < 300
|
||||||
|
|
|
@ -95,7 +95,7 @@ module.exports = DocumentUpdaterHandler =
|
||||||
logger.error err: error, project_id: project_id, doc_id: doc_id, "document updater returned failure status code: #{res.statusCode}"
|
logger.error err: error, project_id: project_id, doc_id: doc_id, "document updater returned failure status code: #{res.statusCode}"
|
||||||
return callback(error)
|
return callback(error)
|
||||||
|
|
||||||
getDocument: (project_id, doc_id, fromVersion, callback = (error, exists, doclines, version) ->) ->
|
getDocument: (project_id, doc_id, fromVersion, callback = (error, doclines, version, ranges, ops) ->) ->
|
||||||
timer = new metrics.Timer("get-document")
|
timer = new metrics.Timer("get-document")
|
||||||
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
|
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
|
||||||
logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater"
|
logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater"
|
||||||
|
@ -110,7 +110,7 @@ module.exports = DocumentUpdaterHandler =
|
||||||
body = JSON.parse(body)
|
body = JSON.parse(body)
|
||||||
catch error
|
catch error
|
||||||
return callback(error)
|
return callback(error)
|
||||||
callback null, body.lines, body.version, body.ops
|
callback null, body.lines, body.version, body.ranges, body.ops
|
||||||
else
|
else
|
||||||
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
|
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||||
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||||
|
@ -137,15 +137,37 @@ module.exports = DocumentUpdaterHandler =
|
||||||
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
|
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||||
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||||
|
|
||||||
getNumberOfDocsInMemory : (callback)->
|
acceptChange: (project_id, doc_id, change_id, callback = (error) ->) ->
|
||||||
request.get "#{settings.apis.documentupdater.url}/total", (err, req, body)->
|
timer = new metrics.Timer("accept-change")
|
||||||
try
|
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept"
|
||||||
body = JSON.parse body
|
logger.log {project_id, doc_id, change_id}, "accepting change in document updater"
|
||||||
catch err
|
request.post url, (error, res, body)->
|
||||||
logger.err err:err, "error parsing response from doc updater about the total number of docs"
|
timer.done()
|
||||||
callback(err, body?.total)
|
if error?
|
||||||
|
logger.error {err:error, project_id, doc_id, change_id}, "error accepting change in doc updater"
|
||||||
|
return callback(error)
|
||||||
|
if res.statusCode >= 200 and res.statusCode < 300
|
||||||
|
logger.log {project_id, doc_id, change_id}, "accepted change in document updater"
|
||||||
|
return callback(null)
|
||||||
|
else
|
||||||
|
logger.error {project_id, doc_id, change_id}, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||||
|
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||||
|
|
||||||
|
deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) ->
|
||||||
|
timer = new metrics.Timer("delete-thread")
|
||||||
|
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/comment/#{thread_id}"
|
||||||
|
logger.log {project_id, doc_id, thread_id}, "deleting comment range in document updater"
|
||||||
|
request.del url, (error, res, body)->
|
||||||
|
timer.done()
|
||||||
|
if error?
|
||||||
|
logger.error {err:error, project_id, doc_id, thread_id}, "error deleting comment range in doc updater"
|
||||||
|
return callback(error)
|
||||||
|
if res.statusCode >= 200 and res.statusCode < 300
|
||||||
|
logger.log {project_id, doc_id, thread_id}, "deleted comment rangee in document updater"
|
||||||
|
return callback(null)
|
||||||
|
else
|
||||||
|
logger.error {project_id, doc_id, thread_id}, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||||
|
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||||
|
|
||||||
PENDINGUPDATESKEY = "PendingUpdates"
|
PENDINGUPDATESKEY = "PendingUpdates"
|
||||||
DOCLINESKEY = "doclines"
|
DOCLINESKEY = "doclines"
|
||||||
|
|
|
@ -7,7 +7,7 @@ module.exports =
|
||||||
doc_id = req.params.doc_id
|
doc_id = req.params.doc_id
|
||||||
plain = req?.query?.plain == 'true'
|
plain = req?.query?.plain == 'true'
|
||||||
logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)"
|
logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)"
|
||||||
ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version) ->
|
ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version, ranges) ->
|
||||||
if error?
|
if error?
|
||||||
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
|
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
|
||||||
return next(error)
|
return next(error)
|
||||||
|
@ -19,14 +19,15 @@ module.exports =
|
||||||
res.send JSON.stringify {
|
res.send JSON.stringify {
|
||||||
lines: lines
|
lines: lines
|
||||||
version: version
|
version: version
|
||||||
|
ranges: ranges
|
||||||
}
|
}
|
||||||
|
|
||||||
setDocument: (req, res, next = (error) ->) ->
|
setDocument: (req, res, next = (error) ->) ->
|
||||||
project_id = req.params.Project_id
|
project_id = req.params.Project_id
|
||||||
doc_id = req.params.doc_id
|
doc_id = req.params.doc_id
|
||||||
{lines, version} = req.body
|
{lines, version, ranges} = req.body
|
||||||
logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)"
|
logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)"
|
||||||
ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, (error) ->
|
ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, ranges, (error) ->
|
||||||
if error?
|
if error?
|
||||||
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
|
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
|
||||||
return next(error)
|
return next(error)
|
||||||
|
|
|
@ -7,7 +7,6 @@ ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
||||||
ProjectDeleter = require("../Project/ProjectDeleter")
|
ProjectDeleter = require("../Project/ProjectDeleter")
|
||||||
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||||
EditorRealTimeController = require("./EditorRealTimeController")
|
EditorRealTimeController = require("./EditorRealTimeController")
|
||||||
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
|
|
||||||
async = require('async')
|
async = require('async')
|
||||||
LockManager = require("../../infrastructure/LockManager")
|
LockManager = require("../../infrastructure/LockManager")
|
||||||
_ = require('underscore')
|
_ = require('underscore')
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
_ = require("underscore")
|
||||||
|
settings = require "settings-sharelatex"
|
||||||
|
|
||||||
|
module.exports = _.template """
|
||||||
|
<table class="row" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 16px; padding-left: 16px; padding-right: 16px; text-align: left; width: 564px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
|
||||||
|
<h3 class="avoid-auto-linking" style="Margin: 0; Margin-bottom: px; color: inherit; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: px; padding: 0; text-align: left; word-wrap: normal;">
|
||||||
|
<%= title %>
|
||||||
|
</h3>
|
||||||
|
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||||
|
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||||
|
<%= greeting %>
|
||||||
|
</p>
|
||||||
|
<p class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||||
|
<%= message %>
|
||||||
|
</p>
|
||||||
|
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||||
|
<center data-parsed="" style="min-width: 532px; width: 100%;">
|
||||||
|
<table class="button float-center" style="Margin: 0 0 16px 0; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 0 16px 0; padding: 0; text-align: center; vertical-align: top; width: auto;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #a93529; border: 2px solid #a93529; border-collapse: collapse !important; color: #fefefe; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||||
|
<a href="<%= ctaURL %>" style="Margin: 0; border: 0 solid #a93529; border-radius: 3px; color: #fefefe; display: inline-block; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: bold; line-height: 1.3; margin: 0; padding: 8px 16px 8px 16px; text-align: left; text-decoration: none;">
|
||||||
|
<%= ctaText %>
|
||||||
|
</a>
|
||||||
|
</td></tr></table></td></tr></table>
|
||||||
|
</center>
|
||||||
|
<% if (secondaryMessage) { %>
|
||||||
|
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||||
|
<p class="avoid-auto-linking" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
|
||||||
|
<%= secondaryMessage %>
|
||||||
|
</p>
|
||||||
|
<% } %>
|
||||||
|
</th>
|
||||||
|
<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<% if (gmailGoToAction) { %>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "http://schema.org",
|
||||||
|
"@type": "EmailMessage",
|
||||||
|
"potentialAction": {
|
||||||
|
"@type": "ViewAction",
|
||||||
|
"target": "<%= gmailGoToAction.target %>",
|
||||||
|
"url": "<%= gmailGoToAction.target %>",
|
||||||
|
"name": "<%= gmailGoToAction.name %>"
|
||||||
|
},
|
||||||
|
"description": "<%= gmailGoToAction.description %>"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<% } %>
|
||||||
|
"""
|
|
@ -1,6 +1,12 @@
|
||||||
_ = require('underscore')
|
_ = require('underscore')
|
||||||
|
|
||||||
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
|
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
|
||||||
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
|
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
|
||||||
|
BaseWithHeaderEmailLayout = require("./Layouts/BaseWithHeaderEmailLayout")
|
||||||
|
|
||||||
|
SingleCTAEmailBody = require("./Bodies/SingleCTAEmailBody")
|
||||||
|
|
||||||
|
|
||||||
settings = require("settings-sharelatex")
|
settings = require("settings-sharelatex")
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,7 +67,7 @@ ShareLaTeX Co-founder
|
||||||
|
|
||||||
templates.passwordResetRequested =
|
templates.passwordResetRequested =
|
||||||
subject: _.template "Password Reset - #{settings.appName}"
|
subject: _.template "Password Reset - #{settings.appName}"
|
||||||
layout: NotificationEmailLayout
|
layout: BaseWithHeaderEmailLayout
|
||||||
type:"notification"
|
type:"notification"
|
||||||
plainTextTemplate: _.template """
|
plainTextTemplate: _.template """
|
||||||
Password Reset
|
Password Reset
|
||||||
|
@ -78,36 +84,21 @@ Thank you
|
||||||
|
|
||||||
#{settings.appName} - <%= siteUrl %>
|
#{settings.appName} - <%= siteUrl %>
|
||||||
"""
|
"""
|
||||||
compiledTemplate: _.template """
|
compiledTemplate: (opts) ->
|
||||||
<h2>Password Reset</h2>
|
SingleCTAEmailBody({
|
||||||
<p>
|
title: "Password Reset"
|
||||||
We got a request to reset your #{settings.appName} password.
|
greeting: "Hi,"
|
||||||
<p>
|
message: "We got a request to reset your #{settings.appName} password."
|
||||||
<center>
|
secondaryMessage: "If you ignore this message, your password won't be changed.<br>If you didn't request a password reset, let us know."
|
||||||
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
|
ctaText: "Reset password"
|
||||||
<div style="padding-right:10px;padding-left:10px">
|
ctaURL: opts.setNewPasswordUrl
|
||||||
<a href="<%= setNewPasswordUrl %>" style="text-decoration:none" target="_blank">
|
gmailGoToAction: null
|
||||||
<span style= "font-size:16px;font-family:Arial;font-weight:bold;color:#fff;white-space:nowrap;display:block; text-align:center">
|
})
|
||||||
Reset password
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</center>
|
|
||||||
|
|
||||||
If you ignore this message, your password won't be changed.
|
|
||||||
<p>
|
|
||||||
If you didn't request a password reset, let us know.
|
|
||||||
|
|
||||||
</p>
|
|
||||||
<p>Thank you</p>
|
|
||||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
templates.projectInvite =
|
templates.projectInvite =
|
||||||
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
|
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
|
||||||
layout: NotificationEmailLayout
|
layout: BaseWithHeaderEmailLayout
|
||||||
type:"notification"
|
type:"notification"
|
||||||
plainTextTemplate: _.template """
|
plainTextTemplate: _.template """
|
||||||
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
|
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
|
||||||
|
@ -118,23 +109,23 @@ Thank you
|
||||||
|
|
||||||
#{settings.appName} - <%= siteUrl %>
|
#{settings.appName} - <%= siteUrl %>
|
||||||
"""
|
"""
|
||||||
compiledTemplate: _.template """
|
compiledTemplate: (opts) ->
|
||||||
<p>Hi, <%= owner.email %> wants to share <a href="<%= inviteUrl %>">'<%= project.name %>'</a> with you</p>
|
SingleCTAEmailBody({
|
||||||
<center>
|
title: "#{ opts.project.name } – shared by #{ opts.owner.email }"
|
||||||
<a style="text-decoration: none; width: 200px; background-color: #a93629; border: 1px solid #e24b3b; border-radius: 3px; padding: 15px; margin: 24px; display: block;" href="<%= inviteUrl %>" style="text-decoration:none" target="_blank">
|
greeting: "Hi,"
|
||||||
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you."
|
||||||
View Project
|
secondaryMessage: null
|
||||||
</span>
|
ctaText: "View project"
|
||||||
</a>
|
ctaURL: opts.inviteUrl
|
||||||
</center>
|
gmailGoToAction:
|
||||||
<p> Thank you</p>
|
target: opts.inviteUrl
|
||||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
name: "View project"
|
||||||
"""
|
description: "Join #{ opts.project.name } at ShareLaTeX"
|
||||||
|
})
|
||||||
|
|
||||||
templates.completeJoinGroupAccount =
|
templates.completeJoinGroupAccount =
|
||||||
subject: _.template "Verify Email to join <%= group_name %> group"
|
subject: _.template "Verify Email to join <%= group_name %> group"
|
||||||
layout: NotificationEmailLayout
|
layout: BaseWithHeaderEmailLayout
|
||||||
type:"notification"
|
type:"notification"
|
||||||
plainTextTemplate: _.template """
|
plainTextTemplate: _.template """
|
||||||
Hi, please verify your email to join the <%= group_name %> and get your free premium account
|
Hi, please verify your email to join the <%= group_name %> and get your free premium account
|
||||||
|
@ -145,22 +136,39 @@ Thank You
|
||||||
|
|
||||||
#{settings.appName} - <%= siteUrl %>
|
#{settings.appName} - <%= siteUrl %>
|
||||||
"""
|
"""
|
||||||
compiledTemplate: _.template """
|
compiledTemplate: (opts) ->
|
||||||
<p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p>
|
SingleCTAEmailBody({
|
||||||
<center>
|
title: "Verify Email to join #{ opts.group_name } group"
|
||||||
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
|
greeting: "Hi,"
|
||||||
<div style="padding-right:10px;padding-left:10px">
|
message: "please verify your email to join the #{ opts.group_name } group and get your free premium account."
|
||||||
<a href="<%= completeJoinUrl %>" style="text-decoration:none" target="_blank">
|
secondaryMessage: null
|
||||||
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
ctaText: "Verify now"
|
||||||
Verify now
|
ctaURL: opts.completeJoinUrl
|
||||||
</span>
|
gmailGoToAction: null
|
||||||
</a>
|
})
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</center>
|
templates.testEmail =
|
||||||
<p> Thank you</p>
|
subject: _.template "A Test Email from ShareLaTeX"
|
||||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
layout: BaseWithHeaderEmailLayout
|
||||||
|
type:"notification"
|
||||||
|
plainTextTemplate: _.template """
|
||||||
|
Hi,
|
||||||
|
|
||||||
|
This is a test email sent from ShareLaTeX.
|
||||||
|
|
||||||
|
#{settings.appName} - <%= siteUrl %>
|
||||||
"""
|
"""
|
||||||
|
compiledTemplate: (opts) ->
|
||||||
|
SingleCTAEmailBody({
|
||||||
|
title: "A Test Email from ShareLaTeX"
|
||||||
|
greeting: "Hi,"
|
||||||
|
message: "This is a test email sent from ShareLaTeX"
|
||||||
|
secondaryMessage: null
|
||||||
|
ctaText: "Open ShareLaTeX"
|
||||||
|
ctaURL: "/"
|
||||||
|
gmailGoToAction: null
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
|
|
@ -4,7 +4,7 @@ Settings = require('settings-sharelatex')
|
||||||
nodemailer = require("nodemailer")
|
nodemailer = require("nodemailer")
|
||||||
sesTransport = require('nodemailer-ses-transport')
|
sesTransport = require('nodemailer-ses-transport')
|
||||||
sgTransport = require('nodemailer-sendgrid-transport')
|
sgTransport = require('nodemailer-sendgrid-transport')
|
||||||
|
rateLimiter = require('../../infrastructure/RateLimiter')
|
||||||
_ = require("underscore")
|
_ = require("underscore")
|
||||||
|
|
||||||
if Settings.email? and Settings.email.fromAddress?
|
if Settings.email? and Settings.email.fromAddress?
|
||||||
|
@ -39,24 +39,39 @@ if nm_client?
|
||||||
else
|
else
|
||||||
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
|
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
|
||||||
|
|
||||||
|
checkCanSendEmail = (options, callback)->
|
||||||
|
if !options.sendingUser_id? #email not sent from user, not rate limited
|
||||||
|
return callback(null, true)
|
||||||
|
opts =
|
||||||
|
endpointName: "send_email"
|
||||||
|
timeInterval: 60 * 60 * 3
|
||||||
|
subjectName: options.sendingUser_id
|
||||||
|
throttle: 100
|
||||||
|
rateLimiter.addCount opts, callback
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
sendEmail : (options, callback = (error) ->)->
|
sendEmail : (options, callback = (error) ->)->
|
||||||
logger.log receiver:options.to, subject:options.subject, "sending email"
|
logger.log receiver:options.to, subject:options.subject, "sending email"
|
||||||
metrics.inc "email"
|
checkCanSendEmail options, (err, canContinue)->
|
||||||
options =
|
|
||||||
to: options.to
|
|
||||||
from: defaultFromAddress
|
|
||||||
subject: options.subject
|
|
||||||
html: options.html
|
|
||||||
text: options.text
|
|
||||||
replyTo: options.replyTo || Settings.email.replyToAddress
|
|
||||||
socketTimeout: 30 * 1000
|
|
||||||
if Settings.email.textEncoding?
|
|
||||||
opts.textEncoding = textEncoding
|
|
||||||
client.sendMail options, (err, res)->
|
|
||||||
if err?
|
if err?
|
||||||
logger.err err:err, "error sending message"
|
return callback(err)
|
||||||
else
|
if !canContinue
|
||||||
logger.log "Message sent to #{options.to}"
|
logger.log sendingUser_id:options.sendingUser_id, to:options.to, subject:options.subject, canContinue:canContinue, "rate limit hit for sending email, not sending"
|
||||||
callback(err)
|
return callback("rate limit hit sending email")
|
||||||
|
metrics.inc "email"
|
||||||
|
options =
|
||||||
|
to: options.to
|
||||||
|
from: defaultFromAddress
|
||||||
|
subject: options.subject
|
||||||
|
html: options.html
|
||||||
|
text: options.text
|
||||||
|
replyTo: options.replyTo || Settings.email.replyToAddress
|
||||||
|
socketTimeout: 30 * 1000
|
||||||
|
if Settings.email.textEncoding?
|
||||||
|
opts.textEncoding = textEncoding
|
||||||
|
client.sendMail options, (err, res)->
|
||||||
|
if err?
|
||||||
|
logger.err err:err, "error sending message"
|
||||||
|
else
|
||||||
|
logger.log "Message sent to #{options.to}"
|
||||||
|
callback(err)
|
||||||
|
|
|
@ -0,0 +1,380 @@
|
||||||
|
_ = require("underscore")
|
||||||
|
settings = require "settings-sharelatex"
|
||||||
|
|
||||||
|
module.exports = _.template """
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" style="Margin: 0; background: #f6f6f6 !important; margin: 0; min-height: 100%; padding: 0;">
|
||||||
|
<head>
|
||||||
|
|
||||||
|
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<title>Project invite</title>
|
||||||
|
<style>.avoid-auto-linking a,
|
||||||
|
.avoid-auto-linking a[href] {
|
||||||
|
color: #a93529 !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
hyphens: none; }
|
||||||
|
.avoid-auto-linking a:visited,
|
||||||
|
.avoid-auto-linking a[href]:visited {
|
||||||
|
color: #a93529; }
|
||||||
|
.avoid-auto-linking a:hover,
|
||||||
|
.avoid-auto-linking a[href]:hover {
|
||||||
|
color: #80281f; }
|
||||||
|
.avoid-auto-linking a:active,
|
||||||
|
.avoid-auto-linking a[href]:active {
|
||||||
|
color: #80281f; }
|
||||||
|
@media only screen {
|
||||||
|
html {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 596px) {
|
||||||
|
.small-float-center {
|
||||||
|
margin: 0 auto !important;
|
||||||
|
float: none !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-text-center {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-text-left {
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-text-right {
|
||||||
|
text-align: right !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 596px) {
|
||||||
|
.hide-for-large {
|
||||||
|
display: block !important;
|
||||||
|
width: auto !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
max-height: none !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 596px) {
|
||||||
|
table.body table.container .hide-for-large,
|
||||||
|
table.body table.container .row.hide-for-large {
|
||||||
|
display: table !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 596px) {
|
||||||
|
table.body table.container .callout-inner.hide-for-large {
|
||||||
|
display: table-cell !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 596px) {
|
||||||
|
table.body table.container .show-for-large {
|
||||||
|
display: none !important;
|
||||||
|
width: 0;
|
||||||
|
mso-hide: all;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 596px) {
|
||||||
|
table.body img {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body center {
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body .container {
|
||||||
|
width: 95% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body .columns,
|
||||||
|
table.body .column {
|
||||||
|
height: auto !important;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-left: 16px !important;
|
||||||
|
padding-right: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body .columns .column,
|
||||||
|
table.body .columns .columns,
|
||||||
|
table.body .column .column,
|
||||||
|
table.body .column .columns {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body .collapse .columns,
|
||||||
|
table.body .collapse .column {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-1,
|
||||||
|
th.small-1 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 8.33333% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-2,
|
||||||
|
th.small-2 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 16.66667% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-3,
|
||||||
|
th.small-3 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 25% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-4,
|
||||||
|
th.small-4 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 33.33333% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-5,
|
||||||
|
th.small-5 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 41.66667% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-6,
|
||||||
|
th.small-6 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-7,
|
||||||
|
th.small-7 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 58.33333% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-8,
|
||||||
|
th.small-8 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 66.66667% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-9,
|
||||||
|
th.small-9 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 75% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-10,
|
||||||
|
th.small-10 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 83.33333% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-11,
|
||||||
|
th.small-11 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 91.66667% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.small-12,
|
||||||
|
th.small-12 {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns td.small-12,
|
||||||
|
.column td.small-12,
|
||||||
|
.columns th.small-12,
|
||||||
|
.column th.small-12 {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-1,
|
||||||
|
table.body th.small-offset-1 {
|
||||||
|
margin-left: 8.33333% !important;
|
||||||
|
Margin-left: 8.33333% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-2,
|
||||||
|
table.body th.small-offset-2 {
|
||||||
|
margin-left: 16.66667% !important;
|
||||||
|
Margin-left: 16.66667% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-3,
|
||||||
|
table.body th.small-offset-3 {
|
||||||
|
margin-left: 25% !important;
|
||||||
|
Margin-left: 25% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-4,
|
||||||
|
table.body th.small-offset-4 {
|
||||||
|
margin-left: 33.33333% !important;
|
||||||
|
Margin-left: 33.33333% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-5,
|
||||||
|
table.body th.small-offset-5 {
|
||||||
|
margin-left: 41.66667% !important;
|
||||||
|
Margin-left: 41.66667% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-6,
|
||||||
|
table.body th.small-offset-6 {
|
||||||
|
margin-left: 50% !important;
|
||||||
|
Margin-left: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-7,
|
||||||
|
table.body th.small-offset-7 {
|
||||||
|
margin-left: 58.33333% !important;
|
||||||
|
Margin-left: 58.33333% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-8,
|
||||||
|
table.body th.small-offset-8 {
|
||||||
|
margin-left: 66.66667% !important;
|
||||||
|
Margin-left: 66.66667% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-9,
|
||||||
|
table.body th.small-offset-9 {
|
||||||
|
margin-left: 75% !important;
|
||||||
|
Margin-left: 75% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-10,
|
||||||
|
table.body th.small-offset-10 {
|
||||||
|
margin-left: 83.33333% !important;
|
||||||
|
Margin-left: 83.33333% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body td.small-offset-11,
|
||||||
|
table.body th.small-offset-11 {
|
||||||
|
margin-left: 91.66667% !important;
|
||||||
|
Margin-left: 91.66667% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body table.columns td.expander,
|
||||||
|
table.body table.columns th.expander {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body .right-text-pad,
|
||||||
|
table.body .text-pad-right {
|
||||||
|
padding-left: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.body .left-text-pad,
|
||||||
|
table.body .text-pad-left {
|
||||||
|
padding-right: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.menu {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.menu td,
|
||||||
|
table.menu th {
|
||||||
|
width: auto !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.menu.vertical td,
|
||||||
|
table.menu.vertical th,
|
||||||
|
table.menu.small-vertical td,
|
||||||
|
table.menu.small-vertical th {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.menu[align="center"] {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.button.small-expand,
|
||||||
|
table.button.small-expanded {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.button.small-expand table,
|
||||||
|
table.button.small-expanded table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.button.small-expand table a,
|
||||||
|
table.button.small-expanded table a {
|
||||||
|
text-align: center !important;
|
||||||
|
width: 100% !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.button.small-expand center,
|
||||||
|
table.button.small-expanded center {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}</style>
|
||||||
|
</head>
|
||||||
|
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="#F6F6F6" style="-moz-box-sizing: border-box; -ms-text-size-adjust: 100%; -webkit-box-sizing: border-box; -webkit-text-size-adjust: 100%; Margin: 0; background: #f6f6f6 !important; box-sizing: border-box; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; min-height: 100%; min-width: 100%; padding: 0; text-align: left; width: 100% !important;">
|
||||||
|
<!-- <span class="preheader"></span> -->
|
||||||
|
<table class="body" border="0" cellspacing="0" cellpadding="0" width="100%" height="100%" style="Margin: 0; background: #f6f6f6 !important; border-collapse: collapse; border-spacing: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; height: 100%; line-height: 1.3; margin: 0; min-height: 100%; padding: 0; text-align: left; vertical-align: top; width: 100%;">
|
||||||
|
<tr style="padding: 0; text-align: left; vertical-align: top;">
|
||||||
|
<td class="body-cell" align="center" valign="top" bgcolor="#F6F6F6" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #f6f6f6 !important; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; padding-bottom: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||||
|
<center data-parsed="" style="min-width: 580px; width: 100%;">
|
||||||
|
|
||||||
|
<table align="center" class="wrapper header float-center" style="Margin: 0 auto; background: #fefefe; border-bottom: solid 1px #cfcfcf; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||||
|
<table align="center" class="container" style="Margin: 0 auto; background: transparent; border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||||
|
<table class="row collapse" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 0; padding-left: 0; padding-right: 0; text-align: left; width: 588px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
|
||||||
|
<h1 class="sl-logotype" style="Margin: 0; Margin-bottom: 0; color: #333333; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 26px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 0; padding: 0; text-align: left; word-wrap: normal;">
|
||||||
|
<span>S</span><span class="sl-logotype-small" style="font-size: 80%;">HARE</span><span>L</span><span class="sl-logotype-small" style="font-size: 80%;">A</span><span>T</span><span class="sl-logotype-small" style="font-size: 80%;">E</span><span>X</span>
|
||||||
|
</h1>
|
||||||
|
</th>
|
||||||
|
<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
</td></tr></tbody></table>
|
||||||
|
</td></tr></table>
|
||||||
|
<table class="spacer float-center" style="Margin: 0 auto; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||||
|
<table align="center" class="container main float-center" style="Margin: 0 auto; Margin-top: 10px; background: #fefefe; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; margin-top: 10px; padding: 0; text-align: center; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||||
|
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||||
|
|
||||||
|
<%= body %>
|
||||||
|
|
||||||
|
<table class="wrapper secondary" align="center" style="background: #f6f6f6; border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||||
|
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="10px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 10px; font-weight: normal; hyphens: auto; line-height: 10px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||||
|
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;"><small style="color: #7a7a7a; font-size: 80%;">
|
||||||
|
#{ settings.appName} • <a href="#{ settings.siteUrl }" style="Margin: 0; color: #a93529; font-family: Helvetica, Arial, sans-serif; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; text-decoration: none;">#{ settings.siteUrl }</a>
|
||||||
|
</small></p>
|
||||||
|
</td></tr></table>
|
||||||
|
</td></tr></tbody></table>
|
||||||
|
|
||||||
|
</center>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- prevent Gmail on iOS font size manipulation -->
|
||||||
|
<div style="display:none; white-space:nowrap; font:15px courier; line-height:0;"> </div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
"""
|
|
@ -0,0 +1,20 @@
|
||||||
|
logger = require "logger-sharelatex"
|
||||||
|
request = require "request"
|
||||||
|
settings = require "settings-sharelatex"
|
||||||
|
AuthenticationController = require "../Authentication/AuthenticationController"
|
||||||
|
|
||||||
|
module.exports = HistoryController =
|
||||||
|
proxyToHistoryApi: (req, res, next = (error) ->) ->
|
||||||
|
user_id = AuthenticationController.getLoggedInUserId req
|
||||||
|
url = settings.apis.trackchanges.url + req.url
|
||||||
|
logger.log url: url, "proxying to track-changes api"
|
||||||
|
getReq = request(
|
||||||
|
url: url
|
||||||
|
method: req.method
|
||||||
|
headers:
|
||||||
|
"X-User-Id": user_id
|
||||||
|
)
|
||||||
|
getReq.pipe(res)
|
||||||
|
getReq.on "error", (error) ->
|
||||||
|
logger.error err: error, "track-changes API error"
|
||||||
|
next(error)
|
|
@ -0,0 +1,28 @@
|
||||||
|
settings = require "settings-sharelatex"
|
||||||
|
request = require "request"
|
||||||
|
logger = require "logger-sharelatex"
|
||||||
|
|
||||||
|
module.exports = HistoryManager =
|
||||||
|
flushProject: (project_id, callback = (error) ->) ->
|
||||||
|
logger.log project_id: project_id, "flushing project in track-changes api"
|
||||||
|
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/flush"
|
||||||
|
request.post url, (error, res, body) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
if 200 <= res.statusCode < 300
|
||||||
|
callback(null)
|
||||||
|
else
|
||||||
|
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
|
||||||
|
logger.error err: error, project_id: project_id, "error flushing project in track-changes api"
|
||||||
|
callback(error)
|
||||||
|
|
||||||
|
archiveProject: (project_id, callback = ()->)->
|
||||||
|
logger.log project_id: project_id, "archving project in track-changes api"
|
||||||
|
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/archive"
|
||||||
|
request.post url, (error, res, body) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
if 200 <= res.statusCode < 300
|
||||||
|
callback(null)
|
||||||
|
else
|
||||||
|
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
|
||||||
|
logger.error err: error, project_id: project_id, "error archving project in track-changes api"
|
||||||
|
callback(error)
|
|
@ -5,8 +5,6 @@ DocstoreManager = require("../Docstore/DocstoreManager")
|
||||||
ProjectGetter = require("../Project/ProjectGetter")
|
ProjectGetter = require("../Project/ProjectGetter")
|
||||||
ProjectUpdateHandler = require("../Project/ProjectUpdateHandler")
|
ProjectUpdateHandler = require("../Project/ProjectUpdateHandler")
|
||||||
Project = require("../../models/Project").Project
|
Project = require("../../models/Project").Project
|
||||||
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
|
|
||||||
|
|
||||||
|
|
||||||
MILISECONDS_IN_DAY = 86400000
|
MILISECONDS_IN_DAY = 86400000
|
||||||
module.exports = InactiveProjectManager =
|
module.exports = InactiveProjectManager =
|
||||||
|
@ -52,7 +50,6 @@ module.exports = InactiveProjectManager =
|
||||||
logger.log project_id:project_id, "deactivating inactive project"
|
logger.log project_id:project_id, "deactivating inactive project"
|
||||||
jobs = [
|
jobs = [
|
||||||
(cb)-> DocstoreManager.archiveProject project_id, cb
|
(cb)-> DocstoreManager.archiveProject project_id, cb
|
||||||
# (cb)-> TrackChangesManager.archiveProject project_id, cb
|
|
||||||
(cb)-> ProjectUpdateHandler.markAsInactive project_id, cb
|
(cb)-> ProjectUpdateHandler.markAsInactive project_id, cb
|
||||||
]
|
]
|
||||||
async.series jobs, (err)->
|
async.series jobs, (err)->
|
||||||
|
|
|
@ -53,7 +53,11 @@ module.exports =
|
||||||
if req.body.login_after
|
if req.body.login_after
|
||||||
UserGetter.getUser user_id, {email: 1}, (err, user) ->
|
UserGetter.getUser user_id, {email: 1}, (err, user) ->
|
||||||
return next(err) if err?
|
return next(err) if err?
|
||||||
AuthenticationController.doLogin {email:user.email, password: password}, req, res, next
|
AuthenticationController.afterLoginSessionSetup req, user, (err) ->
|
||||||
|
if err?
|
||||||
|
logger.err {err, email: user.email}, "Error setting up session after setting password"
|
||||||
|
return next(err)
|
||||||
|
res.json {redir: AuthenticationController._getRedirectFromSession(req) || "/project"}
|
||||||
else
|
else
|
||||||
res.sendStatus 200
|
res.sendStatus 200
|
||||||
else
|
else
|
||||||
|
|
|
@ -197,11 +197,11 @@ module.exports = ProjectController =
|
||||||
user_id = null
|
user_id = null
|
||||||
|
|
||||||
project_id = req.params.Project_id
|
project_id = req.params.Project_id
|
||||||
logger.log project_id:project_id, "loading editor"
|
logger.log project_id:project_id, anonymous:anonymous, user_id:user_id, "loading editor"
|
||||||
|
|
||||||
async.parallel {
|
async.parallel {
|
||||||
project: (cb)->
|
project: (cb)->
|
||||||
ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1}, cb
|
ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1, track_changes: 1 }, cb
|
||||||
user: (cb)->
|
user: (cb)->
|
||||||
if !user_id?
|
if !user_id?
|
||||||
cb null, defaultSettingsForAnonymousUser(user_id)
|
cb null, defaultSettingsForAnonymousUser(user_id)
|
||||||
|
@ -267,6 +267,7 @@ module.exports = ProjectController =
|
||||||
pdfViewer : user.ace.pdfViewer
|
pdfViewer : user.ace.pdfViewer
|
||||||
syntaxValidation: user.ace.syntaxValidation
|
syntaxValidation: user.ace.syntaxValidation
|
||||||
}
|
}
|
||||||
|
trackChangesEnabled: !!project.track_changes
|
||||||
privilegeLevel: privilegeLevel
|
privilegeLevel: privilegeLevel
|
||||||
chatUrl: Settings.apis.chat.url
|
chatUrl: Settings.apis.chat.url
|
||||||
anonymous: anonymous
|
anonymous: anonymous
|
||||||
|
|
|
@ -19,6 +19,11 @@ module.exports = ProjectEditorHandler =
|
||||||
|
|
||||||
if !result.invites?
|
if !result.invites?
|
||||||
result.invites = []
|
result.invites = []
|
||||||
|
|
||||||
|
trackChangesVisible = false
|
||||||
|
for member in members
|
||||||
|
if member.privilegeLevel == "owner" and member.user?.featureSwitches?.track_changes
|
||||||
|
trackChangesVisible = true
|
||||||
|
|
||||||
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
|
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
|
||||||
result.owner = owner
|
result.owner = owner
|
||||||
|
@ -32,6 +37,8 @@ module.exports = ProjectEditorHandler =
|
||||||
compileGroup:"standard"
|
compileGroup:"standard"
|
||||||
templates: false
|
templates: false
|
||||||
references: false
|
references: false
|
||||||
|
trackChanges: false
|
||||||
|
trackChangesVisible: trackChangesVisible
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -126,7 +126,7 @@ module.exports = ProjectEntityHandler =
|
||||||
doc = new Doc name: docName
|
doc = new Doc name: docName
|
||||||
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
|
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
|
||||||
# which hasn't been created in docstore.
|
# which hasn't been created in docstore.
|
||||||
DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, (err, modified, rev) ->
|
DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, {}, (err, modified, rev) ->
|
||||||
return callback(err) if err?
|
return callback(err) if err?
|
||||||
|
|
||||||
ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=>
|
ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=>
|
||||||
|
@ -292,7 +292,7 @@ module.exports = ProjectEntityHandler =
|
||||||
return callback(err)
|
return callback(err)
|
||||||
callback(err, folder, parentFolder_id)
|
callback(err, folder, parentFolder_id)
|
||||||
|
|
||||||
updateDocLines : (project_id, doc_id, lines, version, callback = (error) ->)->
|
updateDocLines : (project_id, doc_id, lines, version, ranges, callback = (error) ->)->
|
||||||
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
|
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
|
||||||
return callback(err) if err?
|
return callback(err) if err?
|
||||||
return callback(new Errors.NotFoundError("project not found")) if !project?
|
return callback(new Errors.NotFoundError("project not found")) if !project?
|
||||||
|
@ -307,7 +307,7 @@ module.exports = ProjectEntityHandler =
|
||||||
return callback(error)
|
return callback(error)
|
||||||
|
|
||||||
logger.log project_id: project_id, doc_id: doc_id, "telling docstore manager to update doc"
|
logger.log project_id: project_id, doc_id: doc_id, "telling docstore manager to update doc"
|
||||||
DocstoreManager.updateDoc project_id, doc_id, lines, version, (err, modified, rev) ->
|
DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) ->
|
||||||
if err?
|
if err?
|
||||||
logger.error err: err, doc_id: doc_id, project_id:project_id, lines: lines, "error sending doc to docstore"
|
logger.error err: err, doc_id: doc_id, project_id:project_id, lines: lines, "error sending doc to docstore"
|
||||||
return callback(err)
|
return callback(err)
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
Settings = require('settings-sharelatex')
|
RateLimiter = require('../../infrastructure/RateLimiter')
|
||||||
redis = require("redis-sharelatex")
|
|
||||||
rclient = redis.createClient(Settings.redis.web)
|
|
||||||
|
|
||||||
buildKey = (k)->
|
|
||||||
return "LoginRateLimit:#{k}"
|
|
||||||
|
|
||||||
ONE_MIN = 60
|
ONE_MIN = 60
|
||||||
ATTEMPT_LIMIT = 10
|
ATTEMPT_LIMIT = 10
|
||||||
|
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
processLoginRequest: (email, callback)->
|
|
||||||
multi = rclient.multi()
|
processLoginRequest: (email, callback) ->
|
||||||
multi.incr(buildKey(email))
|
opts =
|
||||||
multi.get(buildKey(email))
|
endpointName: 'login'
|
||||||
multi.expire(buildKey(email), ONE_MIN * 2)
|
throttle: ATTEMPT_LIMIT
|
||||||
multi.exec (err, results)->
|
timeInterval: ONE_MIN * 2
|
||||||
loginCount = results[1]
|
subjectName: email
|
||||||
allow = loginCount <= ATTEMPT_LIMIT
|
RateLimiter.addCount opts, (err, shouldAllow) ->
|
||||||
callback err, allow
|
callback(err, shouldAllow)
|
||||||
|
|
||||||
recordSuccessfulLogin: (email, callback = ->)->
|
recordSuccessfulLogin: (email, callback = ->)->
|
||||||
rclient.del buildKey(email), callback
|
RateLimiter.clearRateLimit 'login', email, callback
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,15 @@ module.exports = RateLimiterMiddlewear =
|
||||||
user_id = AuthenticationController.getLoggedInUserId(req) || req.ip
|
user_id = AuthenticationController.getLoggedInUserId(req) || req.ip
|
||||||
params = (opts.params or []).map (p) -> req.params[p]
|
params = (opts.params or []).map (p) -> req.params[p]
|
||||||
params.push user_id
|
params.push user_id
|
||||||
|
subjectName = params.join(":")
|
||||||
|
if opts.ipOnly
|
||||||
|
subjectName = req.ip
|
||||||
if !opts.endpointName?
|
if !opts.endpointName?
|
||||||
throw new Error("no endpointName provided")
|
throw new Error("no endpointName provided")
|
||||||
options = {
|
options = {
|
||||||
endpointName: opts.endpointName
|
endpointName: opts.endpointName
|
||||||
timeInterval: opts.timeInterval or 60
|
timeInterval: opts.timeInterval or 60
|
||||||
subjectName: params.join(":")
|
subjectName: subjectName
|
||||||
throttle: opts.maxRequests or 6
|
throttle: opts.maxRequests or 6
|
||||||
}
|
}
|
||||||
RateLimiter.addCount options, (error, canContinue)->
|
RateLimiter.addCount options, (error, canContinue)->
|
||||||
|
|
|
@ -6,8 +6,6 @@ Project = require('../../models/Project').Project
|
||||||
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||||
Settings = require('settings-sharelatex')
|
Settings = require('settings-sharelatex')
|
||||||
util = require('util')
|
util = require('util')
|
||||||
redis = require("redis-sharelatex")
|
|
||||||
rclient = redis.createClient(Settings.redis.web)
|
|
||||||
RecurlyWrapper = require('../Subscription/RecurlyWrapper')
|
RecurlyWrapper = require('../Subscription/RecurlyWrapper')
|
||||||
SubscriptionHandler = require('../Subscription/SubscriptionHandler')
|
SubscriptionHandler = require('../Subscription/SubscriptionHandler')
|
||||||
projectEntityHandler = require('../Project/ProjectEntityHandler')
|
projectEntityHandler = require('../Project/ProjectEntityHandler')
|
||||||
|
|
|
@ -7,7 +7,7 @@ fs = require "fs"
|
||||||
ErrorController = require "../Errors/ErrorController"
|
ErrorController = require "../Errors/ErrorController"
|
||||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||||
|
|
||||||
homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.jade")
|
homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.pug")
|
||||||
|
|
||||||
module.exports = HomeController =
|
module.exports = HomeController =
|
||||||
index : (req,res)->
|
index : (req,res)->
|
||||||
|
@ -28,10 +28,10 @@ module.exports = HomeController =
|
||||||
|
|
||||||
externalPage: (page, title) ->
|
externalPage: (page, title) ->
|
||||||
return (req, res, next = (error) ->) ->
|
return (req, res, next = (error) ->) ->
|
||||||
path = Path.resolve(__dirname + "/../../../views/external/#{page}.jade")
|
path = Path.resolve(__dirname + "/../../../views/external/#{page}.pug")
|
||||||
fs.exists path, (exists) -> # No error in this callback - old method in Node.js!
|
fs.exists path, (exists) -> # No error in this callback - old method in Node.js!
|
||||||
if exists
|
if exists
|
||||||
res.render "external/#{page}.jade",
|
res.render "external/#{page}.pug",
|
||||||
title: title
|
title: title
|
||||||
else
|
else
|
||||||
ErrorController.notFound(req, res, next)
|
ErrorController.notFound(req, res, next)
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
logger = require("logger-sharelatex")
|
logger = require("logger-sharelatex")
|
||||||
Project = require("../../models/Project").Project
|
Project = require("../../models/Project").Project
|
||||||
User = require("../../models/User").User
|
UserGetter = require("../User/UserGetter")
|
||||||
SubscriptionLocator = require("./SubscriptionLocator")
|
SubscriptionLocator = require("./SubscriptionLocator")
|
||||||
Settings = require("settings-sharelatex")
|
Settings = require("settings-sharelatex")
|
||||||
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
|
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
|
||||||
CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
|
CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
|
||||||
allowedNumberOfCollaboratorsInProject: (project_id, callback) ->
|
allowedNumberOfCollaboratorsInProject: (project_id, callback) ->
|
||||||
getOwnerOfProject project_id, (error, owner)->
|
Project.findById project_id, 'owner_ref', (error, project) =>
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
if owner.features? and owner.features.collaborators?
|
@allowedNumberOfCollaboratorsForUser project.owner_ref, callback
|
||||||
callback null, owner.features.collaborators
|
|
||||||
|
allowedNumberOfCollaboratorsForUser: (user_id, callback) ->
|
||||||
|
UserGetter.getUser user_id, {features: 1}, (error, user) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
if user.features? and user.features.collaborators?
|
||||||
|
callback null, user.features.collaborators
|
||||||
else
|
else
|
||||||
callback null, Settings.defaultPlanCode.collaborators
|
callback null, Settings.defaultPlanCode.collaborators
|
||||||
|
|
||||||
|
|
||||||
canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) ->
|
canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) ->
|
||||||
@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
|
@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
|
||||||
|
@ -63,8 +68,4 @@ module.exports =
|
||||||
logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
|
logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
|
||||||
callback(err, limitReached, subscription)
|
callback(err, limitReached, subscription)
|
||||||
|
|
||||||
getOwnerOfProject = (project_id, callback)->
|
getOwnerIdOfProject = (project_id, callback)->
|
||||||
Project.findById project_id, 'owner_ref', (error, project) ->
|
|
||||||
return callback(error) if error?
|
|
||||||
User.findById project.owner_ref, (error, owner) ->
|
|
||||||
callback(error, owner)
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ editorController = require('../Editor/EditorController')
|
||||||
logger = require('logger-sharelatex')
|
logger = require('logger-sharelatex')
|
||||||
Settings = require('settings-sharelatex')
|
Settings = require('settings-sharelatex')
|
||||||
FileTypeManager = require('../Uploads/FileTypeManager')
|
FileTypeManager = require('../Uploads/FileTypeManager')
|
||||||
uuid = require('node-uuid')
|
uuid = require('uuid')
|
||||||
fs = require('fs')
|
fs = require('fs')
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||||
|
DocstoreManager = require "../Docstore/DocstoreManager"
|
||||||
|
UserInfoManager = require "../User/UserInfoManager"
|
||||||
|
async = require "async"
|
||||||
|
|
||||||
|
module.exports = RangesManager =
|
||||||
|
getAllRanges: (project_id, callback = (error, docs) ->) ->
|
||||||
|
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
DocstoreManager.getAllRanges project_id, callback
|
||||||
|
|
||||||
|
getAllChangesUsers: (project_id, callback = (error, users) ->) ->
|
||||||
|
user_ids = {}
|
||||||
|
RangesManager.getAllRanges project_id, (error, docs) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
jobs = []
|
||||||
|
for doc in docs
|
||||||
|
for change in doc.ranges?.changes or []
|
||||||
|
user_ids[change.metadata.user_id] = true
|
||||||
|
|
||||||
|
async.mapSeries Object.keys(user_ids), (user_id, cb) ->
|
||||||
|
UserInfoManager.getPersonalInfo user_id, cb
|
||||||
|
, callback
|
|
@ -1,20 +1,42 @@
|
||||||
|
RangesManager = require "./RangesManager"
|
||||||
logger = require "logger-sharelatex"
|
logger = require "logger-sharelatex"
|
||||||
request = require "request"
|
UserInfoController = require "../User/UserInfoController"
|
||||||
settings = require "settings-sharelatex"
|
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||||
AuthenticationController = require "../Authentication/AuthenticationController"
|
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||||
|
TrackChangesManager = require "./TrackChangesManager"
|
||||||
|
|
||||||
module.exports = TrackChangesController =
|
module.exports = TrackChangesController =
|
||||||
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
|
getAllRanges: (req, res, next) ->
|
||||||
user_id = AuthenticationController.getLoggedInUserId req
|
project_id = req.params.project_id
|
||||||
url = settings.apis.trackchanges.url + req.url
|
logger.log {project_id}, "request for project ranges"
|
||||||
logger.log url: url, "proxying to track-changes api"
|
RangesManager.getAllRanges project_id, (error, docs = []) ->
|
||||||
getReq = request(
|
return next(error) if error?
|
||||||
url: url
|
docs = ({id: d._id, ranges: d.ranges} for d in docs)
|
||||||
method: req.method
|
res.json docs
|
||||||
headers:
|
|
||||||
"X-User-Id": user_id
|
getAllChangesUsers: (req, res, next) ->
|
||||||
)
|
project_id = req.params.project_id
|
||||||
getReq.pipe(res)
|
logger.log {project_id}, "request for project range users"
|
||||||
getReq.on "error", (error) ->
|
RangesManager.getAllChangesUsers project_id, (error, users) ->
|
||||||
logger.error err: error, "track-changes API error"
|
return next(error) if error?
|
||||||
next(error)
|
users = (UserInfoController.formatPersonalInfo(user) for user in users)
|
||||||
|
# Get rid of any anonymous/deleted user objects
|
||||||
|
users = users.filter (u) -> u?.id?
|
||||||
|
res.json users
|
||||||
|
|
||||||
|
acceptChange: (req, res, next) ->
|
||||||
|
{project_id, doc_id, change_id} = req.params
|
||||||
|
logger.log {project_id, doc_id, change_id}, "request to accept change"
|
||||||
|
DocumentUpdaterHandler.acceptChange project_id, doc_id, change_id, (error) ->
|
||||||
|
return next(error) if error?
|
||||||
|
EditorRealTimeController.emitToRoom project_id, "accept-change", doc_id, change_id, (err)->
|
||||||
|
res.send 204
|
||||||
|
|
||||||
|
toggleTrackChanges: (req, res, next) ->
|
||||||
|
{project_id} = req.params
|
||||||
|
track_changes_on = !!req.body.on
|
||||||
|
logger.log {project_id, track_changes_on}, "request to toggle track changes"
|
||||||
|
TrackChangesManager.toggleTrackChanges project_id, track_changes_on, (error) ->
|
||||||
|
return next(error) if error?
|
||||||
|
EditorRealTimeController.emitToRoom project_id, "toggle-track-changes", track_changes_on, (err)->
|
||||||
|
res.send 204
|
||||||
|
|
|
@ -1,28 +1,5 @@
|
||||||
settings = require "settings-sharelatex"
|
Project = require("../../models/Project").Project
|
||||||
request = require "request"
|
|
||||||
logger = require "logger-sharelatex"
|
|
||||||
|
|
||||||
module.exports = TrackChangesManager =
|
module.exports = TrackChangesManager =
|
||||||
flushProject: (project_id, callback = (error) ->) ->
|
toggleTrackChanges: (project_id, track_changes_on, callback = (error) ->) ->
|
||||||
logger.log project_id: project_id, "flushing project in track-changes api"
|
Project.update {_id: project_id}, {track_changes: track_changes_on}, callback
|
||||||
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/flush"
|
|
||||||
request.post url, (error, res, body) ->
|
|
||||||
return callback(error) if error?
|
|
||||||
if 200 <= res.statusCode < 300
|
|
||||||
callback(null)
|
|
||||||
else
|
|
||||||
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
|
|
||||||
logger.error err: error, project_id: project_id, "error flushing project in track-changes api"
|
|
||||||
callback(error)
|
|
||||||
|
|
||||||
archiveProject: (project_id, callback = ()->)->
|
|
||||||
logger.log project_id: project_id, "archving project in track-changes api"
|
|
||||||
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/archive"
|
|
||||||
request.post url, (error, res, body) ->
|
|
||||||
return callback(error) if error?
|
|
||||||
if 200 <= res.statusCode < 300
|
|
||||||
callback(null)
|
|
||||||
else
|
|
||||||
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
|
|
||||||
logger.error err: error, project_id: project_id, "error archving project in track-changes api"
|
|
||||||
callback(error)
|
|
||||||
|
|
|
@ -26,17 +26,14 @@ module.exports = UserController =
|
||||||
UserController.sendFormattedPersonalInfo(user, res, next)
|
UserController.sendFormattedPersonalInfo(user, res, next)
|
||||||
|
|
||||||
sendFormattedPersonalInfo: (user, res, next = (error) ->) ->
|
sendFormattedPersonalInfo: (user, res, next = (error) ->) ->
|
||||||
UserController._formatPersonalInfo user, (error, info) ->
|
info = UserController.formatPersonalInfo(user)
|
||||||
return next(error) if error?
|
res.send JSON.stringify(info)
|
||||||
res.send JSON.stringify(info)
|
|
||||||
|
|
||||||
_formatPersonalInfo: (user, callback = (error, info) ->) ->
|
formatPersonalInfo: (user, callback = (error, info) ->) ->
|
||||||
callback null, {
|
if !user?
|
||||||
id: user._id.toString()
|
return {}
|
||||||
first_name: user.first_name
|
formatted_user = { id: user._id.toString() }
|
||||||
last_name: user.last_name
|
for key in ["first_name", "last_name", "email", "signUpDate", "role", "institution"]
|
||||||
email: user.email
|
if user[key]?
|
||||||
signUpDate: user.signUpDate
|
formatted_user[key] = user[key]
|
||||||
role: user.role
|
return formatted_user
|
||||||
institution: user.institution
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
UserGetter = require "./UserGetter"
|
||||||
|
|
||||||
|
module.exports = UserInfoManager =
|
||||||
|
getPersonalInfo: (user_id, callback = (error) ->) ->
|
||||||
|
UserGetter.getUser user_id, { _id: true, first_name: true, last_name: true, email: true }, callback
|
|
@ -1,5 +1,4 @@
|
||||||
Settings = require('settings-sharelatex')
|
Settings = require('settings-sharelatex')
|
||||||
redis = require('redis-sharelatex')
|
|
||||||
logger = require("logger-sharelatex")
|
logger = require("logger-sharelatex")
|
||||||
Async = require('async')
|
Async = require('async')
|
||||||
_ = require('underscore')
|
_ = require('underscore')
|
||||||
|
|
|
@ -189,6 +189,7 @@ module.exports = (app, webRouter, apiRouter)->
|
||||||
return AuthenticationController.isUserLoggedIn(req)
|
return AuthenticationController.isUserLoggedIn(req)
|
||||||
res.locals.getSessionUser = ->
|
res.locals.getSessionUser = ->
|
||||||
return AuthenticationController.getSessionUser(req)
|
return AuthenticationController.getSessionUser(req)
|
||||||
|
|
||||||
next()
|
next()
|
||||||
|
|
||||||
webRouter.use (req, res, next) ->
|
webRouter.use (req, res, next) ->
|
||||||
|
@ -244,6 +245,8 @@ module.exports = (app, webRouter, apiRouter)->
|
||||||
for key, value of Settings.nav
|
for key, value of Settings.nav
|
||||||
res.locals.nav[key] = _.clone(Settings.nav[key])
|
res.locals.nav[key] = _.clone(Settings.nav[key])
|
||||||
res.locals.templates = Settings.templateLinks
|
res.locals.templates = Settings.templateLinks
|
||||||
|
if res.locals.nav.header
|
||||||
|
console.error {}, "The `nav.header` setting is no longer supported, use `nav.header_extras` instead"
|
||||||
next()
|
next()
|
||||||
|
|
||||||
webRouter.use (req, res, next) ->
|
webRouter.use (req, res, next) ->
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
fs = require "fs"
|
fs = require "fs"
|
||||||
Path = require "path"
|
Path = require "path"
|
||||||
jade = require "jade"
|
pug = require "pug"
|
||||||
async = require "async"
|
async = require "async"
|
||||||
|
|
||||||
MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules")
|
MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules")
|
||||||
|
@ -29,7 +29,7 @@ module.exports = Modules =
|
||||||
for module in @modules
|
for module in @modules
|
||||||
for view, partial of module.viewIncludes or {}
|
for view, partial of module.viewIncludes or {}
|
||||||
@viewIncludes[view] ||= []
|
@viewIncludes[view] ||= []
|
||||||
@viewIncludes[view].push jade.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")), doctype: "html")
|
@viewIncludes[view].push pug.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")), doctype: "html")
|
||||||
|
|
||||||
moduleIncludes: (view, locals) ->
|
moduleIncludes: (view, locals) ->
|
||||||
compiledPartials = Modules.viewIncludes[view] or []
|
compiledPartials = Modules.viewIncludes[view] or []
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
settings = require("settings-sharelatex")
|
settings = require("settings-sharelatex")
|
||||||
redis = require("redis-sharelatex")
|
RedisWrapper = require('./RedisWrapper')
|
||||||
rclient = redis.createClient(settings.redis.web)
|
rclient = RedisWrapper.client('ratelimiter')
|
||||||
redback = require("redback").use(rclient)
|
RollingRateLimiter = require('rolling-rate-limiter')
|
||||||
|
|
||||||
module.exports =
|
|
||||||
|
|
||||||
addCount: (opts, callback = (opts, shouldProcess)->)->
|
module.exports = RateLimiter =
|
||||||
ratelimit = redback.createRateLimit(opts.endpointName)
|
|
||||||
ratelimit.addCount opts.subjectName, opts.timeInterval, (err, callCount)->
|
addCount: (opts, callback = (err, shouldProcess)->)->
|
||||||
shouldProcess = callCount < opts.throttle
|
namespace = "RateLimit:#{opts.endpointName}:"
|
||||||
callback(err, shouldProcess)
|
k = "{#{opts.subjectName}}"
|
||||||
|
limiter = RollingRateLimiter({
|
||||||
|
redis: rclient,
|
||||||
|
namespace: namespace,
|
||||||
|
interval: opts.timeInterval * 1000,
|
||||||
|
maxInInterval: opts.throttle
|
||||||
|
})
|
||||||
|
limiter k, (err, timeLeft, actionsLeft) ->
|
||||||
|
if err?
|
||||||
|
return callback(err)
|
||||||
|
allowed = timeLeft == 0
|
||||||
|
callback(null, allowed)
|
||||||
|
|
||||||
clearRateLimit: (endpointName, subject, callback) ->
|
clearRateLimit: (endpointName, subject, callback) ->
|
||||||
rclient.del "#{endpointName}:#{subject}", callback
|
# same as the key which will be built by RollingRateLimiter (namespace+k)
|
||||||
|
keyName = "RateLimit:#{endpointName}:{#{subject}}"
|
||||||
|
rclient.del keyName, callback
|
||||||
|
|
28
services/web/app/coffee/infrastructure/RedisWrapper.coffee
Normal file
28
services/web/app/coffee/infrastructure/RedisWrapper.coffee
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
Settings = require 'settings-sharelatex'
|
||||||
|
redis = require 'redis-sharelatex'
|
||||||
|
ioredis = require 'ioredis'
|
||||||
|
logger = require 'logger-sharelatex'
|
||||||
|
|
||||||
|
|
||||||
|
# A per-feature interface to Redis,
|
||||||
|
# looks up the feature in `settings.redis`
|
||||||
|
# and returns an appropriate client.
|
||||||
|
# Necessary because we don't want to migrate web over
|
||||||
|
# to redis-cluster all at once.
|
||||||
|
|
||||||
|
# TODO: consider merging into `redis-sharelatex`
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = Redis =
|
||||||
|
|
||||||
|
# feature = 'websessions' | 'ratelimiter' | ...
|
||||||
|
client: (feature) ->
|
||||||
|
redisFeatureSettings = Settings.redis[feature] or Settings.redis.web
|
||||||
|
if redisFeatureSettings?.cluster?
|
||||||
|
logger.log {feature}, "creating redis-cluster client"
|
||||||
|
rclient = new ioredis.Cluster(redisFeatureSettings.cluster)
|
||||||
|
rclient.__is_redis_cluster = true
|
||||||
|
else
|
||||||
|
logger.log {feature}, "creating redis client"
|
||||||
|
rclient = redis.createClient(redisFeatureSettings)
|
||||||
|
return rclient
|
|
@ -7,7 +7,6 @@ crawlerLogger = require('./CrawlerLogger')
|
||||||
expressLocals = require('./ExpressLocals')
|
expressLocals = require('./ExpressLocals')
|
||||||
Router = require('../router')
|
Router = require('../router')
|
||||||
metrics.inc("startup")
|
metrics.inc("startup")
|
||||||
redis = require("redis-sharelatex")
|
|
||||||
UserSessionsRedis = require('../Features/User/UserSessionsRedis')
|
UserSessionsRedis = require('../Features/User/UserSessionsRedis')
|
||||||
|
|
||||||
sessionsRedisClient = UserSessionsRedis.client()
|
sessionsRedisClient = UserSessionsRedis.client()
|
||||||
|
@ -62,7 +61,7 @@ if Settings.behindProxy
|
||||||
|
|
||||||
webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
|
webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
|
||||||
app.set 'views', __dirname + '/../../views'
|
app.set 'views', __dirname + '/../../views'
|
||||||
app.set 'view engine', 'jade'
|
app.set 'view engine', 'pug'
|
||||||
Modules.loadViewIncludes app
|
Modules.loadViewIncludes app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ ProjectSchema = new Schema
|
||||||
archived : { type: Boolean }
|
archived : { type: Boolean }
|
||||||
deletedDocs : [DeletedDocSchema]
|
deletedDocs : [DeletedDocSchema]
|
||||||
imageName : { type: String }
|
imageName : { type: String }
|
||||||
|
track_changes : { type: Boolean }
|
||||||
|
|
||||||
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
|
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
|
||||||
if project_or_id._id?
|
if project_or_id._id?
|
||||||
|
|
|
@ -2,7 +2,7 @@ Project = require('./Project').Project
|
||||||
Settings = require 'settings-sharelatex'
|
Settings = require 'settings-sharelatex'
|
||||||
_ = require('underscore')
|
_ = require('underscore')
|
||||||
mongoose = require('mongoose')
|
mongoose = require('mongoose')
|
||||||
uuid = require('node-uuid')
|
uuid = require('uuid')
|
||||||
Schema = mongoose.Schema
|
Schema = mongoose.Schema
|
||||||
ObjectId = Schema.ObjectId
|
ObjectId = Schema.ObjectId
|
||||||
|
|
||||||
|
@ -37,9 +37,10 @@ UserSchema = new Schema
|
||||||
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
|
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
|
||||||
templates: { type:Boolean, default: Settings.defaultFeatures.templates }
|
templates: { type:Boolean, default: Settings.defaultFeatures.templates }
|
||||||
references: { type:Boolean, default: Settings.defaultFeatures.references }
|
references: { type:Boolean, default: Settings.defaultFeatures.references }
|
||||||
|
trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
|
||||||
}
|
}
|
||||||
featureSwitches : {
|
featureSwitches : {
|
||||||
pdfng: { type: Boolean }
|
track_changes: { type: Boolean }
|
||||||
}
|
}
|
||||||
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
|
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
|
||||||
refered_users: [ type:ObjectId, ref:'User' ]
|
refered_users: [ type:ObjectId, ref:'User' ]
|
||||||
|
|
|
@ -25,7 +25,7 @@ ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")
|
||||||
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
|
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
|
||||||
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
|
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
|
||||||
FileStoreController = require("./Features/FileStore/FileStoreController")
|
FileStoreController = require("./Features/FileStore/FileStoreController")
|
||||||
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
|
HistoryController = require("./Features/History/HistoryController")
|
||||||
PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
|
PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
|
||||||
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
|
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
|
||||||
ChatController = require("./Features/Chat/ChatController")
|
ChatController = require("./Features/Chat/ChatController")
|
||||||
|
@ -40,6 +40,8 @@ AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlew
|
||||||
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
|
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
|
||||||
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
|
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
|
||||||
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
|
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
|
||||||
|
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
|
||||||
|
CommentsController = require "./Features/Comments/CommentsController"
|
||||||
|
|
||||||
logger = require("logger-sharelatex")
|
logger = require("logger-sharelatex")
|
||||||
_ = require("underscore")
|
_ = require("underscore")
|
||||||
|
@ -171,9 +173,14 @@ module.exports = class Router
|
||||||
|
|
||||||
webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject
|
webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject
|
||||||
|
|
||||||
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
|
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
|
||||||
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
|
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
|
||||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
|
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
|
||||||
|
|
||||||
|
webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllRanges
|
||||||
|
webRouter.get "/project/:project_id/changes/users", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllChangesUsers
|
||||||
|
webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.acceptChange
|
||||||
|
webRouter.post "/project/:project_id/track_changes", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.toggleTrackChanges
|
||||||
|
|
||||||
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
|
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
|
||||||
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
|
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
|
||||||
|
@ -223,8 +230,17 @@ module.exports = class Router
|
||||||
webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
|
webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
|
||||||
webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
|
webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
|
||||||
|
|
||||||
webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
|
webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
|
||||||
webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
|
webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
|
||||||
|
|
||||||
|
# Note: Read only users can still comment
|
||||||
|
webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.sendComment
|
||||||
|
webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads
|
||||||
|
webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread
|
||||||
|
webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread
|
||||||
|
webRouter.delete "/project/:project_id/doc/:doc_id/thread/:thread_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteThread
|
||||||
|
webRouter.post "/project/:project_id/thread/:thread_id/messages/:message_id/edit", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.editMessage
|
||||||
|
webRouter.delete "/project/:project_id/thread/:thread_id/messages/:message_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteMessage
|
||||||
|
|
||||||
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
|
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
|
||||||
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
|
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
|
||||||
|
|
|
@ -28,10 +28,10 @@ block content
|
||||||
tab(heading="Open Sockets")
|
tab(heading="Open Sockets")
|
||||||
.row-spaced
|
.row-spaced
|
||||||
ul
|
ul
|
||||||
-each agents, url in openSockets
|
each agents, url in openSockets
|
||||||
li #{url} - total : #{agents.length}
|
li #{url} - total : #{agents.length}
|
||||||
ul
|
ul
|
||||||
-each agent in agents
|
each agent in agents
|
||||||
li #{agent}
|
li #{agent}
|
||||||
|
|
||||||
tab(heading="Close Editor")
|
tab(heading="Close Editor")
|
|
@ -24,10 +24,10 @@ script(type='text/ng-template', id='supportModalTemplate')
|
||||||
a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
|
a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
|
||||||
span(ng-bind-html="suggestion.name")
|
span(ng-bind-html="suggestion.name")
|
||||||
i.fa.fa-angle-right
|
i.fa.fa-angle-right
|
||||||
label.desc(ng-show="'#{getUserEmail()}'.length < 1")
|
label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
|
||||||
| #{translate("email")}
|
| #{translate("email")}
|
||||||
.form-group(ng-show="'#{getUserEmail()}'.length < 1")
|
.form-group(ng-show="'"+getUserEmail()+"'.length < 1")
|
||||||
input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
|
input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '"+getUserEmail()+"'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
|
||||||
label#title12.desc
|
label#title12.desc
|
||||||
| #{translate("project_url")} (#{translate("optional")})
|
| #{translate("project_url")} (#{translate("optional")})
|
||||||
.form-group
|
.form-group
|
||||||
|
@ -37,6 +37,6 @@ script(type='text/ng-template', id='supportModalTemplate')
|
||||||
.form-group
|
.form-group
|
||||||
textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
|
textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
|
||||||
.form-group.text-center
|
.form-group.text-center
|
||||||
input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value='#{translate("contact_us")}')
|
input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value=translate("contact_us"))
|
||||||
span(ng-show="sent")
|
span(ng-show="sent")
|
||||||
p #{translate("request_sent_thank_you")}
|
p #{translate("request_sent_thank_you")}
|
|
@ -11,4 +11,4 @@ block content
|
||||||
| Sorry, ShareLaTeX is briefly down for maintenance.
|
| Sorry, ShareLaTeX is briefly down for maintenance.
|
||||||
| We should be back within minutes, but if not, or you have
|
| We should be back within minutes, but if not, or you have
|
||||||
| an urgent request, please contact us at
|
| an urgent request, please contact us at
|
||||||
| support@sharelatex.com
|
| #{settings.adminEmail}
|
|
@ -21,6 +21,8 @@ html(itemscope, itemtype='http://schema.org/Product')
|
||||||
link(rel="icon", href="/favicon.ico")
|
link(rel="icon", href="/favicon.ico")
|
||||||
link(rel='stylesheet', href=buildCssPath('/style.css'))
|
link(rel='stylesheet', href=buildCssPath('/style.css'))
|
||||||
|
|
||||||
|
block _headLinks
|
||||||
|
|
||||||
if settings.i18n.subdomainLang
|
if settings.i18n.subdomainLang
|
||||||
each subdomainDetails in settings.i18n.subdomainLang
|
each subdomainDetails in settings.i18n.subdomainLang
|
||||||
if !subdomainDetails.hide
|
if !subdomainDetails.hide
|
||||||
|
@ -30,7 +32,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
||||||
meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
|
meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
|
||||||
|
|
||||||
-if (typeof(meta) == "undefined")
|
-if (typeof(meta) == "undefined")
|
||||||
meta(itemprop="description", name="description", content='#{translate("site_description")}')
|
meta(itemprop="description", name="description", content=translate("site_description"))
|
||||||
-else
|
-else
|
||||||
meta(itemprop="description", name="description" , content=meta)
|
meta(itemprop="description", name="description" , content=meta)
|
||||||
|
|
|
@ -13,9 +13,9 @@ footer.site-footer
|
||||||
data-toggle="dropdown",
|
data-toggle="dropdown",
|
||||||
aria-haspopup="true",
|
aria-haspopup="true",
|
||||||
aria-expanded="false",
|
aria-expanded="false",
|
||||||
tooltip="#{translate('language')}"
|
tooltip=translate('language')
|
||||||
)
|
)
|
||||||
figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}")
|
figure(class="sprite-icon sprite-icon-lang sprite-icon-"+currentLngCode)
|
||||||
|
|
||||||
ul.dropdown-menu(role="menu")
|
ul.dropdown-menu(role="menu")
|
||||||
li.dropdown-header #{translate("language")}
|
li.dropdown-header #{translate("language")}
|
||||||
|
@ -23,7 +23,7 @@ footer.site-footer
|
||||||
if !subdomainDetails.hide
|
if !subdomainDetails.hide
|
||||||
li.lngOption
|
li.lngOption
|
||||||
a.menu-indent(href=subdomainDetails.url+currentUrl)
|
a.menu-indent(href=subdomainDetails.url+currentUrl)
|
||||||
figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}")
|
figure(class="sprite-icon sprite-icon-lang sprite-icon-"+subdomainDetails.lngCode)
|
||||||
| #{translate(subdomainDetails.lngCode)}
|
| #{translate(subdomainDetails.lngCode)}
|
||||||
//- img(src="/img/flags/24/.png")
|
//- img(src="/img/flags/24/.png")
|
||||||
each item in nav.left_footer
|
each item in nav.left_footer
|
|
@ -4,7 +4,7 @@ nav.navbar.navbar-default
|
||||||
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}")
|
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}")
|
||||||
i.fa.fa-bars
|
i.fa.fa-bars
|
||||||
if settings.nav.custom_logo
|
if settings.nav.custom_logo
|
||||||
a(href='/', style='background-image:url("#{settings.nav.custom_logo}")').navbar-brand
|
a(href='/', style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
|
||||||
else if (nav.title)
|
else if (nav.title)
|
||||||
a(href='/').navbar-title #{nav.title}
|
a(href='/').navbar-title #{nav.title}
|
||||||
else
|
else
|
||||||
|
@ -24,7 +24,10 @@ nav.navbar.navbar-default
|
||||||
li
|
li
|
||||||
a(href="/admin/user") Manage Users
|
a(href="/admin/user") Manage Users
|
||||||
|
|
||||||
each item in nav.header
|
|
||||||
|
// loop over header_extras
|
||||||
|
each item in nav.header_extras
|
||||||
|
|
||||||
if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
|
if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
|
||||||
if item.dropdown
|
if item.dropdown
|
||||||
li.dropdown(class=item.class, dropdown)
|
li.dropdown(class=item.class, dropdown)
|
||||||
|
@ -35,9 +38,6 @@ nav.navbar.navbar-default
|
||||||
each child in item.dropdown
|
each child in item.dropdown
|
||||||
if child.divider
|
if child.divider
|
||||||
li.divider
|
li.divider
|
||||||
else if child.user_email
|
|
||||||
li
|
|
||||||
div.subdued #{getUserEmail()}
|
|
||||||
else
|
else
|
||||||
li
|
li
|
||||||
if child.url
|
if child.url
|
||||||
|
@ -50,7 +50,35 @@ nav.navbar.navbar-default
|
||||||
a(href=item.url, class=item.class) !{translate(item.text)}
|
a(href=item.url, class=item.class) !{translate(item.text)}
|
||||||
else
|
else
|
||||||
| !{translate(item.text)}
|
| !{translate(item.text)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// logged out
|
||||||
|
if !getSessionUser()
|
||||||
|
// register link
|
||||||
|
if !externalAuthenticationSystemUsed()
|
||||||
|
li
|
||||||
|
a(href="/register") #{translate('register')}
|
||||||
|
|
||||||
|
// login link
|
||||||
|
li
|
||||||
|
a(href="/login") #{translate('log_in')}
|
||||||
|
|
||||||
|
// projects link and account menu
|
||||||
|
if getSessionUser()
|
||||||
|
li
|
||||||
|
a(href="/project") #{translate('Projects')}
|
||||||
|
li.dropdown(dropdown)
|
||||||
|
a.dropbodw-toggle(href, dropdown-toggle)
|
||||||
|
| #{translate('Account')}
|
||||||
|
b.caret
|
||||||
|
ul.dropdown-menu
|
||||||
|
li
|
||||||
|
div.subdued #{getUserEmail()}
|
||||||
|
li.divider
|
||||||
|
li
|
||||||
|
a(href="/user/settings") #{translate('Account Settings')}
|
||||||
|
if nav.showSubscriptionLink
|
||||||
|
li
|
||||||
|
a(href="/user/subscription") #{translate('subscription')}
|
||||||
|
li.divider
|
||||||
|
li
|
||||||
|
a(href="/logout") #{translate('log_out')}
|
|
@ -107,6 +107,7 @@ block requirejs
|
||||||
window.csrfToken = "!{csrfToken}";
|
window.csrfToken = "!{csrfToken}";
|
||||||
window.anonymous = #{anonymous};
|
window.anonymous = #{anonymous};
|
||||||
window.maxDocLength = #{maxDocLength};
|
window.maxDocLength = #{maxDocLength};
|
||||||
|
window.trackChangesEnabled = #{trackChangesEnabled};
|
||||||
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
|
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
|
||||||
window.requirejs = {
|
window.requirejs = {
|
||||||
"paths" : {
|
"paths" : {
|
|
@ -50,7 +50,7 @@ aside.chat(
|
||||||
|
|
||||||
.new-message
|
.new-message
|
||||||
textarea(
|
textarea(
|
||||||
placeholder="#{translate('your_message')}...",
|
placeholder=translate('your_message')+"...",
|
||||||
on-enter="sendMessage()",
|
on-enter="sendMessage()",
|
||||||
ng-model="newMessageContent",
|
ng-model="newMessageContent",
|
||||||
ng-click="resetUnreadMessages()"
|
ng-click="resetUnreadMessages()"
|
|
@ -17,7 +17,9 @@ div.full-size(
|
||||||
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
|
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
|
||||||
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
|
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
|
||||||
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
|
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
|
||||||
'rp-size-expanded': ui.reviewPanelOpen\
|
'rp-size-expanded': ui.reviewPanelOpen,\
|
||||||
|
'rp-layout-left': reviewPanel.layoutToLeft,\
|
||||||
|
'rp-loading-threads': reviewPanel.loadingThreads\
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
|
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
|
||||||
|
@ -51,12 +53,12 @@ div.full-size(
|
||||||
syntax-validation="settings.syntaxValidation",
|
syntax-validation="settings.syntaxValidation",
|
||||||
review-panel="reviewPanel",
|
review-panel="reviewPanel",
|
||||||
events-bridge="reviewPanelEventsBridge"
|
events-bridge="reviewPanelEventsBridge"
|
||||||
track-changes-enabled="trackChangesFeatureFlag",
|
track-changes-enabled="project.features.trackChangesVisible",
|
||||||
track-new-changes= "reviewPanel.trackNewChanges",
|
track-changes= "editor.trackChanges",
|
||||||
changes-tracker="reviewPanel.changesTracker",
|
|
||||||
doc-id="editor.open_doc_id"
|
doc-id="editor.open_doc_id"
|
||||||
|
renderer-data="reviewPanel.rendererData"
|
||||||
)
|
)
|
||||||
|
|
||||||
include ./review-panel
|
include ./review-panel
|
||||||
|
|
||||||
.ui-layout-east
|
.ui-layout-east
|
||||||
|
@ -68,7 +70,7 @@ div.full-size(
|
||||||
ng-controller="PdfSynctexController"
|
ng-controller="PdfSynctexController"
|
||||||
)
|
)
|
||||||
a.btn.btn-default.btn-xs(
|
a.btn.btn-default.btn-xs(
|
||||||
tooltip="#{translate('go_to_code_location_in_pdf')}"
|
tooltip=translate('go_to_code_location_in_pdf')
|
||||||
tooltip-placement="right"
|
tooltip-placement="right"
|
||||||
tooltip-append-to-body="true"
|
tooltip-append-to-body="true"
|
||||||
ng-click="syncToPdf()"
|
ng-click="syncToPdf()"
|
||||||
|
@ -76,7 +78,7 @@ div.full-size(
|
||||||
i.fa.fa-long-arrow-right
|
i.fa.fa-long-arrow-right
|
||||||
br
|
br
|
||||||
a.btn.btn-default.btn-xs(
|
a.btn.btn-default.btn-xs(
|
||||||
tooltip-html="'#{translate('go_to_pdf_location_in_code')}'"
|
tooltip-html="'"+translate('go_to_pdf_location_in_code')+"'"
|
||||||
tooltip-placement="right"
|
tooltip-placement="right"
|
||||||
tooltip-append-to-body="true"
|
tooltip-append-to-body="true"
|
||||||
ng-click="syncToCode()"
|
ng-click="syncToCode()"
|
||||||
|
@ -88,4 +90,4 @@ div.full-size(
|
||||||
ng-show="ui.view == 'pdf'"
|
ng-show="ui.view == 'pdf'"
|
||||||
)
|
)
|
||||||
include ./pdf
|
include ./pdf
|
||||||
|
|
|
@ -3,21 +3,21 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
|
||||||
a(
|
a(
|
||||||
href,
|
href,
|
||||||
ng-click="openNewDocModal()",
|
ng-click="openNewDocModal()",
|
||||||
tooltip-html="'#{translate('new_file').replace(' ', '<br>')}'",
|
tooltip-html="'"+translate('new_file').replace(' ', '<br>')+"'",
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
)
|
)
|
||||||
i.fa.fa-file
|
i.fa.fa-file
|
||||||
a(
|
a(
|
||||||
href,
|
href,
|
||||||
ng-click="openNewFolderModal()",
|
ng-click="openNewFolderModal()",
|
||||||
tooltip-html="'#{translate('new_folder').replace(' ', '<br>')}'",
|
tooltip-html="'"+translate('new_folder').replace(' ', '<br>')+"'",
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
)
|
)
|
||||||
i.fa.fa-folder
|
i.fa.fa-folder
|
||||||
a(
|
a(
|
||||||
href,
|
href,
|
||||||
ng-click="openUploadFileModal()",
|
ng-click="openUploadFileModal()",
|
||||||
tooltip="#{translate('upload')}",
|
tooltip=translate('upload'),
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
)
|
)
|
||||||
i.fa.fa-upload
|
i.fa.fa-upload
|
||||||
|
@ -26,7 +26,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
|
||||||
a(
|
a(
|
||||||
href,
|
href,
|
||||||
ng-click="startRenamingSelected()",
|
ng-click="startRenamingSelected()",
|
||||||
tooltip="#{translate('rename')}",
|
tooltip=translate('rename'),
|
||||||
tooltip-placement="bottom",
|
tooltip-placement="bottom",
|
||||||
ng-show="multiSelectedCount == 0"
|
ng-show="multiSelectedCount == 0"
|
||||||
)
|
)
|
||||||
|
@ -34,7 +34,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
|
||||||
a(
|
a(
|
||||||
href,
|
href,
|
||||||
ng-click="openDeleteModalForSelected()",
|
ng-click="openDeleteModalForSelected()",
|
||||||
tooltip="#{translate('delete')}",
|
tooltip=translate('delete'),
|
||||||
tooltip-placement="bottom",
|
tooltip-placement="bottom",
|
||||||
tooltip-append-to-body="true"
|
tooltip-append-to-body="true"
|
||||||
)
|
)
|
||||||
|
@ -431,4 +431,4 @@ script(type='text/ng-template', id='invalidFileNameModalTemplate')
|
||||||
.modal-footer
|
.modal-footer
|
||||||
button.btn.btn-default(
|
button.btn.btn-default(
|
||||||
ng-click="$close()"
|
ng-click="$close()"
|
||||||
) #{translate('ok')}
|
) #{translate('ok')}
|
|
@ -45,7 +45,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
||||||
ng-if="permissions.admin",
|
ng-if="permissions.admin",
|
||||||
href='#',
|
href='#',
|
||||||
tooltip-placement="bottom",
|
tooltip-placement="bottom",
|
||||||
tooltip="#{translate('rename')}",
|
tooltip=translate('rename'),
|
||||||
tooltip-append-to-body="true",
|
tooltip-append-to-body="true",
|
||||||
ng-click="startRenaming()",
|
ng-click="startRenaming()",
|
||||||
ng-show="!state.renaming"
|
ng-show="!state.renaming"
|
||||||
|
@ -71,7 +71,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
||||||
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
|
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
|
||||||
span.online-user.online-user-multi(
|
span.online-user.online-user-multi(
|
||||||
dropdown-toggle,
|
dropdown-toggle,
|
||||||
tooltip="#{translate('connected_users')}",
|
tooltip=translate('connected_users'),
|
||||||
tooltip-placement="left"
|
tooltip-placement="left"
|
||||||
)
|
)
|
||||||
strong {{ onlineUsersArray.length }}
|
strong {{ onlineUsersArray.length }}
|
||||||
|
@ -87,7 +87,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
||||||
|
|
||||||
a.btn.btn-full-height(
|
a.btn.btn-full-height(
|
||||||
href,
|
href,
|
||||||
ng-if="trackChangesFeatureFlag",
|
ng-if="project.features.trackChangesVisible",
|
||||||
ng-class="{ active: ui.reviewPanelOpen }"
|
ng-class="{ active: ui.reviewPanelOpen }"
|
||||||
ng-click="toggleReviewPanel()"
|
ng-click="toggleReviewPanel()"
|
||||||
)
|
)
|
||||||
|
@ -121,4 +121,4 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
||||||
span.label.label-info(
|
span.label.label-info(
|
||||||
ng-show="unreadMessages > 0"
|
ng-show="unreadMessages > 0"
|
||||||
) {{ unreadMessages }}
|
) {{ unreadMessages }}
|
||||||
p.toolbar-label #{translate("chat")}
|
p.toolbar-label #{translate("chat")}
|
|
@ -24,7 +24,7 @@ aside#left-menu.full-size(
|
||||||
| PDF
|
| PDF
|
||||||
div.link-disabled(
|
div.link-disabled(
|
||||||
ng-if="!pdf.url"
|
ng-if="!pdf.url"
|
||||||
tooltip="#{translate('please_compile_pdf_before_download')}"
|
tooltip=translate('please_compile_pdf_before_download')
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
)
|
)
|
||||||
i.fa.fa-file-pdf-o.fa-2x
|
i.fa.fa-file-pdf-o.fa-2x
|
||||||
|
@ -47,7 +47,7 @@ aside#left-menu.full-size(
|
||||||
a(href, ng-if="pdf.url" ,ng-click="openWordCountModal()")
|
a(href, ng-if="pdf.url" ,ng-click="openWordCountModal()")
|
||||||
i.fa.fa-fw.fa-eye
|
i.fa.fa-fw.fa-eye
|
||||||
span #{translate("word_count")}
|
span #{translate("word_count")}
|
||||||
a.link-disabled(href, ng-if="!pdf.url" , tooltip="#{translate('please_compile_pdf_before_word_count')}")
|
a.link-disabled(href, ng-if="!pdf.url" , tooltip=translate('please_compile_pdf_before_word_count'))
|
||||||
i.fa.fa-fw.fa-eye
|
i.fa.fa-fw.fa-eye
|
||||||
span.link-disabled #{translate("word_count")}
|
span.link-disabled #{translate("word_count")}
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ script(type='text/ng-template', id='wordCountModalTemplate')
|
||||||
span #{translate("loading")}...
|
span #{translate("loading")}...
|
||||||
div.pdf-disabled(
|
div.pdf-disabled(
|
||||||
ng-if="!pdf.url"
|
ng-if="!pdf.url"
|
||||||
tooltip="#{translate('please_compile_pdf_before_word_count')}"
|
tooltip=translate('please_compile_pdf_before_word_count')
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
)
|
)
|
||||||
div(ng-if="!status.loading")
|
div(ng-if="!status.loading")
|
|
@ -2,7 +2,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
.toolbar.toolbar-tall
|
.toolbar.toolbar-tall
|
||||||
.btn-group(
|
.btn-group(
|
||||||
dropdown,
|
dropdown,
|
||||||
tooltip-html="'#{translate('recompile_pdf')} <span class=\"keyboard-shortcut\">({{modifierKey}} + Enter)</span>'"
|
tooltip-html="'"+translate('recompile_pdf')+" <span class=\"keyboard-shortcut\">({{modifierKey}} + Enter)</span>'"
|
||||||
tooltip-class="keyboard-tooltip"
|
tooltip-class="keyboard-tooltip"
|
||||||
tooltip-popup-delay="500"
|
tooltip-popup-delay="500"
|
||||||
tooltip-append-to-body="true"
|
tooltip-append-to-body="true"
|
||||||
|
@ -53,7 +53,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
href
|
href
|
||||||
ng-click="stop()"
|
ng-click="stop()"
|
||||||
ng-show="pdf.compiling",
|
ng-show="pdf.compiling",
|
||||||
tooltip="#{translate('stop_compile')}"
|
tooltip=translate('stop_compile')
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
)
|
)
|
||||||
i.fa.fa-stop()
|
i.fa.fa-stop()
|
||||||
|
@ -61,7 +61,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
href
|
href
|
||||||
ng-click="toggleLogs()"
|
ng-click="toggleLogs()"
|
||||||
ng-class="{ 'active': shouldShowLogs == true }"
|
ng-class="{ 'active': shouldShowLogs == true }"
|
||||||
tooltip="#{translate('logs_and_output_files')}"
|
tooltip=translate('logs_and_output_files')
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
)
|
)
|
||||||
i.fa.fa-file-text-o
|
i.fa.fa-file-text-o
|
||||||
|
@ -77,7 +77,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
ng-href="{{pdf.downloadUrl || pdf.url}}"
|
ng-href="{{pdf.downloadUrl || pdf.url}}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
ng-if="pdf.url"
|
ng-if="pdf.url"
|
||||||
tooltip="#{translate('download_pdf')}"
|
tooltip=translate('download_pdf')
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
)
|
)
|
||||||
i.fa.fa-download
|
i.fa.fa-download
|
||||||
|
@ -87,7 +87,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
href,
|
href,
|
||||||
ng-click="switchToFlatLayout()"
|
ng-click="switchToFlatLayout()"
|
||||||
ng-show="ui.pdfLayout == 'sideBySide'"
|
ng-show="ui.pdfLayout == 'sideBySide'"
|
||||||
tooltip="#{translate('full_screen')}"
|
tooltip=translate('full_screen')
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
tooltip-append-to-body="true"
|
tooltip-append-to-body="true"
|
||||||
)
|
)
|
||||||
|
@ -96,7 +96,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
href,
|
href,
|
||||||
ng-click="switchToSideBySideLayout()"
|
ng-click="switchToSideBySideLayout()"
|
||||||
ng-show="ui.pdfLayout == 'flat'"
|
ng-show="ui.pdfLayout == 'flat'"
|
||||||
tooltip="#{translate('split_screen')}"
|
tooltip=translate('split_screen')
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
tooltip-append-to-body="true"
|
tooltip-append-to-body="true"
|
||||||
)
|
)
|
||||||
|
@ -233,7 +233,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
.files-dropdown-container
|
.files-dropdown-container
|
||||||
a.btn.btn-default.btn-sm(
|
a.btn.btn-default.btn-sm(
|
||||||
href,
|
href,
|
||||||
tooltip="#{translate('clear_cached_files')}",
|
tooltip=translate('clear_cached_files'),
|
||||||
tooltip-placement="top",
|
tooltip-placement="top",
|
||||||
tooltip-append-to-body="true",
|
tooltip-append-to-body="true",
|
||||||
ng-click="openClearCacheModal()"
|
ng-click="openClearCacheModal()"
|
|
@ -1,219 +0,0 @@
|
||||||
#review-panel
|
|
||||||
.review-panel-toolbar
|
|
||||||
span.review-panel-toolbar-label(ng-click="reviewPanel.trackNewChanges = true;", ng-if="reviewPanel.trackNewChanges === false") Track Changes is
|
|
||||||
strong off
|
|
||||||
span.review-panel-toolbar-label(ng-click="reviewPanel.trackNewChanges = false;", ng-if="reviewPanel.trackNewChanges === true") Track Changes is
|
|
||||||
strong on
|
|
||||||
review-panel-toggle(ng-model="reviewPanel.trackNewChanges")
|
|
||||||
|
|
||||||
.rp-entry-list(
|
|
||||||
review-panel-sorted
|
|
||||||
ng-if="reviewPanel.subView === SubViews.CUR_FILE"
|
|
||||||
)
|
|
||||||
.rp-entry-list-inner
|
|
||||||
.rp-entry-wrapper(
|
|
||||||
ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]"
|
|
||||||
)
|
|
||||||
div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
|
|
||||||
change-entry(
|
|
||||||
entry="entry"
|
|
||||||
user="users[entry.metadata.user_id]"
|
|
||||||
on-reject="rejectChange(entry_id);"
|
|
||||||
on-accept="acceptChange(entry_id);"
|
|
||||||
on-indicator-click="toggleReviewPanel();"
|
|
||||||
)
|
|
||||||
|
|
||||||
div(ng-if="entry.type === 'comment'")
|
|
||||||
comment-entry(
|
|
||||||
entry="entry"
|
|
||||||
users="users"
|
|
||||||
on-resolve="resolveComment(entry, entry_id)"
|
|
||||||
on-unresolve="unresolveComment(entry_id)"
|
|
||||||
on-show-thread="showThread(entry)"
|
|
||||||
on-hide-thread="hideThread(entry)"
|
|
||||||
on-delete="deleteComment(entry_id)"
|
|
||||||
on-reply="submitReply(entry, entry_id);"
|
|
||||||
on-indicator-click="toggleReviewPanel();"
|
|
||||||
)
|
|
||||||
|
|
||||||
div(ng-if="entry.type === 'add-comment'")
|
|
||||||
add-comment-entry(
|
|
||||||
on-start-new="startNewComment();"
|
|
||||||
on-submit="submitNewComment(content);"
|
|
||||||
on-cancel="cancelNewComment();"
|
|
||||||
on-indicator-click="toggleReviewPanel();"
|
|
||||||
)
|
|
||||||
|
|
||||||
.rp-entry-list(
|
|
||||||
ng-if="reviewPanel.subView === SubViews.OVERVIEW"
|
|
||||||
)
|
|
||||||
.rp-overview-file(
|
|
||||||
ng-repeat="(doc_id, entries) in reviewPanel.entries"
|
|
||||||
)
|
|
||||||
.rp-overview-file-header
|
|
||||||
| {{ getFileName(doc_id) }}
|
|
||||||
.rp-entry-wrapper(
|
|
||||||
ng-repeat="(entry_id, entry) in entries | orderOverviewEntries"
|
|
||||||
)
|
|
||||||
div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
|
|
||||||
change-entry(
|
|
||||||
entry="entry"
|
|
||||||
user="users[entry.metadata.user_id]"
|
|
||||||
on-reject="rejectChange(entry.id);"
|
|
||||||
on-accept="acceptChange(entry.id);"
|
|
||||||
on-indicator-click="toggleReviewPanel();"
|
|
||||||
ng-click="gotoEntry(doc_id, entry)"
|
|
||||||
)
|
|
||||||
|
|
||||||
div(ng-if="entry.type === 'comment'")
|
|
||||||
comment-entry(
|
|
||||||
entry="entry"
|
|
||||||
users="users"
|
|
||||||
on-resolve="resolveComment(entry, entry.id)"
|
|
||||||
on-unresolve="unresolveComment(entry.id)"
|
|
||||||
on-delete="deleteComment(entry.id)"
|
|
||||||
on-reply="submitReply(entry, entry_id);"
|
|
||||||
on-indicator-click="toggleReviewPanel();"
|
|
||||||
ng-click="gotoEntry(doc_id, entry)"
|
|
||||||
)
|
|
||||||
|
|
||||||
.rp-nav
|
|
||||||
a.rp-nav-item(
|
|
||||||
href
|
|
||||||
ng-click="setSubView(SubViews.CUR_FILE);"
|
|
||||||
ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.CUR_FILE }"
|
|
||||||
)
|
|
||||||
i.fa.fa-file-text-o
|
|
||||||
span.rp-nav-label Current file
|
|
||||||
a.rp-nav-item(
|
|
||||||
href
|
|
||||||
ng-click="setSubView(SubViews.OVERVIEW);"
|
|
||||||
ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.OVERVIEW }"
|
|
||||||
)
|
|
||||||
i.fa.fa-list
|
|
||||||
span.rp-nav-label Overview
|
|
||||||
|
|
||||||
|
|
||||||
script(type='text/ng-template', id='changeEntryTemplate')
|
|
||||||
div
|
|
||||||
.rp-entry-callout(
|
|
||||||
ng-class="'rp-entry-callout-' + entry.type"
|
|
||||||
)
|
|
||||||
.rp-entry-indicator(
|
|
||||||
ng-switch="entry.type"
|
|
||||||
ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
|
|
||||||
ng-click="onIndicatorClick();"
|
|
||||||
)
|
|
||||||
i.fa.fa-pencil(ng-switch-when="insert")
|
|
||||||
i.rp-icon-delete(ng-switch-when="delete")
|
|
||||||
.rp-entry(
|
|
||||||
ng-class="[ 'rp-entry-' + entry.type, (entry.focused ? 'rp-entry-focused' : '')]"
|
|
||||||
)
|
|
||||||
.rp-entry-header
|
|
||||||
.rp-entry-action-icon(ng-switch="entry.type")
|
|
||||||
i.fa.fa-pencil(ng-switch-when="insert")
|
|
||||||
i.rp-icon-delete(ng-switch-when="delete")
|
|
||||||
.rp-entry-metadata
|
|
||||||
p.rp-entry-metadata-line(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }}
|
|
||||||
p.rp-entry-metadata-line {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }}
|
|
||||||
.rp-avatar(style="background-color: hsl({{ user.hue }}, 70%, 50%);") {{ user.avatar_text | limitTo : 1 }}
|
|
||||||
.rp-entry-body(ng-switch="entry.type")
|
|
||||||
span(ng-switch-when="insert") Added
|
|
||||||
ins.rp-content-highlight {{ entry.content }}
|
|
||||||
span(ng-switch-when="delete") Deleted
|
|
||||||
del.rp-content-highlight {{ entry.content }}
|
|
||||||
.rp-entry-actions
|
|
||||||
a.rp-entry-button(href, ng-click="onReject();")
|
|
||||||
i.fa.fa-times
|
|
||||||
| Reject
|
|
||||||
a.rp-entry-button(href, ng-click="onAccept();")
|
|
||||||
i.fa.fa-check
|
|
||||||
| Accept
|
|
||||||
|
|
||||||
script(type='text/ng-template', id='commentEntryTemplate')
|
|
||||||
div
|
|
||||||
.rp-entry-callout.rp-entry-callout-comment(ng-if="!entry.resolved")
|
|
||||||
.rp-entry-indicator(
|
|
||||||
ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
|
|
||||||
ng-click="onIndicatorClick();"
|
|
||||||
)
|
|
||||||
i.fa.fa-comment
|
|
||||||
.rp-entry.rp-entry-comment(
|
|
||||||
ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolved': entry.resolved}"
|
|
||||||
)
|
|
||||||
.rp-comment(
|
|
||||||
ng-if="!entry.resolved || entry.showWhenResolved"
|
|
||||||
ng-repeat="comment in entry.thread"
|
|
||||||
ng-class="users[comment.user_id].isSelf ? 'rp-comment-self' : '';"
|
|
||||||
)
|
|
||||||
.rp-avatar(
|
|
||||||
ng-if="!users[comment.user_id].isSelf;"
|
|
||||||
style="background-color: hsl({{ users[comment.user_id].hue }}, 70%, 50%);"
|
|
||||||
) {{ users[comment.user_id].avatar_text | limitTo : 1 }}
|
|
||||||
.rp-comment-body(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 90%);")
|
|
||||||
p.rp-comment-content {{ comment.content }}
|
|
||||||
p.rp-comment-metadata
|
|
||||||
| {{ comment.ts | date : 'MMM d, y h:mm a' }}
|
|
||||||
| •
|
|
||||||
span(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 40%);") {{ users[comment.user_id].name }}
|
|
||||||
.rp-comment-reply(ng-if="!entry.resolved || entry.showWhenResolved")
|
|
||||||
textarea.rp-comment-input(
|
|
||||||
ng-model="entry.replyContent"
|
|
||||||
ng-keypress="handleCommentReplyKeyPress($event);"
|
|
||||||
stop-propagation="click"
|
|
||||||
placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}"
|
|
||||||
)
|
|
||||||
.rp-comment-resolved-description(ng-if="entry.resolved && !entry.showWhenResolved")
|
|
||||||
div
|
|
||||||
| Comment resolved by
|
|
||||||
span(style="color: hsl({{ users[entry.resolved_data.user_id].hue }}, 70%, 40%);") {{ users[entry.resolved_data.user_id].name }}
|
|
||||||
div {{ entry.resolved_data.ts | date : 'MMM d, y h:mm a' }}
|
|
||||||
.rp-entry-actions
|
|
||||||
a.rp-entry-button(href, ng-click="onResolve();", ng-if="!entry.resolved")
|
|
||||||
i.fa.fa-check
|
|
||||||
| Mark as resolved
|
|
||||||
a.rp-entry-button(href, ng-click="onShowThread();", ng-if="entry.resolved && !entry.showWhenResolved")
|
|
||||||
| Show
|
|
||||||
a.rp-entry-button(href, ng-click="onHideThread();", ng-if="entry.resolved && entry.showWhenResolved")
|
|
||||||
| Hide
|
|
||||||
a.rp-entry-button(href, ng-click="onUnresolve();", ng-if="entry.resolved")
|
|
||||||
| Re-open
|
|
||||||
a.rp-entry-button(href, ng-click="onDelete();", ng-if="entry.resolved")
|
|
||||||
| Delete
|
|
||||||
|
|
||||||
|
|
||||||
script(type='text/ng-template', id='addCommentEntryTemplate')
|
|
||||||
div
|
|
||||||
.rp-entry-callout.rp-entry-callout-add-comment
|
|
||||||
.rp-entry-indicator(
|
|
||||||
ng-if="!commentState.adding"
|
|
||||||
ng-click="startNewComment(); onIndicatorClick();"
|
|
||||||
tooltip="Add a comment"
|
|
||||||
tooltip-placement="right"
|
|
||||||
tooltip-append-to-body="true"
|
|
||||||
)
|
|
||||||
i.fa.fa-commenting
|
|
||||||
.rp-entry.rp-entry-add-comment(
|
|
||||||
ng-class="[ (state.isAdding ? 'rp-entry-adding-comment' : ''), (entry.focused ? 'rp-entry-focused' : '')]"
|
|
||||||
)
|
|
||||||
a.rp-add-comment-btn(
|
|
||||||
href
|
|
||||||
ng-if="!state.isAdding"
|
|
||||||
ng-click="startNewComment();"
|
|
||||||
)
|
|
||||||
i.fa.fa-comment
|
|
||||||
| Add comment
|
|
||||||
div(ng-if="state.isAdding")
|
|
||||||
.rp-new-comment
|
|
||||||
textarea.rp-comment-input(
|
|
||||||
ng-model="state.content"
|
|
||||||
ng-keypress="handleCommentKeyPress($event);"
|
|
||||||
placeholder="Add your comment here"
|
|
||||||
)
|
|
||||||
.rp-entry-actions
|
|
||||||
a.rp-entry-button(href, ng-click="cancelNewComment();")
|
|
||||||
i.fa.fa-times
|
|
||||||
| Cancel
|
|
||||||
a.rp-entry-button(href, ng-click="submitNewComment()")
|
|
||||||
i.fa.fa-comment
|
|
||||||
| Comment
|
|
426
services/web/app/views/project/editor/review-panel.pug
Normal file
426
services/web/app/views/project/editor/review-panel.pug
Normal file
|
@ -0,0 +1,426 @@
|
||||||
|
#review-panel
|
||||||
|
a.rp-track-changes-indicator(
|
||||||
|
href
|
||||||
|
ng-if="editor.wantTrackChanges"
|
||||||
|
ng-click="toggleReviewPanel();"
|
||||||
|
ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
|
||||||
|
) Track changes is
|
||||||
|
strong on
|
||||||
|
|
||||||
|
.review-panel-toolbar
|
||||||
|
resolved-comments-dropdown(
|
||||||
|
class="rp-flex-block"
|
||||||
|
entries="reviewPanel.resolvedComments"
|
||||||
|
threads="reviewPanel.commentThreads"
|
||||||
|
resolved-ids="reviewPanel.resolvedThreadIds"
|
||||||
|
docs="docs"
|
||||||
|
on-open="refreshResolvedCommentsDropdown();"
|
||||||
|
on-unresolve="unresolveComment(threadId);"
|
||||||
|
on-delete="deleteThread(entryId, docId, threadId);"
|
||||||
|
is-loading="reviewPanel.dropdown.loading"
|
||||||
|
permissions="permissions"
|
||||||
|
)
|
||||||
|
span.review-panel-toolbar-label(ng-if="permissions.write")
|
||||||
|
span(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") Track Changes is
|
||||||
|
strong off
|
||||||
|
span(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") Track Changes is
|
||||||
|
strong on
|
||||||
|
review-panel-toggle(
|
||||||
|
ng-if="editor.wantTrackChanges == editor.trackChanges"
|
||||||
|
ng-model="editor.wantTrackChanges"
|
||||||
|
on-toggle="toggleTrackChanges"
|
||||||
|
disabled="!project.features.trackChanges"
|
||||||
|
on-disabled-click="openTrackChangesUpgradeModal"
|
||||||
|
)
|
||||||
|
span.review-panel-toolbar-label.review-panel-toolbar-label-disabled(ng-if="!permissions.write")
|
||||||
|
span(ng-if="editor.wantTrackChanges === false") Track Changes is
|
||||||
|
strong off
|
||||||
|
span(ng-if="editor.wantTrackChanges === true") Track Changes is
|
||||||
|
strong on
|
||||||
|
span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges")
|
||||||
|
i.fa.fa-spin.fa-spinner
|
||||||
|
|
||||||
|
.rp-entry-list(
|
||||||
|
review-panel-sorted
|
||||||
|
ng-if="reviewPanel.subView === SubViews.CUR_FILE"
|
||||||
|
)
|
||||||
|
.rp-entry-list-inner
|
||||||
|
.rp-entry-wrapper(
|
||||||
|
ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]"
|
||||||
|
)
|
||||||
|
div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
|
||||||
|
change-entry(
|
||||||
|
entry="entry"
|
||||||
|
user="users[entry.metadata.user_id]"
|
||||||
|
on-reject="rejectChange(entry_id);"
|
||||||
|
on-accept="acceptChange(entry_id);"
|
||||||
|
on-indicator-click="toggleReviewPanel();"
|
||||||
|
on-body-click="gotoEntry(editor.open_doc_id, entry)"
|
||||||
|
permissions="permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
div(ng-if="entry.type === 'comment'")
|
||||||
|
comment-entry(
|
||||||
|
entry="entry"
|
||||||
|
threads="reviewPanel.commentThreads"
|
||||||
|
on-resolve="resolveComment(entry, entry_id)"
|
||||||
|
on-reply="submitReply(entry, entry_id);"
|
||||||
|
on-indicator-click="toggleReviewPanel();"
|
||||||
|
on-save-edit="saveEdit(entry.thread_id, comment)"
|
||||||
|
on-delete="deleteComment(entry.thread_id, comment)"
|
||||||
|
on-body-click="gotoEntry(editor.open_doc_id, entry)"
|
||||||
|
permissions="permissions"
|
||||||
|
ng-if="!reviewPanel.loadingThreads"
|
||||||
|
)
|
||||||
|
|
||||||
|
div(ng-if="entry.type === 'add-comment' && permissions.comment")
|
||||||
|
add-comment-entry(
|
||||||
|
on-start-new="startNewComment();"
|
||||||
|
on-submit="submitNewComment(content);"
|
||||||
|
on-cancel="cancelNewComment();"
|
||||||
|
on-indicator-click="toggleReviewPanel();"
|
||||||
|
)
|
||||||
|
|
||||||
|
.rp-entry-list(
|
||||||
|
ng-if="reviewPanel.subView === SubViews.OVERVIEW"
|
||||||
|
)
|
||||||
|
.rp-loading(ng-if="reviewPanel.overview.loading")
|
||||||
|
i.fa.fa-spinner.fa-spin
|
||||||
|
.rp-overview-file(
|
||||||
|
ng-repeat="doc in docs"
|
||||||
|
ng-if="!reviewPanel.overview.loading"
|
||||||
|
)
|
||||||
|
.rp-overview-file-header(
|
||||||
|
ng-if="reviewPanel.entries[doc.doc.id] | notEmpty"
|
||||||
|
)
|
||||||
|
| {{ doc.path }}
|
||||||
|
.rp-entry-wrapper(
|
||||||
|
ng-repeat="(entry_id, entry) in reviewPanel.entries[doc.doc.id] | orderOverviewEntries"
|
||||||
|
ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)"
|
||||||
|
)
|
||||||
|
div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
|
||||||
|
change-entry(
|
||||||
|
entry="entry"
|
||||||
|
user="users[entry.metadata.user_id]"
|
||||||
|
on-indicator-click="toggleReviewPanel();"
|
||||||
|
ng-click="gotoEntry(doc.doc.id, entry)"
|
||||||
|
permissions="permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
div(ng-if="entry.type === 'comment'")
|
||||||
|
comment-entry(
|
||||||
|
entry="entry"
|
||||||
|
threads="reviewPanel.commentThreads"
|
||||||
|
on-reply="submitReply(entry, entry_id);"
|
||||||
|
on-save-edit="saveEdit(entry.thread_id, comment)"
|
||||||
|
on-delete="deleteComment(entry.thread_id, comment)"
|
||||||
|
on-indicator-click="toggleReviewPanel();"
|
||||||
|
ng-click="gotoEntry(doc.doc.id, entry)"
|
||||||
|
permissions="permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
.rp-nav
|
||||||
|
a.rp-nav-item(
|
||||||
|
href
|
||||||
|
ng-click="setSubView(SubViews.CUR_FILE);"
|
||||||
|
ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.CUR_FILE }"
|
||||||
|
)
|
||||||
|
i.fa.fa-file-text-o
|
||||||
|
span.rp-nav-label Current file
|
||||||
|
a.rp-nav-item(
|
||||||
|
href
|
||||||
|
ng-click="setSubView(SubViews.OVERVIEW);"
|
||||||
|
ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.OVERVIEW }"
|
||||||
|
)
|
||||||
|
i.fa.fa-list
|
||||||
|
span.rp-nav-label Overview
|
||||||
|
|
||||||
|
|
||||||
|
script(type='text/ng-template', id='changeEntryTemplate')
|
||||||
|
div
|
||||||
|
.rp-entry-callout(
|
||||||
|
ng-class="'rp-entry-callout-' + entry.type"
|
||||||
|
)
|
||||||
|
.rp-entry-indicator(
|
||||||
|
ng-switch="entry.type"
|
||||||
|
ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
|
||||||
|
ng-click="onIndicatorClick();"
|
||||||
|
)
|
||||||
|
i.fa.fa-pencil(ng-switch-when="insert")
|
||||||
|
i.rp-icon-delete(ng-switch-when="delete")
|
||||||
|
.rp-entry(
|
||||||
|
ng-class="[ 'rp-entry-' + entry.type, (entry.focused ? 'rp-entry-focused' : '')]"
|
||||||
|
)
|
||||||
|
.rp-entry-body
|
||||||
|
.rp-entry-action-icon(ng-switch="entry.type")
|
||||||
|
i.fa.fa-pencil(ng-switch-when="insert")
|
||||||
|
i.rp-icon-delete(ng-switch-when="delete")
|
||||||
|
.rp-entry-details
|
||||||
|
.rp-entry-description(ng-switch="entry.type")
|
||||||
|
span(ng-switch-when="insert") Added
|
||||||
|
ins.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }}
|
||||||
|
a.rp-collapse-toggle(
|
||||||
|
href
|
||||||
|
ng-if="needsCollapsing"
|
||||||
|
ng-click="toggleCollapse();"
|
||||||
|
) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
|
||||||
|
span(ng-switch-when="delete") Deleted
|
||||||
|
del.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }}
|
||||||
|
a.rp-collapse-toggle(
|
||||||
|
href
|
||||||
|
ng-if="needsCollapsing"
|
||||||
|
ng-click="toggleCollapse();"
|
||||||
|
) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
|
||||||
|
.rp-entry-metadata
|
||||||
|
| {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •
|
||||||
|
span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }}
|
||||||
|
.rp-entry-actions(ng-if="permissions.write")
|
||||||
|
a.rp-entry-button(href, ng-click="onReject();")
|
||||||
|
i.fa.fa-times
|
||||||
|
| Reject
|
||||||
|
a.rp-entry-button(href, ng-click="onAccept();")
|
||||||
|
i.fa.fa-check
|
||||||
|
| Accept
|
||||||
|
|
||||||
|
script(type='text/ng-template', id='commentEntryTemplate')
|
||||||
|
.rp-comment-wrapper(
|
||||||
|
ng-class="{ 'rp-comment-wrapper-resolving': state.animating }"
|
||||||
|
)
|
||||||
|
.rp-entry-callout.rp-entry-callout-comment
|
||||||
|
.rp-entry-indicator(
|
||||||
|
ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
|
||||||
|
ng-click="onIndicatorClick();"
|
||||||
|
)
|
||||||
|
i.fa.fa-comment
|
||||||
|
.rp-entry.rp-entry-comment(
|
||||||
|
ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolving': state.animating }"
|
||||||
|
)
|
||||||
|
|
||||||
|
.rp-loading(ng-if="!threads[entry.thread_id].submitting && (!threads[entry.thread_id] || threads[entry.thread_id].messages.length == 0)")
|
||||||
|
| No comments
|
||||||
|
.rp-comment-loaded
|
||||||
|
.rp-comment(
|
||||||
|
ng-repeat="comment in threads[entry.thread_id].messages track by comment.id"
|
||||||
|
)
|
||||||
|
p.rp-comment-content
|
||||||
|
span(ng-if="!comment.editing")
|
||||||
|
span.rp-entry-user(
|
||||||
|
style="color: hsl({{ comment.user.hue }}, 70%, 40%);",
|
||||||
|
) {{ comment.user.name }}:
|
||||||
|
span(ng-bind-html="comment.content | linky:'_blank'")
|
||||||
|
textarea.rp-comment-input(
|
||||||
|
expandable-text-area
|
||||||
|
ng-if="comment.editing"
|
||||||
|
ng-model="comment.content"
|
||||||
|
ng-keypress="saveEditOnEnter($event, comment);"
|
||||||
|
ng-blur="saveEdit(comment)"
|
||||||
|
autofocus
|
||||||
|
stop-propagation="click"
|
||||||
|
)
|
||||||
|
.rp-entry-metadata(ng-if="!comment.editing")
|
||||||
|
span(ng-if="!comment.deleting") {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
|
||||||
|
span.rp-comment-actions(ng-if="comment.user.isSelf && !comment.deleting")
|
||||||
|
| •
|
||||||
|
a(href, ng-click="startEditing(comment)") Edit
|
||||||
|
span(ng-if="threads[entry.thread_id].messages.length > 1")
|
||||||
|
| •
|
||||||
|
a(href, ng-click="confirmDelete(comment)") Delete
|
||||||
|
span.rp-confim-delete(ng-if="comment.user.isSelf && comment.deleting")
|
||||||
|
| Are you sure?
|
||||||
|
| •
|
||||||
|
a(href, ng-click="doDelete(comment)") Delete
|
||||||
|
| •
|
||||||
|
a(href, ng-click="cancelDelete(comment)") Cancel
|
||||||
|
|
||||||
|
.rp-loading(ng-if="threads[entry.thread_id].submitting")
|
||||||
|
i.fa.fa-spinner.fa-spin
|
||||||
|
.rp-comment-reply(ng-if="permissions.comment")
|
||||||
|
textarea.rp-comment-input(
|
||||||
|
expandable-text-area
|
||||||
|
ng-model="entry.replyContent"
|
||||||
|
ng-keypress="handleCommentReplyKeyPress($event);"
|
||||||
|
stop-propagation="click"
|
||||||
|
placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}"
|
||||||
|
)
|
||||||
|
.rp-entry-actions
|
||||||
|
button.rp-entry-button(
|
||||||
|
ng-click="animateAndCallOnResolve();"
|
||||||
|
ng-if="permissions.comment && permissions.write"
|
||||||
|
)
|
||||||
|
i.fa.fa-inbox
|
||||||
|
| Resolve
|
||||||
|
button.rp-entry-button(
|
||||||
|
ng-click="onReply();"
|
||||||
|
ng-if="permissions.comment"
|
||||||
|
ng-disabled="!entry.replyContent.length"
|
||||||
|
)
|
||||||
|
i.fa.fa-reply
|
||||||
|
| Reply
|
||||||
|
|
||||||
|
script(type='text/ng-template', id='resolvedCommentEntryTemplate')
|
||||||
|
.rp-resolved-comment
|
||||||
|
div
|
||||||
|
.rp-resolved-comment-context
|
||||||
|
| Quoted text on
|
||||||
|
span.rp-resolved-comment-context-file {{ thread.docName }}
|
||||||
|
p.rp-resolved-comment-context-quote
|
||||||
|
span {{ thread.content | limitTo:(isCollapsed ? contentLimit : thread.content.length)}}
|
||||||
|
a.rp-collapse-toggle(
|
||||||
|
href
|
||||||
|
ng-if="needsCollapsing"
|
||||||
|
ng-click="toggleCollapse();"
|
||||||
|
) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
|
||||||
|
.rp-comment(
|
||||||
|
ng-repeat="comment in thread.messages track by comment.id"
|
||||||
|
)
|
||||||
|
p.rp-comment-content
|
||||||
|
span.rp-entry-user(
|
||||||
|
style="color: hsl({{ comment.user.hue }}, 70%, 40%);"
|
||||||
|
ng-if="$first || comment.user.id !== thread.messages[$index - 1].user.id"
|
||||||
|
) {{ comment.user.name }}:
|
||||||
|
span(ng-bind-html="comment.content | linky:'_blank'")
|
||||||
|
.rp-entry-metadata
|
||||||
|
| {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
|
||||||
|
.rp-comment.rp-comment-resolver
|
||||||
|
p.rp-comment-resolver-content
|
||||||
|
span.rp-entry-user(
|
||||||
|
style="color: hsl({{ thread.resolved_by_user.hue }}, 70%, 40%);"
|
||||||
|
) {{ thread.resolved_by_user.name }}:
|
||||||
|
| Marked as resolved.
|
||||||
|
.rp-entry-metadata
|
||||||
|
| {{ thread.resolved_at | date : 'MMM d, y h:mm a' }}
|
||||||
|
|
||||||
|
.rp-entry-actions(ng-if="permissions.comment && permissions.write")
|
||||||
|
a.rp-entry-button(
|
||||||
|
href
|
||||||
|
ng-click="onUnresolve({ 'threadId': thread.threadId });"
|
||||||
|
)
|
||||||
|
| Re-open
|
||||||
|
a.rp-entry-button(
|
||||||
|
href
|
||||||
|
ng-click="onDelete({ 'entryId': thread.entryId, 'docId': thread.docId, 'threadId': thread.threadId });"
|
||||||
|
)
|
||||||
|
| Delete
|
||||||
|
|
||||||
|
|
||||||
|
script(type='text/ng-template', id='addCommentEntryTemplate')
|
||||||
|
div
|
||||||
|
.rp-entry-callout.rp-entry-callout-add-comment
|
||||||
|
.rp-entry-indicator(
|
||||||
|
ng-if="!commentState.adding"
|
||||||
|
ng-click="startNewComment(); onIndicatorClick();"
|
||||||
|
tooltip="Add a comment"
|
||||||
|
tooltip-placement="right"
|
||||||
|
tooltip-append-to-body="true"
|
||||||
|
)
|
||||||
|
i.fa.fa-commenting
|
||||||
|
.rp-entry.rp-entry-add-comment(
|
||||||
|
ng-class="[ (state.isAdding ? 'rp-entry-adding-comment' : ''), (entry.focused ? 'rp-entry-focused' : '')]"
|
||||||
|
)
|
||||||
|
a.rp-add-comment-btn(
|
||||||
|
href
|
||||||
|
ng-if="!state.isAdding"
|
||||||
|
ng-click="startNewComment();"
|
||||||
|
)
|
||||||
|
i.fa.fa-comment
|
||||||
|
| Add comment
|
||||||
|
div(ng-if="state.isAdding")
|
||||||
|
.rp-new-comment
|
||||||
|
textarea.rp-comment-input(
|
||||||
|
expandable-text-area
|
||||||
|
ng-model="state.content"
|
||||||
|
ng-keypress="handleCommentKeyPress($event);"
|
||||||
|
placeholder="Add your comment here"
|
||||||
|
focus-on="comment:new:open"
|
||||||
|
)
|
||||||
|
.rp-entry-actions
|
||||||
|
button.rp-entry-button(
|
||||||
|
ng-click="cancelNewComment();"
|
||||||
|
)
|
||||||
|
i.fa.fa-times
|
||||||
|
| Cancel
|
||||||
|
button.rp-entry-button(
|
||||||
|
ng-click="submitNewComment()"
|
||||||
|
ng-disabled="!state.content.length"
|
||||||
|
)
|
||||||
|
i.fa.fa-comment
|
||||||
|
| Comment
|
||||||
|
|
||||||
|
script(type='text/ng-template', id='resolvedCommentsDropdownTemplate')
|
||||||
|
.resolved-comments
|
||||||
|
.resolved-comments-backdrop(
|
||||||
|
ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }"
|
||||||
|
ng-click="state.isOpen = false"
|
||||||
|
)
|
||||||
|
a.resolved-comments-toggle(
|
||||||
|
href
|
||||||
|
ng-click="toggleOpenState();"
|
||||||
|
tooltip="Resolved Comments"
|
||||||
|
tooltip-placement="bottom"
|
||||||
|
tooltip-append-to-body="true"
|
||||||
|
)
|
||||||
|
i.fa.fa-inbox
|
||||||
|
.resolved-comments-dropdown(
|
||||||
|
ng-class="{ 'resolved-comments-dropdown-open' : state.isOpen }"
|
||||||
|
)
|
||||||
|
.rp-loading(ng-if="isLoading")
|
||||||
|
i.fa.fa-spinner.fa-spin
|
||||||
|
.resolved-comments-scroller(
|
||||||
|
ng-if="!isLoading"
|
||||||
|
)
|
||||||
|
resolved-comment-entry(
|
||||||
|
ng-repeat="thread in resolvedComments | orderBy:'resolved_at':true"
|
||||||
|
thread="thread"
|
||||||
|
on-unresolve="handleUnresolve(threadId);"
|
||||||
|
on-delete="handleDelete(entryId, docId, threadId);"
|
||||||
|
permissions="permissions"
|
||||||
|
)
|
||||||
|
.rp-loading(ng-if="!resolvedComments.length")
|
||||||
|
| No resolved threads.
|
||||||
|
|
||||||
|
script(type="text/ng-template", id="trackChangesUpgradeModalTemplate")
|
||||||
|
.modal-header
|
||||||
|
button.close(
|
||||||
|
type="button"
|
||||||
|
data-dismiss="modal"
|
||||||
|
ng-click="cancel()"
|
||||||
|
) ×
|
||||||
|
h3 Upgrade to Track Changes
|
||||||
|
.modal-body
|
||||||
|
.teaser-video-container
|
||||||
|
video.teaser-video(autoplay, loop)
|
||||||
|
source(src="/img/teasers/track-changes/teaser-track-changes.mp4", type="video/mp4")
|
||||||
|
img(src="/img/teasers/track-changes/teaser-track-changes.gif")
|
||||||
|
|
||||||
|
h4.teaser-title See changes in your documents, live
|
||||||
|
|
||||||
|
p.small(ng-show="startedFreeTrial")
|
||||||
|
| #{translate("refresh_page_after_starting_free_trial")}
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col-md-10.col-md-offset-1
|
||||||
|
ul.list-unstyled
|
||||||
|
li
|
||||||
|
i.fa.fa-check
|
||||||
|
| Track any change, in real-time
|
||||||
|
|
||||||
|
li
|
||||||
|
i.fa.fa-check
|
||||||
|
| Review your peers' work
|
||||||
|
|
||||||
|
li
|
||||||
|
i.fa.fa-check
|
||||||
|
| Accept or reject each change individually
|
||||||
|
|
||||||
|
|
||||||
|
.row.text-center(ng-controller="FreeTrialModalController")
|
||||||
|
a.btn.btn-success(
|
||||||
|
href
|
||||||
|
ng-click="startFreeTrial('track-changes')"
|
||||||
|
) Try it for free
|
||||||
|
|
||||||
|
.modal-footer()
|
||||||
|
button.btn.btn-default(
|
||||||
|
ng-click="cancel()"
|
||||||
|
)
|
||||||
|
span #{translate("close")}
|
|
@ -38,7 +38,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||||
.col-xs-1
|
.col-xs-1
|
||||||
a(
|
a(
|
||||||
href
|
href
|
||||||
tooltip="#{translate('remove_collaborator')}"
|
tooltip=translate('remove_collaborator')
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
ng-click="removeMember(member)"
|
ng-click="removeMember(member)"
|
||||||
)
|
)
|
||||||
|
@ -55,7 +55,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||||
.col-xs-1
|
.col-xs-1
|
||||||
a(
|
a(
|
||||||
href
|
href
|
||||||
tooltip="#{translate('revoke_invite')}"
|
tooltip=translate('revoke_invite')
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
ng-click="revokeInvite(invite)"
|
ng-click="revokeInvite(invite)"
|
||||||
)
|
)
|
||||||
|
@ -66,7 +66,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||||
.form-group
|
.form-group
|
||||||
tags-input(
|
tags-input(
|
||||||
template="shareTagTemplate"
|
template="shareTagTemplate"
|
||||||
placeholder="#{settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'}"
|
placeholder=settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'
|
||||||
ng-model="inputs.contacts"
|
ng-model="inputs.contacts"
|
||||||
focus-on="open"
|
focus-on="open"
|
||||||
display-property="display"
|
display-property="display"
|
||||||
|
@ -144,6 +144,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||||
span(ng-switch="state.errorReason")
|
span(ng-switch="state.errorReason")
|
||||||
span(ng-switch-when="cannot_invite_non_user")
|
span(ng-switch-when="cannot_invite_non_user")
|
||||||
| #{translate("cannot_invite_non_user")}
|
| #{translate("cannot_invite_non_user")}
|
||||||
|
span(ng-switch-when="cannot_invite_self")
|
||||||
|
| #{translate("cannot_invite_self")}
|
||||||
span(ng-switch-default)
|
span(ng-switch-default)
|
||||||
| #{translate("generic_something_went_wrong")}
|
| #{translate("generic_something_went_wrong")}
|
||||||
button.btn.btn-default(
|
button.btn.btn-default(
|
|
@ -20,12 +20,12 @@ block content
|
||||||
form.form(
|
form.form(
|
||||||
name="acceptForm",
|
name="acceptForm",
|
||||||
method="POST",
|
method="POST",
|
||||||
action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept"
|
action="/project/"+invite.projectId+"/invite/token/"+invite.token+"/accept"
|
||||||
)
|
)
|
||||||
input(name='_csrf', type='hidden', value=csrfToken)
|
input(name='_csrf', type='hidden', value=csrfToken)
|
||||||
input(name='token', type='hidden', value="#{invite.token}")
|
input(name='token', type='hidden', value=invite.token)
|
||||||
.form-group.text-center
|
.form-group.text-center
|
||||||
button.btn.btn-lg.btn-primary(type="submit")
|
button.btn.btn-lg.btn-primary(type="submit")
|
||||||
| #{translate("join_project")}
|
| #{translate("join_project")}
|
||||||
.form-group.text-center
|
.form-group.text-center
|
||||||
|
|
|
@ -73,4 +73,4 @@ block content
|
||||||
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
|
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
|
||||||
include ./list/empty-project-list
|
include ./list/empty-project-list
|
||||||
|
|
||||||
include ./list/modals
|
include ./list/modals
|
|
@ -6,7 +6,7 @@
|
||||||
form.project-search.form-horizontal(role="form")
|
form.project-search.form-horizontal(role="form")
|
||||||
.form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12
|
.form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12
|
||||||
input.form-control.col-md-7.col-xs-12(
|
input.form-control.col-md-7.col-xs-12(
|
||||||
placeholder="#{translate('search_projects')}…",
|
placeholder=translate('search_projects')+"…",
|
||||||
autofocus='autofocus',
|
autofocus='autofocus',
|
||||||
ng-model="searchText.value",
|
ng-model="searchText.value",
|
||||||
focus-on='search:clear',
|
focus-on='search:clear',
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
.btn-group(ng-hide="selectedProjects.length < 1")
|
.btn-group(ng-hide="selectedProjects.length < 1")
|
||||||
a.btn.btn-default(
|
a.btn.btn-default(
|
||||||
href,
|
href,
|
||||||
tooltip="#{translate('download')}",
|
tooltip=translate('download'),
|
||||||
tooltip-placement="bottom",
|
tooltip-placement="bottom",
|
||||||
tooltip-append-to-body="true",
|
tooltip-append-to-body="true",
|
||||||
ng-click="downloadSelectedProjects()"
|
ng-click="downloadSelectedProjects()"
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
i.fa.fa-cloud-download
|
i.fa.fa-cloud-download
|
||||||
a.btn.btn-default(
|
a.btn.btn-default(
|
||||||
href,
|
href,
|
||||||
tooltip="#{translate('delete')}",
|
tooltip=translate('delete'),
|
||||||
tooltip-placement="bottom",
|
tooltip-placement="bottom",
|
||||||
tooltip-append-to-body="true",
|
tooltip-append-to-body="true",
|
||||||
ng-click="openArchiveProjectsModal()"
|
ng-click="openArchiveProjectsModal()"
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
href,
|
href,
|
||||||
data-toggle="dropdown",
|
data-toggle="dropdown",
|
||||||
dropdown-toggle,
|
dropdown-toggle,
|
||||||
tooltip="#{translate('add_to_folders')}",
|
tooltip=translate('add_to_folders'),
|
||||||
tooltip-append-to-body="true",
|
tooltip-append-to-body="true",
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
)
|
)
|
|
@ -24,7 +24,7 @@ block content
|
||||||
.row
|
.row
|
||||||
.col-md-8.col-md-offset-2.bonus-banner
|
.col-md-8.col-md-offset-2.bonus-banner
|
||||||
.title
|
.title
|
||||||
a(href='https://twitter.com/share?text=is%20trying%20out%20the%20online%20LaTeX%20Editor%20ShareLaTeX&url=#{encodeURIComponent(buildReferalUrl("t"))}&counturl=https://www.sharelatex.com', target="_blank").twitter Tweet
|
a(href='https://twitter.com/share?text=is%20trying%20out%20the%20online%20LaTeX%20Editor%20ShareLaTeX&url='+encodeURIComponent(buildReferalUrl("t"))+'&counturl=https://www.sharelatex.com', target="_blank").twitter Tweet
|
||||||
|
|
||||||
.row
|
.row
|
||||||
.col-md-8.col-md-offset-2.bonus-banner
|
.col-md-8.col-md-offset-2.bonus-banner
|
||||||
|
@ -34,12 +34,12 @@ block content
|
||||||
.row
|
.row
|
||||||
.col-md-8.col-md-offset-2.bonus-banner
|
.col-md-8.col-md-offset-2.bonus-banner
|
||||||
.title
|
.title
|
||||||
a(href="https://plus.google.com/share?url=#{encodeURIComponent(buildReferalUrl('gp'))}", onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;").google-plus #{translate("share_us_on_googleplus")}
|
a(href="https://plus.google.com/share?url="+encodeURIComponent(buildReferalUrl('gp')), onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;").google-plus #{translate("share_us_on_googleplus")}
|
||||||
|
|
||||||
.row
|
.row
|
||||||
.col-md-8.col-md-offset-2.bonus-banner
|
.col-md-8.col-md-offset-2.bonus-banner
|
||||||
.title
|
.title
|
||||||
a(href='mailto:?subject=Online LaTeX editor you may like &body=Hey, I have been using the online LaTeX editor ShareLaTeX recently and thought you might like to check it out. #{encodeURIComponent(buildReferalUrl("e"))}', title='Share by Email').email #{translate("email_us_to_your_friends")}
|
a(href='mailto:?subject=Online LaTeX editor you may like &body=Hey, I have been using the online LaTeX editor ShareLaTeX recently and thought you might like to check it out. '+encodeURIComponent(buildReferalUrl("e")), title='Share by Email').email #{translate("email_us_to_your_friends")}
|
||||||
|
|
||||||
.row
|
.row
|
||||||
.col-md-8.col-md-offset-2.bonus-banner
|
.col-md-8.col-md-offset-2.bonus-banner
|
||||||
|
@ -58,9 +58,9 @@ block content
|
||||||
.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 30px; margin-top: 20px;")
|
.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 30px; margin-top: 20px;")
|
||||||
- for (var i = 0; i <= 10; i++) {
|
- for (var i = 0; i <= 10; i++) {
|
||||||
- if (refered_user_count == i)
|
- if (refered_user_count == i)
|
||||||
.number(style="left: #{i}0%").active #{i}
|
.number(style="left: "+i+"0%").active #{i}
|
||||||
- else
|
- else
|
||||||
.number(style="left: #{i}0%") #{i}
|
.number(style="left: "+i+"0%") #{i}
|
||||||
- }
|
- }
|
||||||
|
|
||||||
.row.ab-bonus
|
.row.ab-bonus
|
||||||
|
@ -68,7 +68,7 @@ block content
|
||||||
.progress
|
.progress
|
||||||
- if (refered_user_count == 0)
|
- if (refered_user_count == 0)
|
||||||
div(style="text-align: center; padding: 4px;") #{translate("spread_the_word_and_fill_bar")}
|
div(style="text-align: center; padding: 4px;") #{translate("spread_the_word_and_fill_bar")}
|
||||||
.progress-bar.progress-bar-info(style="width: #{refered_user_count}0%")
|
.progress-bar.progress-bar-info(style="width: "+refered_user_count+"0%")
|
||||||
|
|
||||||
.row.ab-bonus
|
.row.ab-bonus
|
||||||
.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 70px;")
|
.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 70px;")
|
|
@ -19,11 +19,11 @@ block content
|
||||||
|
|
||||||
.row-fluid
|
.row-fluid
|
||||||
table.table
|
table.table
|
||||||
-each project in projects
|
each project in projects
|
||||||
tr
|
tr
|
||||||
- project_id = project._id.toString()
|
- project_id = project._id.toString()
|
||||||
td(width="50%") #{project.name}
|
td(width="50%") #{project.name}
|
||||||
td(width="25%")
|
td(width="25%")
|
||||||
a.btn(href="/project/#{project_id}/zip") Download latest version as Zip
|
a.btn(href="/project/"+project_id+"/zip") Download latest version as Zip
|
||||||
|
|
||||||
include general/small-footer
|
include general/small-footer
|
|
@ -5,4 +5,4 @@ script(type='text/ng-template', id='scribtexModalTemplate')
|
||||||
p ScribTeX has moved to <strong>https://scribtex.sharelatex.com</strong>. Please update your bookmarks.
|
p ScribTeX has moved to <strong>https://scribtex.sharelatex.com</strong>. Please update your bookmarks.
|
||||||
p(style="text-align: center") You can find the page you were looking for here:
|
p(style="text-align: center") You can find the page you were looking for here:
|
||||||
p(style="text-align: center")
|
p(style="text-align: center")
|
||||||
a(href="https://scribtex.sharelatex.com#{scribtexPath}", style="font-size: 16px") https://scribtex.sharelatex.com#{scribtexPath}
|
a(href="https://scribtex.sharelatex.com"+scribtexPath, style="font-size: 16px") https://scribtex.sharelatex.com#{scribtexPath}
|
|
@ -1,8 +1,8 @@
|
||||||
- if (typeof(sentrySrc) != "undefined")
|
- if (typeof(sentrySrc) != "undefined")
|
||||||
- if (sentrySrc.match(/^([a-z]+:)?\/\//i))
|
- if (sentrySrc.match(/^([a-z]+:)?\/\//i))
|
||||||
script(src="#{sentrySrc}")
|
script(src=sentrySrc)
|
||||||
- else
|
- else
|
||||||
script(src=buildJsPath("libs/#{sentrySrc}", {fingerprint:false}))
|
script(src=buildJsPath("libs/"+sentrySrc, {fingerprint:false}))
|
||||||
- if (typeof(sentrySrc) != "undefined")
|
- if (typeof(sentrySrc) != "undefined")
|
||||||
script(type="text/javascript").
|
script(type="text/javascript").
|
||||||
if (typeof(Raven) != "undefined" && Raven.config) {
|
if (typeof(Raven) != "undefined" && Raven.config) {
|
|
@ -12,7 +12,7 @@ block scripts
|
||||||
|
|
||||||
mixin printPlan(plan)
|
mixin printPlan(plan)
|
||||||
-if (!plan.hideFromUsers)
|
-if (!plan.hideFromUsers)
|
||||||
tr(ng-controller="ChangePlanFormController", ng-init="plan=#{JSON.stringify(plan)}", ng-show="shouldShowPlan(plan.planCode)")
|
tr(ng-controller="ChangePlanFormController", ng-init="plan="+JSON.stringify(plan), ng-show="shouldShowPlan(plan.planCode)")
|
||||||
td
|
td
|
||||||
strong #{plan.name}
|
strong #{plan.name}
|
||||||
td {{refreshPrice(plan.planCode)}}
|
td {{refreshPrice(plan.planCode)}}
|
||||||
|
@ -22,18 +22,18 @@ mixin printPlan(plan)
|
||||||
| {{prices[plan.planCode]}} / #{translate("month")}
|
| {{prices[plan.planCode]}} / #{translate("month")}
|
||||||
td
|
td
|
||||||
-if (subscription.state == "free-trial")
|
-if (subscription.state == "free-trial")
|
||||||
a(href="/user/subscription/new?planCode=#{plan.planCode}").btn.btn-success #{translate("subscribe_to_this_plan")}
|
a(href="/user/subscription/new?planCode="+plan.planCode).btn.btn-success #{translate("subscribe_to_this_plan")}
|
||||||
-else if (typeof(subscription.planCode) != "undefined" && plan.planCode == subscription.planCode.split("_")[0])
|
-else if (typeof(subscription.planCode) != "undefined" && plan.planCode == subscription.planCode.split("_")[0])
|
||||||
button.btn.disabled #{translate("your_plan")}
|
button.btn.disabled #{translate("your_plan")}
|
||||||
-else
|
-else
|
||||||
form
|
form
|
||||||
input(type="hidden", ng-model="plan_code", name="plan_code", value="#{plan.planCode}")
|
input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode)
|
||||||
input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success
|
input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success
|
||||||
|
|
||||||
|
|
||||||
mixin printPlans(plans)
|
mixin printPlans(plans)
|
||||||
-each plan in plans
|
each plan in plans
|
||||||
mixin printPlan(plan)
|
+printPlan(plan)
|
||||||
|
|
||||||
block content
|
block content
|
||||||
.content.content-alt(ng-cloak)
|
.content.content-alt(ng-cloak)
|
||||||
|
@ -46,7 +46,7 @@ block content
|
||||||
|
|
|
|
||||||
| #{translate("your_billing_details_were_saved")}
|
| #{translate("your_billing_details_were_saved")}
|
||||||
.card(ng-if="view == 'overview'")
|
.card(ng-if="view == 'overview'")
|
||||||
.page-header(x-current-plan="#{subscription.planCode}")
|
.page-header(x-current-plan=subscription.planCode)
|
||||||
h1 #{translate("your_subscription")}
|
h1 #{translate("your_subscription")}
|
||||||
|
|
||||||
- if (subscription && user._id+'' == subscription.admin_id+'')
|
- if (subscription && user._id+'' == subscription.admin_id+'')
|
||||||
|
@ -97,9 +97,9 @@ block content
|
||||||
th !{translate("name")}
|
th !{translate("name")}
|
||||||
th !{translate("price")}
|
th !{translate("price")}
|
||||||
th
|
th
|
||||||
mixin printPlans(plans.studentAccounts)
|
+printPlans(plans.studentAccounts)
|
||||||
mixin printPlans(plans.individualMonthlyPlans)
|
+printPlans(plans.individualMonthlyPlans)
|
||||||
mixin printPlans(plans.individualAnnualPlans)
|
+printPlans(plans.individualAnnualPlans)
|
||||||
|
|
||||||
|
|
||||||
each groupSubscription in groupSubscriptions
|
each groupSubscription in groupSubscriptions
|
||||||
|
@ -107,7 +107,7 @@ block content
|
||||||
div
|
div
|
||||||
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
|
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
|
||||||
span
|
span
|
||||||
button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
|
button.btn.btn-danger(ng-click="removeSelfFromGroup('"+groupSubscription.admin_id._id+"')") #{translate("leave_group")}
|
||||||
|
|
||||||
-if(subscription.groupPlan && user._id+'' == subscription.admin_id+'')
|
-if(subscription.groupPlan && user._id+'' == subscription.admin_id+'')
|
||||||
div
|
div
|
|
@ -164,7 +164,7 @@ block content
|
||||||
ng-change="updateCountry()"
|
ng-change="updateCountry()"
|
||||||
required
|
required
|
||||||
)
|
)
|
||||||
mixin countries_options()
|
+countries_options()
|
||||||
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
|
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
|
||||||
|
|
||||||
if (showVatField)
|
if (showVatField)
|
|
@ -6,7 +6,7 @@ block content
|
||||||
.container(ng-controller="AnnualUpgradeController")
|
.container(ng-controller="AnnualUpgradeController")
|
||||||
.row(ng-cloak)
|
.row(ng-cloak)
|
||||||
.col-md-6.col-md-offset-3
|
.col-md-6.col-md-offset-3
|
||||||
.card(ng-init="planName = #{JSON.stringify(planName)}")
|
.card(ng-init="planName = "+JSON.stringify(planName))
|
||||||
.page-header
|
.page-header
|
||||||
h1.text-centered #{translate("move_to_annual_billing")}
|
h1.text-centered #{translate("move_to_annual_billing")}
|
||||||
div(ng-hide="upgradeComplete")
|
div(ng-hide="upgradeComplete")
|
|
@ -2,7 +2,7 @@
|
||||||
span(ng-controller="TranslationsPopupController", ng-cloak)
|
span(ng-controller="TranslationsPopupController", ng-cloak)
|
||||||
.translations-message(ng-hide="hidei18nNotification")
|
.translations-message(ng-hide="hidei18nNotification")
|
||||||
a(href=recomendSubdomain.url+currentUrl) !{translate("click_here_to_view_sl_in_lng", {lngName:"<strong>" + translate(recomendSubdomain.lngCode) + "</strong>"})}
|
a(href=recomendSubdomain.url+currentUrl) !{translate("click_here_to_view_sl_in_lng", {lngName:"<strong>" + translate(recomendSubdomain.lngCode) + "</strong>"})}
|
||||||
img(src=buildImgPath("flags/24/#{recomendSubdomain.lngCode}.png"))
|
img(src=buildImgPath("flags/24/" + recomendSubdomain.lngCode + ".png"))
|
||||||
button(ng-click="dismiss()").close.pull-right
|
button(ng-click="dismiss()").close.pull-right
|
||||||
span(aria-hidden="true") ×
|
span(aria-hidden="true") ×
|
||||||
span.sr-only #{translate("close")}
|
span.sr-only #{translate("close")}
|
|
@ -36,7 +36,7 @@ block content
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
required,
|
required,
|
||||||
ng-model="email",
|
ng-model="email",
|
||||||
ng-init="email = #{JSON.stringify(email)}",
|
ng-init="email = "+JSON.stringify(email),
|
||||||
ng-model-options="{ updateOn: 'blur' }",
|
ng-model-options="{ updateOn: 'blur' }",
|
||||||
disabled
|
disabled
|
||||||
)
|
)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue