Merge branch 'master' of github.com:sharelatex/web-sharelatex

This commit is contained in:
Henry Oswald 2014-03-18 15:09:50 +00:00
commit 3c9f6e0ce0
21 changed files with 259 additions and 156 deletions

View file

@ -18,9 +18,11 @@ module.exports = EditorUpdatesController =
if takeSnapshot
AutomaticSnapshotManager.markProjectAsUpdated(project_id)
logger.log doc_id: doc_id, project_id: project_id, client_id: update.meta?.source, version: update.v, "sending update to doc updater"
DocumentUpdaterHandler.queueChange project_id, doc_id, update, (error) ->
if error?
logger.error err:error, project_id: project_id, "document was not available for update"
logger.error err:error, project_id: project_id, doc_id: doc_id, client_id: update.meta?.source, version: update.v, "document was not available for update"
client.disconnect()
callback(error)
@ -52,8 +54,10 @@ module.exports = EditorUpdatesController =
io = require('../../infrastructure/Server').io
for client in io.sockets.clients(doc_id)
if client.id == update.meta.source
logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, "distributing update to sender"
client.emit "otUpdateApplied", v: update.v, doc: update.doc
else
logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, client_id: client.id, "distributing update to collaborator"
client.emit "otUpdateApplied", update
_processErrorFromDocumentUpdater: (doc_id, error, message) ->

View file

@ -2,24 +2,30 @@ logger = require('logger-sharelatex')
metrics = require('../../infrastructure/Metrics')
Settings = require('settings-sharelatex')
metrics = require("../../infrastructure/Metrics")
ses = require('node-ses')
nodemailer = require("nodemailer")
if Settings.email? and Settings.email.fromAddress?
defaultFromAddress = Settings.email.fromAddress
else
defaultFromAddress = ""
if Settings.email?.ses? and Settings.email.ses?.key? and Settings.email.ses?.key != "" and Settings.email.ses?.secret? and Settings.email.ses?.secret != ""
client = ses.createClient({ key: Settings.email.ses.key, secret: Settings.email.ses.secret });
else
logger.warn "AWS SES credentials are not configured. No emails will be sent."
client =
sendemail: (options, callback = (err, data, res) ->) ->
logger.log options: options, "would send email if SES credentials enabled"
# provide dummy mailer unless we have a better one configured.
client =
sendMail: (options, callback = (err,status) ->) ->
logger.log options:options, "Would send email if enabled."
callback()
module.exports =
if Settings.email?
if Settings.email.transport? and Settings.email.parameters?
nm_client = nodemailer.createTransport( Settings.email.transport, Settings.email.parameters )
if nm_client
client = nm_client
else
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
else
logger.warn "Email transport and/or parameters not defined. No emails will be sent."
module.exports =
sendEmail : (options, callback = (error) ->)->
logger.log receiver:options.to, subject:options.subject, "sending email"
metrics.inc "email"
@ -27,9 +33,9 @@ module.exports =
to: options.to
from: defaultFromAddress
subject: options.subject
message: options.html
html: options.html
replyTo: options.replyTo || Settings.email.replyToAddress
client.sendemail options, (err, data, res)->
client.sendMail options, (err, res)->
if err?
logger.err err:err, "error sending message"
else

View file

@ -217,6 +217,10 @@ module.exports = class Router
require("./models/Project").Project.findOne {}, () ->
throw new Error("Test error")
app.post '/error/client', (req, res, next) ->
logger.error err: req.body.error, meta: req.body.meta, "client side error"
res.send(204)
app.get '*', HomeController.notFound

View file

@ -22,6 +22,9 @@ html(itemscope, itemtype='http://schema.org/Product')
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '#{gaToken}', 'sharelatex.com');
ga('send', 'pageview');
- else
script(type='text/javascript')
window.ga = function() {};
script
window.csrfToken = "#{csrfToken}";

View file

@ -77,15 +77,6 @@
.modal-body
span.message
#genericServerErrorModal(style='display: none')
.modal
.modal-header
h3 There was a problem talking to the server
.modal-body
span.message Sorry, we couldn't complete your request right now. Please wait a few moments and try again. If the problem persists, please let us know.
.modal-footer
button.btn.btn-primary.cancel Ok
#projectUploadModal(style='display: none')
.modal
.modal-header

View file

@ -136,16 +136,6 @@
#subscribeForm
.modal-footer
script(type="text/template")#genericModalTemplateWithButton
.modal
.modal-header
h3 {{ title }}
.modal-body
.message {{ message }}
.modal-footer
button.btn.btn-primary ok
script(type="text/template")#genericModalButtonTemplate
button(class="btn {{ class }}") {{ text }}
@ -300,22 +290,29 @@
-if(session && session.user && session.user.isAdmin)
.box
.page-header
h2 Publish Project
h2 Publish project as template
#publishedAsTemplateArea(style="display:none;")
a#templateLink(href='{{canonicalUrl}}') View Template
p published at {{publishedDate}}
.btn.btn-warning#unPublishProjectAsTemplate unpublish project as template
.btn.btn-success#republishProjectAsTemplate re publish project as template
#publishedAsTemplateArea.show-when-published.alert.alert-success
p
.btn.btn-warning#unPublishProjectAsTemplate.pull-right Unpublish
i.icon-ok
| Your project is currently published.
a#templateLink(href='{{canonicalUrl}}') View in template gallery.
p
| Lastest version: {{publishedDate}}.
#problemWithPublishingArea(style="display:none;")
#problemWithPublishingArea
p There is a problem with our publishing service, please try again in a few minutes.
#publishWorkingArea(style="display:none;")
p Working.....
#unpublishedAsTemplateArea(style="display:none;")
.btn.btn-success#publishProjectAsTemplate Publish project as template
div
textarea.span6#projectDescription {{description}}
#publishWorkingArea
p Working...
div.show-when-published.show-when-unpublished.project-description
label(for="project-description") Description
.row-fluid
textarea(placeholder="Template description", name="project-description").span12#projectDescription {{description}}
#unpublishedAsTemplateArea.show-when-unpublished
.btn.btn-success#publishProjectAsTemplate Publish
p.show-when-published
button.btn.btn-success#republishProjectAsTemplate Re-Publish
script(type="text/template")#settingsPanelTemplate

View file

@ -134,6 +134,22 @@ module.exports =
{name: "English", code: "en"}
]
# Email support
# -------------
#
# ShareLaTeX uses nodemailer (http://www.nodemailer.com/) to send transactional emails.
# To see the range of transport and options they support, see http://www.nodemailer.com/docs/transports
#email:
# fromAddress: ""
# replyTo: ""
# lifecycle: false
## Example transport and parameter settings for Amazon SES
# transport: "SES"
# parameters:
# AWSAccessKeyID: ""
# AWSSecretKey: ""
# Third party services
# --------------------
#
@ -153,15 +169,6 @@ module.exports =
# tenderUrl: ""
#
# email:
# fromAddress: ""
# replyTo: ""
# lifecycle: false
# ShareLaTeX uses Amazon's SES api to send transactional emails.
# Uncomment these lines and provide your credentials to be able to send emails.
# ses:
# "key":""
# "secret":""
# Production Settings
# -------------------

View file

@ -32,7 +32,7 @@
"fairy": "0.0.2",
"node-uuid": "1.4.0",
"mongojs": "0.9.8",
"node-ses": "0.0.3",
"nodemailer": "0.6.1",
"bcrypt": "0.7.5",
"archiver": "0.5.1",
"nodetime": "0.8.15",

View file

@ -79,6 +79,14 @@ define () ->
\\end{enumerate}
"""
meta: "env"
}, {
caption: "\\begin{itemize}..."
snippet: """
\\begin{itemize}
\\item $1
\\end{itemize}
"""
meta: "env"
}, {
caption: "\\begin{frame}..."
snippet: """

View file

@ -136,16 +136,18 @@ define [
@unBindFromSocketEvents()
_bindToShareJsDocEvents: () ->
@doc.on "error", (error) => @_onError error
@doc.on "error", (error, meta) => @_onError error, meta
@doc.on "externalUpdate", () => @trigger "externalUpdate"
@doc.on "remoteop", () => @trigger "remoteop"
@doc.on "op:sent", () => @trigger "op:sent"
@doc.on "op:acknowledged", () => @trigger "op:acknowledged"
_onError: (error) ->
console.error "ShareJS error", error
ga('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" )
_onError: (error, meta = {}) ->
console.error "ShareJS error", error, meta
ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" )
@ide.socket.disconnect()
meta.doc_id = @doc_id
@ide.reportError(error, meta)
@doc?.clearInflightAndPendingOps()
@_cleanUp()
@trigger "error", error

View file

@ -121,7 +121,7 @@ define [
@aceEditor = aceEditor = AceEditor.edit("editor")
@on "resize", => @aceEditor.resize()
@ide.layoutManager.on "resize", => @aceEditor.resize()
@ide.layoutManager.on "resize", => @trigger "resize"
mode = window.userSettings.mode
theme = window.userSettings.theme

View file

@ -94,14 +94,17 @@ define [
INFLIGHT_OP_TIMEOUT: 10000
_startInflightOpTimeout: (update) ->
meta =
v: update.v
op_sent_at: new Date()
timer = setTimeout () =>
@_handleError "Doc op was not acknowledged in time"
@_handleError new Error("Doc op was not acknowledged in time"), meta
, @INFLIGHT_OP_TIMEOUT
@_doc.inflightCallbacks.push () =>
clearTimeout timer
_handleError: (error) ->
@trigger "error", error
_handleError: (error, meta = {}) ->
@trigger "error", error, meta
_bindToDocChanges: (doc) ->
submitOp = doc.submitOp

View file

@ -48,7 +48,7 @@ define [
SearchManager,
Project,
User,
StandaloneModal,
Modal,
FileTreeManager,
MessageManager,
HelpManager,
@ -144,16 +144,39 @@ define [
setTimeout(joinProject, 100)
showErrorModal: (title, message)->
modalOptions =
templateId:'genericModalTemplate'
isStatic: false
new Modal {
title: title
message:message
new Modal modalOptions
message: message
buttons: [ text: "OK" ]
}
showGenericServerErrorMessage: (message)->
new Modal
templateId : "genericServerErrorModal"
showGenericServerErrorMessage: ()->
new Modal {
title: "There was a problem talking to the server"
message: "Sorry, we couldn't complete your request right now. Please wait a few moments and try again. If the problem persists, please let us know."
buttons: [ text: "OK" ]
}
reportError: (error, meta = {}) ->
meta.client_id = @socket?.socket?.sessionid
meta.transport = @socket?.socket?.transport?.name
meta.client_now = new Date()
meta.last_connected = @connectionManager.lastConnected
meta.second_last_connected = @connectionManager.secondLastConnected
meta.last_disconnected = @connectionManager.lastDisconnected
meta.second_last_disconnected = @connectionManager.secondLastDisconnected
errorObj = {}
for key in Object.getOwnPropertyNames(error)
errorObj[key] = error[key]
$.ajax
url: "/error/client"
type: "POST"
data: JSON.stringify
error: errorObj
meta: meta
contentType: "application/json; charset=utf-8"
headers:
"X-Csrf-Token": window.csrfToken
setLoadingMessage: (message) ->
$("#loadingMessage").text(message)
@ -171,45 +194,6 @@ define [
ide.layoutManager.resizeAllSplitters()
ide.tourManager = new IdeTour ide
class Modal
#templateId, title, message, isStatic, cancelCallback
constructor: (options, completeCallback = () -> {})->
html = $("##{options.templateId}").html()
modal = "<div id='modal' style='display:none'>#{html}</div>"
$('body').append(modal)
$modal = $('#modal')
if options.title?
$modal.find('h3').text(options.title)
if options.message?
$modal.find('.message').text(options.message)
if options.inputValue?
$modal.find('input').val(options.inputValue)
backdrop = true
if options.backdrop?
backdrop = options.backdrop
$modal.modal backdrop:backdrop, show:true, keyboard:true, isStatic:options.isStatic
$modal.find('input').focus()
$modal.find('button').click (e)=>
e.preventDefault()
$modal.modal('hide')
if e.target.className.indexOf("cancel") == -1
inputval = $modal.find('input').val()
completeCallback(inputval)
$modal.find('input').keydown (event)=>
code = event.keyCode || event.which
if code == 13
$modal.find('button.primary').click()
$modal.bind 'hide', ()->
if options.cancelCallback?
options.cancelCallback()
$('#modal').remove()
ide.savingAreaManager =
$savingArea : $('#saving-area')
timeOut: undefined
@ -220,7 +204,7 @@ define [
return if @timeOut?
@clearTimeout()
@timeOut = setTimeout((=>
ga('send', 'event', 'editor-interaction', 'notification-shown', "saving")
ga?('send', 'event', 'editor-interaction', 'notification-shown', "saving")
$("#savingProblems").show()
), 1000)

View file

@ -8,11 +8,15 @@ define [
@socket = @ide.socket
@socket.on "connect", () =>
@connected = true
@secondLastConnected = @lastConnected
@lastConnected = new Date()
@hideModal()
@cancelReconnect()
@socket.on 'disconnect', () =>
@connected = false
@secondLastDisconnected = @lastDisconnected
@lastDisconnected = new Date()
@ide.trigger "disconnect"
setTimeout(=>
ga('send', 'event', 'editor-interaction', 'disconnect')

View file

@ -3,9 +3,10 @@ define [
"models/ProjectMemberList"
"account/AccountManager"
"utils/Modal"
"moment"
"libs/backbone"
"libs/mustache"
], (User, ProjectMemberList, AccountManager, Modal) ->
], (User, ProjectMemberList, AccountManager, Modal, moment) ->
INFINITE_COLLABORATORS = -1
class ProjectMembersManager
@ -20,6 +21,8 @@ define [
name: "Share"
content : $(@templates.userPanel)
lock: true
onShown: () =>
@publishProjectView?.refreshPublishStatus()
setupPublish = _.once =>
@publishProjectView = new PublishProjectView
@ -177,25 +180,32 @@ define [
initialize: () ->
@ide = @options.ide
@model = @ide.project
_.bindAll(this, "render");
this.model.bind('change', this.render)
@refreshPublishStatus()
@render()
render: ->
viewModel =
description: @model.get("description")
canonicalUrl: @model.get("template.canonicalUrl")
isPublished: @model.get("template.isPublished")
publishedDate: @model.get("template.publishedDate")
publishedDate: moment(@model.get("template.publishedDate")).format("Do MMM YYYY, h:mm a")
$(@el).html $(Mustache.to_html(@template, viewModel))
@publishedArea = $('#publishedAsTemplateArea')
@unpublishedArea = $('#unpublishedAsTemplateArea')
@$el.html $(Mustache.to_html(@template, viewModel))
@publishedArea = $('.show-when-published')
@unpublishedArea = $('.show-when-unpublished')
$('#problemWithPublishingArea').hide()
$('#publishWorkingArea').hide()
refreshView: () ->
if @model.get("template.isPublished")
@$("a#templateLink").attr("href", @model.get("template.canonicalUrl"))
@publishedArea.show()
else
@unpublishedArea.show()
refreshPublishStatus: ->
@showWorking()
@ide.socket.emit "getPublishedDetails", @ide.user.get("id"), (err, details)=>
@hideWorking()
if err?
return @showError()
@ -203,16 +213,17 @@ define [
if details.exists
@model.set("template.canonicalUrl", details.canonicalUrl)
@model.set("template.publishedDate", details.publishedDate)
@publishedArea.show()
@unpublishedArea.hide()
else
@publishedArea.hide()
@unpublishedArea.show()
@refreshView()
showError: ->
@publishedArea.hide()
@unpublishedArea.hide()
$('#problemWithPublishingArea').show()
showWorking: ->
@publishedArea.hide()
@unpublishedArea.hide()
$('#publishWorkingArea').show()
hideWorking: ->

View file

@ -16,11 +16,11 @@ define [
@createAceEditor()
@aceEditor.setValue(@getPlainDiffContent())
@aceEditor.clearSelection()
session = @aceEditor.getSession()
session.setMode(new LatexMode.Mode())
session.setUseWrapMode(true)
@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
@insertMarkers()
@insertNameTag()
@insertMoreChangeLabels()
@scrollToFirstChange()
return @
destroy: () ->
@ -34,12 +34,18 @@ define [
@aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
@aceEditor.setReadOnly true
@aceEditor.setShowPrintMargin(false)
session = @aceEditor.getSession()
session.setMode(new LatexMode.Mode())
session.setUseWrapMode(true)
@aceEditor.on "mousemove", (e) =>
position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
e.position = position
@updateVisibleNames(e)
session.on "changeScrollTop", (e) =>
@updateMoreChangeLabels()
getPlainDiffContent: () ->
content = ""
for entry in @model.get("diff") or []
@ -102,7 +108,6 @@ define [
, foreground
insertNameTag: () ->
@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
@$nameTagEl = $("<div class='change-name-marker'></div>")
@$nameTagEl.css({
position: "absolute"
@ -110,6 +115,19 @@ define [
@$nameTagEl.hide()
@$ace.append(@$nameTagEl)
insertMoreChangeLabels: () ->
@$changesBefore = $("<div class='changes-before'><span></span> <i class='icon-arrow-up'></div>")
@$changesAfter = $("<div class='changes-after'><span></span> <i class='icon-arrow-down'></div>")
@$ace.append(@$changesBefore)
@$ace.append(@$changesAfter)
@updateMoreChangeLabels()
scrollToFirstChange: () ->
if @entries? and @entries[0]?
row = @entries[0].range.start.row
@aceEditor.gotoLine(0)
@aceEditor.gotoLine(row, 0, true)
_drawNameTag: (entry, position) ->
@$nameTagEl.show()
@ -161,5 +179,30 @@ define [
if !visibleName
@_hideNameTag()
updateMoreChangeLabels: () ->
return if !@$changesBefore or !@$changesAfter
firstRow = @aceEditor.renderer.getFirstFullyVisibleRow()
lastRow = @aceEditor.renderer.getLastFullyVisibleRow()
changesBefore = 0
changesAfter = 0
for entry in @entries or []
if entry.range.start.row < firstRow
changesBefore += 1
if entry.range.end.row > lastRow
changesAfter += 1
if changesBefore > 0
@$changesBefore.find("span").text("#{changesBefore} more change#{if changesBefore > 1 then "s" else ""} above")
@$changesBefore.show()
else
@$changesBefore.hide()
if changesAfter > 0
@$changesAfter.find("span").text("#{changesAfter} more change#{if changesAfter > 1 then "s" else ""} below")
@$changesAfter.show()
else
@$changesAfter.hide()
resize: () ->
@aceEditor.resize()
return DiffView

View file

@ -19,6 +19,9 @@ define [
@ide.editor.on "change:doc", () =>
@hideEl()
@ide.editor.on "resize", () =>
@diffView?.resize()
@$el.find(".track-changes-close").on "click", (e) =>
e.preventDefault
@hide()

View file

@ -891,6 +891,18 @@ i[class*="sprite-"] {
}
}
.project-description {
textarea {
min-height: 120px;
}
label {
border-bottom: 1px solid #eeeeee;
font-size: 1.2em;
line-height: 1.8em;
margin-bottom: 6px;
}
}
.btn-facebook {
color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);

View file

@ -56,7 +56,12 @@
}
}
.deleted-change-background, .deleted-change-foreground, .inserted-change-background, .change-name-marker {
.deleted-change-background,
.deleted-change-foreground,
.inserted-change-background,
.change-name-marker,
.changes-before,
.changes-after {
position: absolute;
z-index: 2;
}
@ -71,6 +76,21 @@
white-space: pre;
}
.changes-before {
top: 6px;
right: 6px;
}
.changes-after {
bottom: 6px;
right: 6px;
}
.changes-before, .changes-after {
padding: 4px 8px;
background-color: #eee;
border: 1px solid #999;
.border-radius(3px);
}
ul.change-list {
li {
position: relative;

View file

@ -12,7 +12,7 @@ describe "EditorUpdatesController", ->
@client = new MockClient()
@callback = sinon.stub()
@EditorUpdatesController = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger = { error: sinon.stub() }
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub() }
"./EditorRealTimeController" : @EditorRealTimeController = {}
"../DocumentUpdater/DocumentUpdaterHandler" : @DocumentUpdaterHandler = {}
"../Versioning/AutomaticSnapshotManager" : @AutomaticSnapshotManager = {}

View file

@ -12,18 +12,19 @@ describe "Email", ->
@settings =
email:
ses:
key: "key"
secret: "secret"
transport: "ses"
parameters:
AWSAccessKeyID: "key"
AWSSecretKey: "secret"
fromAddress: "bob@bob.com"
replyToAddress: "sally@gmail.com"
@sesClient =
sendemail: sinon.stub()
sendMail: sinon.stub()
@ses =
createClient: => @sesClient
createTransport: => @sesClient
@sender = SandboxedModule.require modulePath, requires:
'node-ses': @ses
'nodemailer': @ses
"settings-sharelatex":@settings
"logger-sharelatex":
log:->
@ -38,44 +39,44 @@ describe "Email", ->
describe "sendEmail", ->
it "should set the properties on the email to send", (done)->
@sesClient.sendemail.callsArgWith(1)
@sesClient.sendMail.callsArgWith(1)
@sender.sendEmail @opts, =>
args = @sesClient.sendemail.args[0][0]
args.message.should.equal @opts.html
args = @sesClient.sendMail.args[0][0]
args.html.should.equal @opts.html
args.to.should.equal @opts.to
args.subject.should.equal @opts.subject
done()
it "should return the error", (done)->
@sesClient.sendemail.callsArgWith(1, "error")
@sesClient.sendMail.callsArgWith(1, "error")
@sender.sendEmail {}, (err)=>
err.should.equal "error"
done()
it "should use the from address from settings", (done)->
@sesClient.sendemail.callsArgWith(1)
@sesClient.sendMail.callsArgWith(1)
@sender.sendEmail @opts, =>
args = @sesClient.sendemail.args[0][0]
args = @sesClient.sendMail.args[0][0]
args.from.should.equal @settings.email.fromAddress
done()
it "should use the reply to address from settings", (done)->
@sesClient.sendemail.callsArgWith(1)
@sesClient.sendMail.callsArgWith(1)
@sender.sendEmail @opts, =>
args = @sesClient.sendemail.args[0][0]
args = @sesClient.sendMail.args[0][0]
args.replyTo.should.equal @settings.email.replyToAddress
done()
it "should use the reply to address in options as an override", (done)->
@sesClient.sendemail.callsArgWith(1)
@sesClient.sendMail.callsArgWith(1)
@opts.replyTo = "someone@else.com"
@sender.sendEmail @opts, =>
args = @sesClient.sendemail.args[0][0]
args = @sesClient.sendMail.args[0][0]
args.replyTo.should.equal @opts.replyTo
done()