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"
|
||||
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
|
||||
require('es6-promise').polyfill()
|
||||
|
||||
module.exports = (grunt) ->
|
||||
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||
|
@ -18,6 +19,7 @@ module.exports = (grunt) ->
|
|||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||
grunt.loadNpmTasks 'grunt-parallel'
|
||||
grunt.loadNpmTasks 'grunt-exec'
|
||||
grunt.loadNpmTasks 'grunt-postcss'
|
||||
# grunt.loadNpmTasks 'grunt-contrib-imagemin'
|
||||
# grunt.loadNpmTasks 'grunt-sprity'
|
||||
|
||||
|
@ -136,8 +138,14 @@ module.exports = (grunt) ->
|
|||
files:
|
||||
"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:
|
||||
run:
|
||||
|
@ -222,11 +230,11 @@ module.exports = (grunt) ->
|
|||
|
||||
sed:
|
||||
version:
|
||||
path: "app/views/sentry.jade"
|
||||
path: "app/views/sentry.pug"
|
||||
pattern: '@@COMMIT@@',
|
||||
replacement: '<%= commit %>',
|
||||
release:
|
||||
path: "app/views/sentry.jade"
|
||||
path: "app/views/sentry.pug"
|
||||
pattern: "@@RELEASE@@"
|
||||
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: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: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']
|
||||
|
@ -389,5 +397,5 @@ module.exports = (grunt) ->
|
|||
|
||||
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?
|
||||
return res.json []
|
||||
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {user_id}, "getting unread announcements"
|
||||
AnnouncementsHandler.getUnreadAnnouncements user_id, (err, announcements)->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
logger.log {user_id:user?._id}, "getting unread announcements"
|
||||
AnnouncementsHandler.getUnreadAnnouncements user, (err, announcements)->
|
||||
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)
|
||||
else
|
||||
res.json announcements
|
||||
|
|
|
@ -1,24 +1,46 @@
|
|||
AnalyticsManager = require("../Analytics/AnalyticsManager")
|
||||
BlogHandler = require("../Blog/BlogHandler")
|
||||
async = require("async")
|
||||
_ = require("lodash")
|
||||
logger = require("logger-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 {
|
||||
lastEvent: (cb)->
|
||||
AnalyticsManager.getLastOccurance user_id, "announcement-alert-dismissed", cb
|
||||
AnalyticsManager.getLastOccurance user._id, "announcement-alert-dismissed", cb
|
||||
announcements: (cb)->
|
||||
BlogHandler.getLatestAnnouncements cb
|
||||
}, (err, results)->
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
|
@ -35,6 +57,6 @@ module.exports =
|
|||
announcement.read = read
|
||||
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
|
||||
|
|
|
@ -148,6 +148,7 @@ module.exports = AuthenticationController =
|
|||
return next()
|
||||
else
|
||||
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
|
||||
AuthenticationController._setRedirectInSession(req)
|
||||
return res.redirect "/login"
|
||||
|
||||
httpAuth: basicAuth (user, pass)->
|
||||
|
@ -193,8 +194,8 @@ module.exports = AuthenticationController =
|
|||
|
||||
_setRedirectInSession: (req, value) ->
|
||||
if !value?
|
||||
value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else req.path
|
||||
if req.session?
|
||||
value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}"
|
||||
if req.session? && !value.match(new RegExp('^\/(socket.io|js|stylesheets|img)\/.*$'))
|
||||
req.session.postLoginRedirect = value
|
||||
|
||||
_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")
|
||||
logger = require("logger-sharelatex")
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserInfoManager = require('../User/UserInfoManager')
|
||||
UserInfoController = require('../User/UserInfoController')
|
||||
CommentsController = require('../Comments/CommentsController')
|
||||
|
||||
module.exports =
|
||||
|
||||
|
||||
sendMessage: (req, res, next)->
|
||||
project_id = req.params.Project_id
|
||||
messageContent = req.body.content
|
||||
project_id = req.params.project_id
|
||||
content = req.body.content
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
if !user_id?
|
||||
err = new Error('no logged-in user')
|
||||
return next(err)
|
||||
ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api"
|
||||
return res.sendStatus(500)
|
||||
EditorRealTimeController.emitToRoom project_id, "new-chat-message", builtMessge, (err)->
|
||||
res.send()
|
||||
ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) ->
|
||||
return next(err) if err?
|
||||
UserInfoManager.getPersonalInfo message.user_id, (err, user) ->
|
||||
return next(err) if err?
|
||||
message.user = UserInfoController.formatPersonalInfo(user)
|
||||
EditorRealTimeController.emitToRoom project_id, "new-chat-message", message, (err)->
|
||||
res.send(204)
|
||||
|
||||
getMessages: (req, res)->
|
||||
project_id = req.params.Project_id
|
||||
getMessages: (req, res, next)->
|
||||
project_id = req.params.project_id
|
||||
query = req.query
|
||||
logger.log project_id:project_id, query:query, "getting messages"
|
||||
ChatHandler.getMessages project_id, query, (err, messages)->
|
||||
if err?
|
||||
logger.err err:err, query:query, "problem getting messages from chat api"
|
||||
return res.sendStatus 500
|
||||
ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
|
||||
return next(err) if err?
|
||||
CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
|
||||
return next(err) if err?
|
||||
logger.log length: messages?.length, "sending messages to client"
|
||||
res.set 'Content-Type', 'application/json'
|
||||
res.send messages
|
||||
res.json 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)}"
|
||||
].join("&")
|
||||
|
||||
notifyUserOfProjectInvite: (project_id, email, invite, callback)->
|
||||
notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)->
|
||||
Project
|
||||
.findOne(_id: project_id )
|
||||
.select("name owner_ref")
|
||||
|
@ -24,4 +24,5 @@ module.exports = CollaboratorsEmailHandler =
|
|||
name: project.name
|
||||
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
|
||||
owner: project.owner_ref
|
||||
sendingUser_id: sendingUser._id
|
||||
EmailHandler.sendEmail "projectInvite", emailOptions, callback
|
||||
|
|
|
@ -10,6 +10,7 @@ EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
|||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||
AnalyticsManger = require("../Analytics/AnalyticsManager")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
rateLimiter = require("../../infrastructure/RateLimiter")
|
||||
|
||||
module.exports = CollaboratorsInviteController =
|
||||
|
||||
|
@ -32,11 +33,27 @@ module.exports = CollaboratorsInviteController =
|
|||
else
|
||||
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) ->
|
||||
projectId = req.params.Project_id
|
||||
email = req.body.email
|
||||
sendingUser = AuthenticationController.getSessionUser(req)
|
||||
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"
|
||||
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
|
||||
return next(error) if error?
|
||||
|
@ -48,6 +65,10 @@ module.exports = CollaboratorsInviteController =
|
|||
if !email? or email == ""
|
||||
logger.log {projectId, email, sendingUserId}, "invalid email address"
|
||||
return res.sendStatus(400)
|
||||
CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) ->
|
||||
return next(error) if error?
|
||||
if !underRateLimit
|
||||
return res.sendStatus(429)
|
||||
CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
|
||||
if err?
|
||||
logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
|
||||
|
|
|
@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler =
|
|||
|
||||
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
|
||||
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?
|
||||
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
|
||||
return callback(err) if err?
|
||||
|
@ -80,7 +80,7 @@ module.exports = CollaboratorsInviteHandler =
|
|||
# Send email and notification in background
|
||||
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (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)
|
||||
|
||||
|
||||
|
|
|
@ -22,9 +22,15 @@ module.exports =
|
|||
webRouter.post(
|
||||
'/project/:Project_id/invite',
|
||||
RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "invite-to-project"
|
||||
endpointName: "invite-to-project-by-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
|
||||
}),
|
||||
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
|
|
@ -30,6 +30,21 @@ module.exports = DocstoreManager =
|
|||
logger.error err: error, project_id: project_id, "error getting all docs from docstore"
|
||||
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) ->) ->
|
||||
if typeof(options) == "function"
|
||||
callback = options
|
||||
|
@ -45,13 +60,13 @@ module.exports = DocstoreManager =
|
|||
return callback(error) if error?
|
||||
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"
|
||||
callback(null, doc.lines, doc.rev, doc.version)
|
||||
callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
|
||||
else
|
||||
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"
|
||||
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"
|
||||
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
|
||||
request.post {
|
||||
|
@ -59,6 +74,7 @@ module.exports = DocstoreManager =
|
|||
json:
|
||||
lines: lines
|
||||
version: version
|
||||
ranges: ranges
|
||||
}, (error, res, result) ->
|
||||
return callback(error) if error?
|
||||
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}"
|
||||
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")
|
||||
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"
|
||||
|
@ -110,7 +110,7 @@ module.exports = DocumentUpdaterHandler =
|
|||
body = JSON.parse(body)
|
||||
catch error
|
||||
return callback(error)
|
||||
callback null, body.lines, body.version, body.ops
|
||||
callback null, body.lines, body.version, body.ranges, body.ops
|
||||
else
|
||||
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}")
|
||||
|
@ -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}"
|
||||
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||
|
||||
getNumberOfDocsInMemory : (callback)->
|
||||
request.get "#{settings.apis.documentupdater.url}/total", (err, req, body)->
|
||||
try
|
||||
body = JSON.parse body
|
||||
catch err
|
||||
logger.err err:err, "error parsing response from doc updater about the total number of docs"
|
||||
callback(err, body?.total)
|
||||
|
||||
acceptChange: (project_id, doc_id, change_id, callback = (error) ->) ->
|
||||
timer = new metrics.Timer("accept-change")
|
||||
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept"
|
||||
logger.log {project_id, doc_id, change_id}, "accepting change in document updater"
|
||||
request.post url, (error, res, body)->
|
||||
timer.done()
|
||||
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"
|
||||
DOCLINESKEY = "doclines"
|
||||
|
|
|
@ -7,7 +7,7 @@ module.exports =
|
|||
doc_id = req.params.doc_id
|
||||
plain = req?.query?.plain == 'true'
|
||||
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?
|
||||
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
|
||||
return next(error)
|
||||
|
@ -19,14 +19,15 @@ module.exports =
|
|||
res.send JSON.stringify {
|
||||
lines: lines
|
||||
version: version
|
||||
ranges: ranges
|
||||
}
|
||||
|
||||
setDocument: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_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)"
|
||||
ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, (error) ->
|
||||
ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, ranges, (error) ->
|
||||
if error?
|
||||
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
|
||||
return next(error)
|
||||
|
|
|
@ -7,7 +7,6 @@ ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
|||
ProjectDeleter = require("../Project/ProjectDeleter")
|
||||
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
EditorRealTimeController = require("./EditorRealTimeController")
|
||||
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
|
||||
async = require('async')
|
||||
LockManager = require("../../infrastructure/LockManager")
|
||||
_ = 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')
|
||||
|
||||
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
|
||||
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
|
||||
BaseWithHeaderEmailLayout = require("./Layouts/BaseWithHeaderEmailLayout")
|
||||
|
||||
SingleCTAEmailBody = require("./Bodies/SingleCTAEmailBody")
|
||||
|
||||
|
||||
settings = require("settings-sharelatex")
|
||||
|
||||
|
||||
|
@ -61,7 +67,7 @@ ShareLaTeX Co-founder
|
|||
|
||||
templates.passwordResetRequested =
|
||||
subject: _.template "Password Reset - #{settings.appName}"
|
||||
layout: NotificationEmailLayout
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
Password Reset
|
||||
|
@ -78,36 +84,21 @@ Thank you
|
|||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: _.template """
|
||||
<h2>Password Reset</h2>
|
||||
<p>
|
||||
We got a request to reset your #{settings.appName} password.
|
||||
<p>
|
||||
<center>
|
||||
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
|
||||
<div style="padding-right:10px;padding-left:10px">
|
||||
<a href="<%= setNewPasswordUrl %>" style="text-decoration:none" target="_blank">
|
||||
<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>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "Password Reset"
|
||||
greeting: "Hi,"
|
||||
message: "We got a request to reset your #{settings.appName} password."
|
||||
secondaryMessage: "If you ignore this message, your password won't be changed.<br>If you didn't request a password reset, let us know."
|
||||
ctaText: "Reset password"
|
||||
ctaURL: opts.setNewPasswordUrl
|
||||
gmailGoToAction: null
|
||||
})
|
||||
|
||||
|
||||
templates.projectInvite =
|
||||
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
|
||||
layout: NotificationEmailLayout
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
|
||||
|
@ -118,23 +109,23 @@ Thank you
|
|||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: _.template """
|
||||
<p>Hi, <%= owner.email %> wants to share <a href="<%= inviteUrl %>">'<%= project.name %>'</a> with you</p>
|
||||
<center>
|
||||
<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">
|
||||
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
||||
View Project
|
||||
</span>
|
||||
</a>
|
||||
</center>
|
||||
<p> Thank you</p>
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
"""
|
||||
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "#{ opts.project.name } – shared by #{ opts.owner.email }"
|
||||
greeting: "Hi,"
|
||||
message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you."
|
||||
secondaryMessage: null
|
||||
ctaText: "View project"
|
||||
ctaURL: opts.inviteUrl
|
||||
gmailGoToAction:
|
||||
target: opts.inviteUrl
|
||||
name: "View project"
|
||||
description: "Join #{ opts.project.name } at ShareLaTeX"
|
||||
})
|
||||
|
||||
templates.completeJoinGroupAccount =
|
||||
subject: _.template "Verify Email to join <%= group_name %> group"
|
||||
layout: NotificationEmailLayout
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
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 %>
|
||||
"""
|
||||
compiledTemplate: _.template """
|
||||
<p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p>
|
||||
<center>
|
||||
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
|
||||
<div style="padding-right:10px;padding-left:10px">
|
||||
<a href="<%= completeJoinUrl %>" style="text-decoration:none" target="_blank">
|
||||
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
||||
Verify now
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</center>
|
||||
<p> Thank you</p>
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "Verify Email to join #{ opts.group_name } group"
|
||||
greeting: "Hi,"
|
||||
message: "please verify your email to join the #{ opts.group_name } group and get your free premium account."
|
||||
secondaryMessage: null
|
||||
ctaText: "Verify now"
|
||||
ctaURL: opts.completeJoinUrl
|
||||
gmailGoToAction: null
|
||||
})
|
||||
|
||||
|
||||
templates.testEmail =
|
||||
subject: _.template "A Test Email from ShareLaTeX"
|
||||
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 =
|
||||
|
|
|
@ -4,7 +4,7 @@ Settings = require('settings-sharelatex')
|
|||
nodemailer = require("nodemailer")
|
||||
sesTransport = require('nodemailer-ses-transport')
|
||||
sgTransport = require('nodemailer-sendgrid-transport')
|
||||
|
||||
rateLimiter = require('../../infrastructure/RateLimiter')
|
||||
_ = require("underscore")
|
||||
|
||||
if Settings.email? and Settings.email.fromAddress?
|
||||
|
@ -39,10 +39,25 @@ if nm_client?
|
|||
else
|
||||
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 =
|
||||
sendEmail : (options, callback = (error) ->)->
|
||||
logger.log receiver:options.to, subject:options.subject, "sending email"
|
||||
checkCanSendEmail options, (err, canContinue)->
|
||||
if err?
|
||||
return callback(err)
|
||||
if !canContinue
|
||||
logger.log sendingUser_id:options.sendingUser_id, to:options.to, subject:options.subject, canContinue:canContinue, "rate limit hit for sending email, not sending"
|
||||
return callback("rate limit hit sending email")
|
||||
metrics.inc "email"
|
||||
options =
|
||||
to: options.to
|
||||
|
|
|
@ -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")
|
||||
ProjectUpdateHandler = require("../Project/ProjectUpdateHandler")
|
||||
Project = require("../../models/Project").Project
|
||||
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
|
||||
|
||||
|
||||
MILISECONDS_IN_DAY = 86400000
|
||||
module.exports = InactiveProjectManager =
|
||||
|
@ -52,7 +50,6 @@ module.exports = InactiveProjectManager =
|
|||
logger.log project_id:project_id, "deactivating inactive project"
|
||||
jobs = [
|
||||
(cb)-> DocstoreManager.archiveProject project_id, cb
|
||||
# (cb)-> TrackChangesManager.archiveProject project_id, cb
|
||||
(cb)-> ProjectUpdateHandler.markAsInactive project_id, cb
|
||||
]
|
||||
async.series jobs, (err)->
|
||||
|
|
|
@ -53,7 +53,11 @@ module.exports =
|
|||
if req.body.login_after
|
||||
UserGetter.getUser user_id, {email: 1}, (err, user) ->
|
||||
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
|
||||
res.sendStatus 200
|
||||
else
|
||||
|
|
|
@ -197,11 +197,11 @@ module.exports = ProjectController =
|
|||
user_id = null
|
||||
|
||||
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 {
|
||||
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)->
|
||||
if !user_id?
|
||||
cb null, defaultSettingsForAnonymousUser(user_id)
|
||||
|
@ -267,6 +267,7 @@ module.exports = ProjectController =
|
|||
pdfViewer : user.ace.pdfViewer
|
||||
syntaxValidation: user.ace.syntaxValidation
|
||||
}
|
||||
trackChangesEnabled: !!project.track_changes
|
||||
privilegeLevel: privilegeLevel
|
||||
chatUrl: Settings.apis.chat.url
|
||||
anonymous: anonymous
|
||||
|
|
|
@ -20,6 +20,11 @@ module.exports = ProjectEditorHandler =
|
|||
if !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)
|
||||
result.owner = owner
|
||||
result.members = members
|
||||
|
@ -32,6 +37,8 @@ module.exports = ProjectEditorHandler =
|
|||
compileGroup:"standard"
|
||||
templates: false
|
||||
references: false
|
||||
trackChanges: false
|
||||
trackChangesVisible: trackChangesVisible
|
||||
})
|
||||
|
||||
return result
|
||||
|
|
|
@ -126,7 +126,7 @@ module.exports = ProjectEntityHandler =
|
|||
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
|
||||
# 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?
|
||||
|
||||
ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=>
|
||||
|
@ -292,7 +292,7 @@ module.exports = ProjectEntityHandler =
|
|||
return callback(err)
|
||||
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)->
|
||||
return callback(err) if err?
|
||||
return callback(new Errors.NotFoundError("project not found")) if !project?
|
||||
|
@ -307,7 +307,7 @@ module.exports = ProjectEntityHandler =
|
|||
return callback(error)
|
||||
|
||||
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?
|
||||
logger.error err: err, doc_id: doc_id, project_id:project_id, lines: lines, "error sending doc to docstore"
|
||||
return callback(err)
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
Settings = require('settings-sharelatex')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
RateLimiter = require('../../infrastructure/RateLimiter')
|
||||
|
||||
buildKey = (k)->
|
||||
return "LoginRateLimit:#{k}"
|
||||
|
||||
ONE_MIN = 60
|
||||
ATTEMPT_LIMIT = 10
|
||||
|
||||
|
||||
module.exports =
|
||||
|
||||
processLoginRequest: (email, callback) ->
|
||||
multi = rclient.multi()
|
||||
multi.incr(buildKey(email))
|
||||
multi.get(buildKey(email))
|
||||
multi.expire(buildKey(email), ONE_MIN * 2)
|
||||
multi.exec (err, results)->
|
||||
loginCount = results[1]
|
||||
allow = loginCount <= ATTEMPT_LIMIT
|
||||
callback err, allow
|
||||
opts =
|
||||
endpointName: 'login'
|
||||
throttle: ATTEMPT_LIMIT
|
||||
timeInterval: ONE_MIN * 2
|
||||
subjectName: email
|
||||
RateLimiter.addCount opts, (err, shouldAllow) ->
|
||||
callback(err, shouldAllow)
|
||||
|
||||
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
|
||||
params = (opts.params or []).map (p) -> req.params[p]
|
||||
params.push user_id
|
||||
subjectName = params.join(":")
|
||||
if opts.ipOnly
|
||||
subjectName = req.ip
|
||||
if !opts.endpointName?
|
||||
throw new Error("no endpointName provided")
|
||||
options = {
|
||||
endpointName: opts.endpointName
|
||||
timeInterval: opts.timeInterval or 60
|
||||
subjectName: params.join(":")
|
||||
subjectName: subjectName
|
||||
throttle: opts.maxRequests or 6
|
||||
}
|
||||
RateLimiter.addCount options, (error, canContinue)->
|
||||
|
|
|
@ -6,8 +6,6 @@ Project = require('../../models/Project').Project
|
|||
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
Settings = require('settings-sharelatex')
|
||||
util = require('util')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
RecurlyWrapper = require('../Subscription/RecurlyWrapper')
|
||||
SubscriptionHandler = require('../Subscription/SubscriptionHandler')
|
||||
projectEntityHandler = require('../Project/ProjectEntityHandler')
|
||||
|
|
|
@ -7,7 +7,7 @@ fs = require "fs"
|
|||
ErrorController = require "../Errors/ErrorController"
|
||||
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 =
|
||||
index : (req,res)->
|
||||
|
@ -28,10 +28,10 @@ module.exports = HomeController =
|
|||
|
||||
externalPage: (page, title) ->
|
||||
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!
|
||||
if exists
|
||||
res.render "external/#{page}.jade",
|
||||
res.render "external/#{page}.pug",
|
||||
title: title
|
||||
else
|
||||
ErrorController.notFound(req, res, next)
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
logger = require("logger-sharelatex")
|
||||
Project = require("../../models/Project").Project
|
||||
User = require("../../models/User").User
|
||||
UserGetter = require("../User/UserGetter")
|
||||
SubscriptionLocator = require("./SubscriptionLocator")
|
||||
Settings = require("settings-sharelatex")
|
||||
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
|
||||
CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
|
||||
|
||||
module.exports =
|
||||
|
||||
allowedNumberOfCollaboratorsInProject: (project_id, callback) ->
|
||||
getOwnerOfProject project_id, (error, owner)->
|
||||
Project.findById project_id, 'owner_ref', (error, project) =>
|
||||
return callback(error) if error?
|
||||
if owner.features? and owner.features.collaborators?
|
||||
callback null, owner.features.collaborators
|
||||
@allowedNumberOfCollaboratorsForUser project.owner_ref, callback
|
||||
|
||||
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
|
||||
callback null, Settings.defaultPlanCode.collaborators
|
||||
|
||||
|
||||
canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) ->
|
||||
@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
|
||||
return callback(error) if error?
|
||||
|
@ -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"
|
||||
callback(err, limitReached, subscription)
|
||||
|
||||
getOwnerOfProject = (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)
|
||||
getOwnerIdOfProject = (project_id, callback)->
|
||||
|
|
|
@ -4,7 +4,7 @@ editorController = require('../Editor/EditorController')
|
|||
logger = require('logger-sharelatex')
|
||||
Settings = require('settings-sharelatex')
|
||||
FileTypeManager = require('../Uploads/FileTypeManager')
|
||||
uuid = require('node-uuid')
|
||||
uuid = require('uuid')
|
||||
fs = require('fs')
|
||||
|
||||
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"
|
||||
request = require "request"
|
||||
settings = require "settings-sharelatex"
|
||||
AuthenticationController = require "../Authentication/AuthenticationController"
|
||||
UserInfoController = require "../User/UserInfoController"
|
||||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
TrackChangesManager = require "./TrackChangesManager"
|
||||
|
||||
module.exports = TrackChangesController =
|
||||
proxyToTrackChangesApi: (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)
|
||||
getAllRanges: (req, res, next) ->
|
||||
project_id = req.params.project_id
|
||||
logger.log {project_id}, "request for project ranges"
|
||||
RangesManager.getAllRanges project_id, (error, docs = []) ->
|
||||
return next(error) if error?
|
||||
docs = ({id: d._id, ranges: d.ranges} for d in docs)
|
||||
res.json docs
|
||||
|
||||
getAllChangesUsers: (req, res, next) ->
|
||||
project_id = req.params.project_id
|
||||
logger.log {project_id}, "request for project range users"
|
||||
RangesManager.getAllChangesUsers project_id, (error, users) ->
|
||||
return next(error) if 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"
|
||||
request = require "request"
|
||||
logger = require "logger-sharelatex"
|
||||
Project = require("../../models/Project").Project
|
||||
|
||||
module.exports = TrackChangesManager =
|
||||
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)
|
||||
toggleTrackChanges: (project_id, track_changes_on, callback = (error) ->) ->
|
||||
Project.update {_id: project_id}, {track_changes: track_changes_on}, callback
|
||||
|
|
|
@ -26,17 +26,14 @@ module.exports = UserController =
|
|||
UserController.sendFormattedPersonalInfo(user, res, next)
|
||||
|
||||
sendFormattedPersonalInfo: (user, res, next = (error) ->) ->
|
||||
UserController._formatPersonalInfo user, (error, info) ->
|
||||
return next(error) if error?
|
||||
info = UserController.formatPersonalInfo(user)
|
||||
res.send JSON.stringify(info)
|
||||
|
||||
_formatPersonalInfo: (user, callback = (error, info) ->) ->
|
||||
callback null, {
|
||||
id: user._id.toString()
|
||||
first_name: user.first_name
|
||||
last_name: user.last_name
|
||||
email: user.email
|
||||
signUpDate: user.signUpDate
|
||||
role: user.role
|
||||
institution: user.institution
|
||||
}
|
||||
formatPersonalInfo: (user, callback = (error, info) ->) ->
|
||||
if !user?
|
||||
return {}
|
||||
formatted_user = { id: user._id.toString() }
|
||||
for key in ["first_name", "last_name", "email", "signUpDate", "role", "institution"]
|
||||
if user[key]?
|
||||
formatted_user[key] = user[key]
|
||||
return formatted_user
|
||||
|
|
|
@ -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')
|
||||
redis = require('redis-sharelatex')
|
||||
logger = require("logger-sharelatex")
|
||||
Async = require('async')
|
||||
_ = require('underscore')
|
||||
|
|
|
@ -189,6 +189,7 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
return AuthenticationController.isUserLoggedIn(req)
|
||||
res.locals.getSessionUser = ->
|
||||
return AuthenticationController.getSessionUser(req)
|
||||
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next) ->
|
||||
|
@ -244,6 +245,8 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
for key, value of Settings.nav
|
||||
res.locals.nav[key] = _.clone(Settings.nav[key])
|
||||
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()
|
||||
|
||||
webRouter.use (req, res, next) ->
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
fs = require "fs"
|
||||
Path = require "path"
|
||||
jade = require "jade"
|
||||
pug = require "pug"
|
||||
async = require "async"
|
||||
|
||||
MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules")
|
||||
|
@ -29,7 +29,7 @@ module.exports = Modules =
|
|||
for module in @modules
|
||||
for view, partial of module.viewIncludes or {}
|
||||
@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) ->
|
||||
compiledPartials = Modules.viewIncludes[view] or []
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
settings = require("settings-sharelatex")
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(settings.redis.web)
|
||||
redback = require("redback").use(rclient)
|
||||
RedisWrapper = require('./RedisWrapper')
|
||||
rclient = RedisWrapper.client('ratelimiter')
|
||||
RollingRateLimiter = require('rolling-rate-limiter')
|
||||
|
||||
module.exports =
|
||||
|
||||
addCount: (opts, callback = (opts, shouldProcess)->)->
|
||||
ratelimit = redback.createRateLimit(opts.endpointName)
|
||||
ratelimit.addCount opts.subjectName, opts.timeInterval, (err, callCount)->
|
||||
shouldProcess = callCount < opts.throttle
|
||||
callback(err, shouldProcess)
|
||||
module.exports = RateLimiter =
|
||||
|
||||
addCount: (opts, callback = (err, shouldProcess)->)->
|
||||
namespace = "RateLimit:#{opts.endpointName}:"
|
||||
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) ->
|
||||
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')
|
||||
Router = require('../router')
|
||||
metrics.inc("startup")
|
||||
redis = require("redis-sharelatex")
|
||||
UserSessionsRedis = require('../Features/User/UserSessionsRedis')
|
||||
|
||||
sessionsRedisClient = UserSessionsRedis.client()
|
||||
|
@ -62,7 +61,7 @@ if Settings.behindProxy
|
|||
|
||||
webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
|
||||
app.set 'views', __dirname + '/../../views'
|
||||
app.set 'view engine', 'jade'
|
||||
app.set 'view engine', 'pug'
|
||||
Modules.loadViewIncludes app
|
||||
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ ProjectSchema = new Schema
|
|||
archived : { type: Boolean }
|
||||
deletedDocs : [DeletedDocSchema]
|
||||
imageName : { type: String }
|
||||
track_changes : { type: Boolean }
|
||||
|
||||
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
|
||||
if project_or_id._id?
|
||||
|
|
|
@ -2,7 +2,7 @@ Project = require('./Project').Project
|
|||
Settings = require 'settings-sharelatex'
|
||||
_ = require('underscore')
|
||||
mongoose = require('mongoose')
|
||||
uuid = require('node-uuid')
|
||||
uuid = require('uuid')
|
||||
Schema = mongoose.Schema
|
||||
ObjectId = Schema.ObjectId
|
||||
|
||||
|
@ -37,9 +37,10 @@ UserSchema = new Schema
|
|||
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
|
||||
templates: { type:Boolean, default: Settings.defaultFeatures.templates }
|
||||
references: { type:Boolean, default: Settings.defaultFeatures.references }
|
||||
trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
|
||||
}
|
||||
featureSwitches : {
|
||||
pdfng: { type: Boolean }
|
||||
track_changes: { type: Boolean }
|
||||
}
|
||||
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
|
||||
refered_users: [ type:ObjectId, ref:'User' ]
|
||||
|
|
|
@ -25,7 +25,7 @@ ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")
|
|||
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
|
||||
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
|
||||
FileStoreController = require("./Features/FileStore/FileStoreController")
|
||||
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
|
||||
HistoryController = require("./Features/History/HistoryController")
|
||||
PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
|
||||
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
|
||||
ChatController = require("./Features/Chat/ChatController")
|
||||
|
@ -40,6 +40,8 @@ AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlew
|
|||
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
|
||||
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
|
||||
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
|
||||
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
|
||||
CommentsController = require "./Features/Comments/CommentsController"
|
||||
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
|
@ -171,9 +173,14 @@ module.exports = class Router
|
|||
|
||||
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/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
|
||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", 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, HistoryController.proxyToHistoryApi
|
||||
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/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/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
|
||||
|
||||
webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
|
||||
webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
|
||||
webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
|
||||
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/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
|
||||
|
|
|
@ -28,10 +28,10 @@ block content
|
|||
tab(heading="Open Sockets")
|
||||
.row-spaced
|
||||
ul
|
||||
-each agents, url in openSockets
|
||||
each agents, url in openSockets
|
||||
li #{url} - total : #{agents.length}
|
||||
ul
|
||||
-each agent in agents
|
||||
each agent in agents
|
||||
li #{agent}
|
||||
|
||||
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")
|
||||
span(ng-bind-html="suggestion.name")
|
||||
i.fa.fa-angle-right
|
||||
label.desc(ng-show="'#{getUserEmail()}'.length < 1")
|
||||
label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
|
||||
| #{translate("email")}
|
||||
.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')
|
||||
.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')
|
||||
label#title12.desc
|
||||
| #{translate("project_url")} (#{translate("optional")})
|
||||
.form-group
|
||||
|
@ -37,6 +37,6 @@ script(type='text/ng-template', id='supportModalTemplate')
|
|||
.form-group
|
||||
textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
|
||||
.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")
|
||||
p #{translate("request_sent_thank_you")}
|
|
@ -11,4 +11,4 @@ block content
|
|||
| Sorry, ShareLaTeX is briefly down for maintenance.
|
||||
| We should be back within minutes, but if not, or you have
|
||||
| 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='stylesheet', href=buildCssPath('/style.css'))
|
||||
|
||||
block _headLinks
|
||||
|
||||
if settings.i18n.subdomainLang
|
||||
each subdomainDetails in settings.i18n.subdomainLang
|
||||
if !subdomainDetails.hide
|
||||
|
@ -30,7 +32,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
|
||||
|
||||
-if (typeof(meta) == "undefined")
|
||||
meta(itemprop="description", name="description", content='#{translate("site_description")}')
|
||||
meta(itemprop="description", name="description", content=translate("site_description"))
|
||||
-else
|
||||
meta(itemprop="description", name="description" , content=meta)
|
||||
|
|
@ -13,9 +13,9 @@ footer.site-footer
|
|||
data-toggle="dropdown",
|
||||
aria-haspopup="true",
|
||||
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")
|
||||
li.dropdown-header #{translate("language")}
|
||||
|
@ -23,7 +23,7 @@ footer.site-footer
|
|||
if !subdomainDetails.hide
|
||||
li.lngOption
|
||||
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)}
|
||||
//- img(src="/img/flags/24/.png")
|
||||
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}")
|
||||
i.fa.fa-bars
|
||||
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)
|
||||
a(href='/').navbar-title #{nav.title}
|
||||
else
|
||||
|
@ -24,7 +24,10 @@ nav.navbar.navbar-default
|
|||
li
|
||||
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.dropdown
|
||||
li.dropdown(class=item.class, dropdown)
|
||||
|
@ -35,9 +38,6 @@ nav.navbar.navbar-default
|
|||
each child in item.dropdown
|
||||
if child.divider
|
||||
li.divider
|
||||
else if child.user_email
|
||||
li
|
||||
div.subdued #{getUserEmail()}
|
||||
else
|
||||
li
|
||||
if child.url
|
||||
|
@ -51,6 +51,34 @@ nav.navbar.navbar-default
|
|||
else
|
||||
| !{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.anonymous = #{anonymous};
|
||||
window.maxDocLength = #{maxDocLength};
|
||||
window.trackChangesEnabled = #{trackChangesEnabled};
|
||||
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
|
||||
window.requirejs = {
|
||||
"paths" : {
|
|
@ -50,7 +50,7 @@ aside.chat(
|
|||
|
||||
.new-message
|
||||
textarea(
|
||||
placeholder="#{translate('your_message')}...",
|
||||
placeholder=translate('your_message')+"...",
|
||||
on-enter="sendMessage()",
|
||||
ng-model="newMessageContent",
|
||||
ng-click="resetUnreadMessages()"
|
|
@ -17,7 +17,9 @@ div.full-size(
|
|||
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
|
||||
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
|
||||
'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")
|
||||
|
@ -51,10 +53,10 @@ div.full-size(
|
|||
syntax-validation="settings.syntaxValidation",
|
||||
review-panel="reviewPanel",
|
||||
events-bridge="reviewPanelEventsBridge"
|
||||
track-changes-enabled="trackChangesFeatureFlag",
|
||||
track-new-changes= "reviewPanel.trackNewChanges",
|
||||
changes-tracker="reviewPanel.changesTracker",
|
||||
track-changes-enabled="project.features.trackChangesVisible",
|
||||
track-changes= "editor.trackChanges",
|
||||
doc-id="editor.open_doc_id"
|
||||
renderer-data="reviewPanel.rendererData"
|
||||
)
|
||||
|
||||
include ./review-panel
|
||||
|
@ -68,7 +70,7 @@ div.full-size(
|
|||
ng-controller="PdfSynctexController"
|
||||
)
|
||||
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-append-to-body="true"
|
||||
ng-click="syncToPdf()"
|
||||
|
@ -76,7 +78,7 @@ div.full-size(
|
|||
i.fa.fa-long-arrow-right
|
||||
br
|
||||
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-append-to-body="true"
|
||||
ng-click="syncToCode()"
|
|
@ -3,21 +3,21 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
|
|||
a(
|
||||
href,
|
||||
ng-click="openNewDocModal()",
|
||||
tooltip-html="'#{translate('new_file').replace(' ', '<br>')}'",
|
||||
tooltip-html="'"+translate('new_file').replace(' ', '<br>')+"'",
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
i.fa.fa-file
|
||||
a(
|
||||
href,
|
||||
ng-click="openNewFolderModal()",
|
||||
tooltip-html="'#{translate('new_folder').replace(' ', '<br>')}'",
|
||||
tooltip-html="'"+translate('new_folder').replace(' ', '<br>')+"'",
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
i.fa.fa-folder
|
||||
a(
|
||||
href,
|
||||
ng-click="openUploadFileModal()",
|
||||
tooltip="#{translate('upload')}",
|
||||
tooltip=translate('upload'),
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
i.fa.fa-upload
|
||||
|
@ -26,7 +26,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
|
|||
a(
|
||||
href,
|
||||
ng-click="startRenamingSelected()",
|
||||
tooltip="#{translate('rename')}",
|
||||
tooltip=translate('rename'),
|
||||
tooltip-placement="bottom",
|
||||
ng-show="multiSelectedCount == 0"
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
|
|||
a(
|
||||
href,
|
||||
ng-click="openDeleteModalForSelected()",
|
||||
tooltip="#{translate('delete')}",
|
||||
tooltip=translate('delete'),
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true"
|
||||
)
|
|
@ -45,7 +45,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
|||
ng-if="permissions.admin",
|
||||
href='#',
|
||||
tooltip-placement="bottom",
|
||||
tooltip="#{translate('rename')}",
|
||||
tooltip=translate('rename'),
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
|
@ -71,7 +71,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
|||
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
|
||||
span.online-user.online-user-multi(
|
||||
dropdown-toggle,
|
||||
tooltip="#{translate('connected_users')}",
|
||||
tooltip=translate('connected_users'),
|
||||
tooltip-placement="left"
|
||||
)
|
||||
strong {{ onlineUsersArray.length }}
|
||||
|
@ -87,7 +87,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
|||
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-if="trackChangesFeatureFlag",
|
||||
ng-if="project.features.trackChangesVisible",
|
||||
ng-class="{ active: ui.reviewPanelOpen }"
|
||||
ng-click="toggleReviewPanel()"
|
||||
)
|
|
@ -24,7 +24,7 @@ aside#left-menu.full-size(
|
|||
| PDF
|
||||
div.link-disabled(
|
||||
ng-if="!pdf.url"
|
||||
tooltip="#{translate('please_compile_pdf_before_download')}"
|
||||
tooltip=translate('please_compile_pdf_before_download')
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
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()")
|
||||
i.fa.fa-fw.fa-eye
|
||||
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
|
||||
span.link-disabled #{translate("word_count")}
|
||||
|
||||
|
@ -200,7 +200,7 @@ script(type='text/ng-template', id='wordCountModalTemplate')
|
|||
span #{translate("loading")}...
|
||||
div.pdf-disabled(
|
||||
ng-if="!pdf.url"
|
||||
tooltip="#{translate('please_compile_pdf_before_word_count')}"
|
||||
tooltip=translate('please_compile_pdf_before_word_count')
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
div(ng-if="!status.loading")
|
|
@ -2,7 +2,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
.toolbar.toolbar-tall
|
||||
.btn-group(
|
||||
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-popup-delay="500"
|
||||
tooltip-append-to-body="true"
|
||||
|
@ -53,7 +53,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
href
|
||||
ng-click="stop()"
|
||||
ng-show="pdf.compiling",
|
||||
tooltip="#{translate('stop_compile')}"
|
||||
tooltip=translate('stop_compile')
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
i.fa.fa-stop()
|
||||
|
@ -61,7 +61,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
href
|
||||
ng-click="toggleLogs()"
|
||||
ng-class="{ 'active': shouldShowLogs == true }"
|
||||
tooltip="#{translate('logs_and_output_files')}"
|
||||
tooltip=translate('logs_and_output_files')
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
i.fa.fa-file-text-o
|
||||
|
@ -77,7 +77,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
ng-href="{{pdf.downloadUrl || pdf.url}}"
|
||||
target="_blank"
|
||||
ng-if="pdf.url"
|
||||
tooltip="#{translate('download_pdf')}"
|
||||
tooltip=translate('download_pdf')
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
i.fa.fa-download
|
||||
|
@ -87,7 +87,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
href,
|
||||
ng-click="switchToFlatLayout()"
|
||||
ng-show="ui.pdfLayout == 'sideBySide'"
|
||||
tooltip="#{translate('full_screen')}"
|
||||
tooltip=translate('full_screen')
|
||||
tooltip-placement="bottom"
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
|
@ -96,7 +96,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
href,
|
||||
ng-click="switchToSideBySideLayout()"
|
||||
ng-show="ui.pdfLayout == 'flat'"
|
||||
tooltip="#{translate('split_screen')}"
|
||||
tooltip=translate('split_screen')
|
||||
tooltip-placement="bottom"
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
|
@ -233,7 +233,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
.files-dropdown-container
|
||||
a.btn.btn-default.btn-sm(
|
||||
href,
|
||||
tooltip="#{translate('clear_cached_files')}",
|
||||
tooltip=translate('clear_cached_files'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
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
|
||||
a(
|
||||
href
|
||||
tooltip="#{translate('remove_collaborator')}"
|
||||
tooltip=translate('remove_collaborator')
|
||||
tooltip-placement="bottom"
|
||||
ng-click="removeMember(member)"
|
||||
)
|
||||
|
@ -55,7 +55,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
|||
.col-xs-1
|
||||
a(
|
||||
href
|
||||
tooltip="#{translate('revoke_invite')}"
|
||||
tooltip=translate('revoke_invite')
|
||||
tooltip-placement="bottom"
|
||||
ng-click="revokeInvite(invite)"
|
||||
)
|
||||
|
@ -66,7 +66,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
|||
.form-group
|
||||
tags-input(
|
||||
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"
|
||||
focus-on="open"
|
||||
display-property="display"
|
||||
|
@ -144,6 +144,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
|||
span(ng-switch="state.errorReason")
|
||||
span(ng-switch-when="cannot_invite_non_user")
|
||||
| #{translate("cannot_invite_non_user")}
|
||||
span(ng-switch-when="cannot_invite_self")
|
||||
| #{translate("cannot_invite_self")}
|
||||
span(ng-switch-default)
|
||||
| #{translate("generic_something_went_wrong")}
|
||||
button.btn.btn-default(
|
|
@ -20,10 +20,10 @@ block content
|
|||
form.form(
|
||||
name="acceptForm",
|
||||
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='token', type='hidden', value="#{invite.token}")
|
||||
input(name='token', type='hidden', value=invite.token)
|
||||
.form-group.text-center
|
||||
button.btn.btn-lg.btn-primary(type="submit")
|
||||
| #{translate("join_project")}
|
|
@ -6,7 +6,7 @@
|
|||
form.project-search.form-horizontal(role="form")
|
||||
.form-group.has-feedback.has-feedback-left.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',
|
||||
ng-model="searchText.value",
|
||||
focus-on='search:clear',
|
||||
|
@ -25,7 +25,7 @@
|
|||
.btn-group(ng-hide="selectedProjects.length < 1")
|
||||
a.btn.btn-default(
|
||||
href,
|
||||
tooltip="#{translate('download')}",
|
||||
tooltip=translate('download'),
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="downloadSelectedProjects()"
|
||||
|
@ -33,7 +33,7 @@
|
|||
i.fa.fa-cloud-download
|
||||
a.btn.btn-default(
|
||||
href,
|
||||
tooltip="#{translate('delete')}",
|
||||
tooltip=translate('delete'),
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="openArchiveProjectsModal()"
|
||||
|
@ -45,7 +45,7 @@
|
|||
href,
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle,
|
||||
tooltip="#{translate('add_to_folders')}",
|
||||
tooltip=translate('add_to_folders'),
|
||||
tooltip-append-to-body="true",
|
||||
tooltip-placement="bottom"
|
||||
)
|
|
@ -24,7 +24,7 @@ block content
|
|||
.row
|
||||
.col-md-8.col-md-offset-2.bonus-banner
|
||||
.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
|
||||
.col-md-8.col-md-offset-2.bonus-banner
|
||||
|
@ -34,12 +34,12 @@ block content
|
|||
.row
|
||||
.col-md-8.col-md-offset-2.bonus-banner
|
||||
.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
|
||||
.col-md-8.col-md-offset-2.bonus-banner
|
||||
.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
|
||||
.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;")
|
||||
- for (var i = 0; i <= 10; i++) {
|
||||
- if (refered_user_count == i)
|
||||
.number(style="left: #{i}0%").active #{i}
|
||||
.number(style="left: "+i+"0%").active #{i}
|
||||
- else
|
||||
.number(style="left: #{i}0%") #{i}
|
||||
.number(style="left: "+i+"0%") #{i}
|
||||
- }
|
||||
|
||||
.row.ab-bonus
|
||||
|
@ -68,7 +68,7 @@ block content
|
|||
.progress
|
||||
- if (refered_user_count == 0)
|
||||
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
|
||||
.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 70px;")
|
|
@ -19,11 +19,11 @@ block content
|
|||
|
||||
.row-fluid
|
||||
table.table
|
||||
-each project in projects
|
||||
each project in projects
|
||||
tr
|
||||
- project_id = project._id.toString()
|
||||
td(width="50%") #{project.name}
|
||||
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
|
|
@ -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(style="text-align: center") You can find the page you were looking for here:
|
||||
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 (sentrySrc.match(/^([a-z]+:)?\/\//i))
|
||||
script(src="#{sentrySrc}")
|
||||
script(src=sentrySrc)
|
||||
- else
|
||||
script(src=buildJsPath("libs/#{sentrySrc}", {fingerprint:false}))
|
||||
script(src=buildJsPath("libs/"+sentrySrc, {fingerprint:false}))
|
||||
- if (typeof(sentrySrc) != "undefined")
|
||||
script(type="text/javascript").
|
||||
if (typeof(Raven) != "undefined" && Raven.config) {
|
|
@ -12,7 +12,7 @@ block scripts
|
|||
|
||||
mixin printPlan(plan)
|
||||
-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
|
||||
strong #{plan.name}
|
||||
td {{refreshPrice(plan.planCode)}}
|
||||
|
@ -22,18 +22,18 @@ mixin printPlan(plan)
|
|||
| {{prices[plan.planCode]}} / #{translate("month")}
|
||||
td
|
||||
-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])
|
||||
button.btn.disabled #{translate("your_plan")}
|
||||
-else
|
||||
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
|
||||
|
||||
|
||||
mixin printPlans(plans)
|
||||
-each plan in plans
|
||||
mixin printPlan(plan)
|
||||
each plan in plans
|
||||
+printPlan(plan)
|
||||
|
||||
block content
|
||||
.content.content-alt(ng-cloak)
|
||||
|
@ -46,7 +46,7 @@ block content
|
|||
|
|
||||
| #{translate("your_billing_details_were_saved")}
|
||||
.card(ng-if="view == 'overview'")
|
||||
.page-header(x-current-plan="#{subscription.planCode}")
|
||||
.page-header(x-current-plan=subscription.planCode)
|
||||
h1 #{translate("your_subscription")}
|
||||
|
||||
- if (subscription && user._id+'' == subscription.admin_id+'')
|
||||
|
@ -97,9 +97,9 @@ block content
|
|||
th !{translate("name")}
|
||||
th !{translate("price")}
|
||||
th
|
||||
mixin printPlans(plans.studentAccounts)
|
||||
mixin printPlans(plans.individualMonthlyPlans)
|
||||
mixin printPlans(plans.individualAnnualPlans)
|
||||
+printPlans(plans.studentAccounts)
|
||||
+printPlans(plans.individualMonthlyPlans)
|
||||
+printPlans(plans.individualAnnualPlans)
|
||||
|
||||
|
||||
each groupSubscription in groupSubscriptions
|
||||
|
@ -107,7 +107,7 @@ block content
|
|||
div
|
||||
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
|
||||
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+'')
|
||||
div
|
|
@ -164,7 +164,7 @@ block content
|
|||
ng-change="updateCountry()"
|
||||
required
|
||||
)
|
||||
mixin countries_options()
|
||||
+countries_options()
|
||||
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
|
||||
|
||||
if (showVatField)
|
|
@ -6,7 +6,7 @@ block content
|
|||
.container(ng-controller="AnnualUpgradeController")
|
||||
.row(ng-cloak)
|
||||
.col-md-6.col-md-offset-3
|
||||
.card(ng-init="planName = #{JSON.stringify(planName)}")
|
||||
.card(ng-init="planName = "+JSON.stringify(planName))
|
||||
.page-header
|
||||
h1.text-centered #{translate("move_to_annual_billing")}
|
||||
div(ng-hide="upgradeComplete")
|
|
@ -2,7 +2,7 @@
|
|||
span(ng-controller="TranslationsPopupController", ng-cloak)
|
||||
.translations-message(ng-hide="hidei18nNotification")
|
||||
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
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
|
@ -36,7 +36,7 @@ block content
|
|||
placeholder="email@example.com"
|
||||
required,
|
||||
ng-model="email",
|
||||
ng-init="email = #{JSON.stringify(email)}",
|
||||
ng-init="email = "+JSON.stringify(email),
|
||||
ng-model-options="{ updateOn: 'blur' }",
|
||||
disabled
|
||||
)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue