Merge branch 'master' into pr-email-tokens

Conflicts:
	app/coffee/Features/Notifications/NotificationsBuilder.coffee
	public/coffee/ide/share/controllers/ShareController.coffee
This commit is contained in:
Shane Kilkelly
2016-08-15 10:29:21 +01:00
45 changed files with 1292 additions and 837 deletions
-54
View File
@@ -386,57 +386,3 @@ module.exports = (grunt) ->
grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
grunt.registerTask 'create-admin-user', "Create a user with the given email address and make them an admin. Update in place if the user already exists", () ->
done = @async()
email = grunt.option("email")
if !email?
console.error "Usage: grunt create-admin-user --email joe@example.com"
process.exit(1)
settings = require "settings-sharelatex"
UserRegistrationHandler = require "./app/js/Features/User/UserRegistrationHandler"
OneTimeTokenHandler = require "./app/js/Features/Security/OneTimeTokenHandler"
UserRegistrationHandler.registerNewUser {
email: email
password: require("crypto").randomBytes(32).toString("hex")
}, (error, user) ->
if error? and error?.message != "EmailAlreadyRegistered"
throw error
user.isAdmin = true
user.save (error) ->
throw error if error?
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
return next(err) if err?
console.log ""
console.log """
Successfully created #{email} as an admin user.
Please visit the following URL to set a password for #{email} and log in:
#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}
"""
done()
grunt.registerTask 'delete-user', "deletes a user and all their data", () ->
done = @async()
email = grunt.option("email")
if !email?
console.error "Usage: grunt delete-user --email joe@example.com"
process.exit(1)
settings = require "settings-sharelatex"
UserGetter = require "./app/js/Features/User/UserGetter"
UserDeleter = require "./app/js/Features/User/UserDeleter"
UserGetter.getUser email:email, (error, user) ->
if error?
throw error
if !user?
console.log("user #{email} not in database, potentially already deleted")
return done()
UserDeleter.deleteUser user._id, (err)->
if err?
throw err
done()
@@ -0,0 +1,7 @@
AnalyticsManager = require "./AnalyticsManager"
module.exports = AnalyticsController =
recordEvent: (req, res, next) ->
AnalyticsManager.recordEvent req.session?.user?._id, req.params.event, req.body, (error) ->
return next(error) if error?
res.send 204
@@ -0,0 +1,43 @@
Settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
_ = require "underscore"
if !Settings.analytics?.postgres?
module.exports =
recordEvent: (user_id, event, segmentation, callback = () ->) ->
logger.log {user_id, event, segmentation}, "no event tracking configured, logging event"
callback()
else
Sequelize = require "sequelize"
options = _.extend {logging:false}, Settings.analytics.postgres
sequelize = new Sequelize(
Settings.analytics.postgres.database,
Settings.analytics.postgres.username,
Settings.analytics.postgres.password,
options
)
Event = sequelize.define("Event", {
user_id: Sequelize.STRING,
event: Sequelize.STRING,
segmentation: Sequelize.JSONB
})
module.exports =
recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
if user_id? and typeof(user_id) != "string"
user_id = user_id.toString()
if user_id == Settings.smokeTest?.userId
# Don't record smoke tests analytics
return callback()
Event
.create({ user_id, event, segmentation })
.then(
(result) -> callback(),
(error) ->
logger.err {err: error, user_id, event, segmentation}, "error recording analytics event"
callback(error)
)
sync: () -> sequelize.sync()
@@ -10,6 +10,7 @@ Settings = require "settings-sharelatex"
basicAuth = require('basic-auth-connect')
UserHandler = require("../User/UserHandler")
UserSessionsManager = require("../User/UserSessionsManager")
Analytics = require "../Analytics/AnalyticsManager"
module.exports = AuthenticationController =
login: (req, res, next = (error) ->) ->
@@ -37,6 +38,7 @@ module.exports = AuthenticationController =
return next(error) if error?
req.session.justLoggedIn = true
logger.log email: email, user_id: user._id.toString(), "successful log in"
Analytics.recordEvent user._id, "user-logged-in"
res.json redir: redir
else
AuthenticationController._recordFailedLogin()
@@ -147,6 +147,7 @@ module.exports = ClsiManager =
timeout: options.timeout
imageName: project.imageName
draft: !!options.draft
check: options.check
rootResourcePath: rootResourcePath
resources: resources
}
@@ -29,6 +29,8 @@ module.exports = CompileController =
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
if req.body?.check
options.check = if req.body.check is "validate" then "validate" else undefined
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error?
@@ -4,7 +4,7 @@ settings = require("settings-sharelatex")
ProjectGetter = require "../Project/ProjectGetter"
UserGetter = require "../User/UserGetter"
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
U = require('underscore')
_ = require('underscore')
Async = require('async')
oneMinInMs = 60 * 1000
@@ -22,24 +22,24 @@ module.exports = ReferencesHandler =
_findBibFileIds: (project) ->
ids = []
_process = (folder) ->
(folder.fileRefs or []).forEach (file) ->
_.each (folder.fileRefs or []), (file) ->
if file?.name?.match(/^.*\.bib$/)
ids.push(file._id)
(folder.folders or []).forEach (folder) ->
_.each (folder.folders or []), (folder) ->
_process(folder)
(project.rootFolder or []).forEach (rootFolder) ->
_.each (project.rootFolder or []), (rootFolder) ->
_process(rootFolder)
return ids
_findBibDocIds: (project) ->
ids = []
_process = (folder) ->
(folder.docs or []).forEach (doc) ->
_.each (folder.docs or []), (doc) ->
if doc?.name?.match(/^.*\.bib$/)
ids.push(doc._id)
(folder.folders or []).forEach (folder) ->
_.each (folder.folders or []), (folder) ->
_process(folder)
(project.rootFolder or []).forEach (rootFolder) ->
_.each (project.rootFolder or []), (rootFolder) ->
_process(rootFolder)
return ids
@@ -30,5 +30,5 @@ module.exports =
multi.get buildKey(token)
multi.del buildKey(token)
multi.exec (err, results)->
callback err, results[0]
callback err, results?[0]
@@ -27,8 +27,8 @@ module.exports = ProjectUploadController =
uploadFile: (req, res, next) ->
timer = new metrics.Timer("file-upload")
name = req.files.qqfile.originalname
path = req.files.qqfile.path
name = req.files.qqfile?.originalname
path = req.files.qqfile?.path
project_id = req.params.Project_id
folder_id = req.query.folder_id
if !name? or name.length == 0 or name.length > 150
@@ -8,6 +8,7 @@ logger = require("logger-sharelatex")
crypto = require("crypto")
EmailHandler = require("../Email/EmailHandler")
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
Analytics = require "../Analytics/AnalyticsManager"
settings = require "settings-sharelatex"
module.exports = UserRegistrationHandler =
@@ -62,6 +63,7 @@ module.exports = UserRegistrationHandler =
cb() #this can be slow, just fire it off
], (err)->
logger.log user: user, "registered"
Analytics.recordEvent user._id, "user-registered"
callback(err, user)
registerNewUserAndSendActivationEmail: (email, callback = (error, user, setNewPasswordUrl) ->) ->
+4 -2
View File
@@ -38,6 +38,7 @@ ContactRouter = require("./Features/Contacts/ContactRouter")
ReferencesController = require('./Features/References/ReferencesController')
AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear')
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
logger = require("logger-sharelatex")
_ = require("underscore")
@@ -68,7 +69,8 @@ module.exports = class Router
StaticPagesRouter.apply(webRouter, apiRouter)
RealTimeProxyRouter.apply(webRouter, apiRouter)
ContactRouter.apply(webRouter, apiRouter)
AnalyticsRouter.apply(webRouter, apiRouter)
Modules.applyRouter(webRouter, apiRouter)
@@ -280,4 +282,4 @@ module.exports = class Router
metrics.inc("client-side-error")
res.sendStatus(204)
webRouter.get '*', ErrorController.notFound
webRouter.get '*', ErrorController.notFound
-70
View File
@@ -46,76 +46,6 @@ html(itemscope, itemtype='http://schema.org/Product')
- else
script(type='text/javascript').
window.ga = function() { console.log("would send to GA", arguments) };
// Countly Analytics
if (settings.analytics && settings.analytics.countly && settings.analytics.countly.token)
script(type="text/javascript").
var Countly = Countly || {};
Countly.q = Countly.q || [];
Countly.app_key = '#{settings.analytics.countly.token}';
Countly.url = '#{settings.analytics.countly.server}';
!{ session.user ? 'Countly.device_id = "' + session.user._id + '";' : '' }
(function() {
var cly = document.createElement('script'); cly.type = 'text/javascript';
cly.async = true;
//enter url of script here
cly.src = 'https://cdnjs.cloudflare.com/ajax/libs/countly-sdk-web/16.6.0/countly.min.js';
cly.onload = function(){Countly.init()};
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s);
})();
script(type="text/javascript")
if (session && session.user)
- var name = session.user.first_name + (session.user.last_name ? ' ' + session.user.last_name : '');
| Countly.q.push(['user_details', { email: '#{session.user.email}', name: '#{name}' }]);
if (justRegistered)
| Countly.q.push(['add_event',{ key: 'user-registered' }]);
if (justLoggedIn)
| Countly.q.push(['add_event',{ key: 'user-logged-in' }]);
if (user && user.features)
- featureFlagSet = false;
if user.features.hasOwnProperty('collaborators')
| Countly.q.push([ 'userData.set', 'features-collaborators', #{ user.features.collaborators } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('compileGroup')
| Countly.q.push([ 'userData.set', 'features-compileGroup', '#{ user.features.compileGroup }' ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('compileTimeout')
| Countly.q.push([ 'userData.set', 'features-compileTimeout', #{ user.features.compileTimeout } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('dropbox')
| Countly.q.push([ 'userData.set', 'features-dropbox', #{ user.features.dropbox } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('github')
| Countly.q.push([ 'userData.set', 'features-github', #{ user.features.github } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('references')
| Countly.q.push([ 'userData.set', 'features-references', #{ user.features.references } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('templates')
| Countly.q.push([ 'userData.set', 'features-templates', #{ user.features.templates } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('versioning')
| Countly.q.push([ 'userData.set', 'features-versioning', #{ user.features.versioning } ]);
- featureFlagSet = true;
if featureFlagSet
| Countly.q.push(['userData.save'])
// End countly Analytics
script(type="text/javascript").
window.csrfToken = "#{csrfToken}";
@@ -45,7 +45,7 @@ aside.chat(
mathjax,
ng-repeat="content in message.contents track by $index"
)
span(ng-bind-html="content | linky:'_blank'")
span(ng-bind-html="content | linky:'_blank' | wrapLongWords")
.new-message
textarea(
@@ -91,12 +91,14 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
ng-if="permissions.admin",
tooltip="#{translate('share')}",
tooltip-placement="bottom",
ng-mouseenter="trackHover('share')"
ng-click="openShareProjectModal()",
ng-controller="ShareController"
)
i.fa.fa-fw.fa-group
a.btn.btn-full-height(
href,
ng-mouseenter="trackHover('track-changes')"
ng-click="toggleTrackChanges()",
ng-class="{ active: (ui.view == 'track-changes') }"
tooltip="#{translate('recent_changes')}",
@@ -108,6 +110,7 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
tooltip="#{translate('chat')}",
tooltip-placement="bottom",
ng-class="{ active: ui.chatOpen }",
ng-mouseenter="trackHover('chat')"
ng-click="toggleChat()",
ng-controller="ChatButtonController",
ng-show="!anonymous"
+11 -3
View File
@@ -25,7 +25,7 @@ div.full-size.pdf(ng-controller="PdfController")
dropdown-toggle
)
span.caret
ul.dropdown-menu.dropdown-menu-right
ul.dropdown-menu.dropdown-menu-left
li.dropdown-header #{translate("compile_mode")}
li
a(href, ng-click="draft = false")
@@ -36,6 +36,13 @@ div.full-size.pdf(ng-controller="PdfController")
i.fa.fa-fw(ng-class="{'fa-check': draft}")
|  #{translate("fast")} 
span.subdued [draft]
if user.betaProgram
li.dropdown-header #{translate("file_checks")}
li
a(href, ng-click="recompile({check:true})")
i.fa.fa-fw()
|  #{translate("run_syntax_check")}
span.beta-feature-badge
a(
href
ng-click="stop()"
@@ -115,7 +122,8 @@ div.full-size.pdf(ng-controller="PdfController")
|  
span(ng-show="entry.file") {{ entry.file }}
span(ng-show="entry.line") , line {{ entry.line }}
p.entry-message(ng-show="entry.message") {{ entry.message }}
p.entry-message(ng-show="entry.message")
| {{ entry.type }} {{ entry.message }}
.card.card-hint(
ng-if="entry.humanReadableHint"
stop-propagation="click"
@@ -126,7 +134,7 @@ div.full-size.pdf(ng-controller="PdfController")
ng-show="entry.humanReadableHint",
ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)")
.card-hint-actions.clearfix
.card-hint-ext-link(ng-if="wikiEnabled")
.card-hint-ext-link(ng-if="wikiEnabled && entry.extraInfoURL")
a(
ng-href="{{ entry.extraInfoURL }}",
ng-click="trackLogHintsLearnMore()"
@@ -143,15 +143,27 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
navigate-highlights="true"
)
.diff-deleted.text-centered(
ng-show="trackChanges.diff.deleted"
ng-show="trackChanges.diff.deleted && !trackChanges.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ trackChanges.diff.doc.name }} "})}
p
a.btn.btn-primary.btn-lg(
href,
ng-click="restoreDeletedDoc()"
ng-click="restoreDeletedDoc()",
ng-disabled="trackChanges.diff.restoreInProgress"
) #{translate("restore")}
.diff-deleted.text-centered(
ng-show="trackChanges.diff.deleted && trackChanges.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_restored", {filename:"{{ trackChanges.diff.doc.name }} "})}
p.text-serif #{translate("file_restored_back_to_editor")}
p
a.btn.btn-default(
href,
ng-click="backToEditorAfterRestore()",
) #{translate("file_restored_back_to_editor_btn")}
.loading-panel(ng-show="trackChanges.diff.loading")
i.fa.fa-spin.fa-refresh
|   #{translate("loading")}...
+2 -2
View File
@@ -106,8 +106,8 @@ module.exports = settings =
url: ""
# references:
# url: "http://localhost:3040"
# notifications:
# url: "http://localhost:3042"
notifications:
url: "http://localhost:3042"
templates:
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
+876 -627
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -46,6 +46,8 @@
"nodemailer-sendgrid-transport": "^0.2.0",
"nodemailer-ses-transport": "^1.3.0",
"optimist": "0.6.1",
"pg": "^6.0.3",
"pg-hstore": "^2.3.2",
"redback": "0.4.0",
"redis": "0.10.1",
"redis-sharelatex": "0.0.9",
@@ -53,6 +55,7 @@
"requests": "^0.1.7",
"rimraf": "2.2.6",
"sanitizer": "0.1.1",
"sequelize": "^3.2.0",
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
"sixpack-client": "^1.0.0",
"temp": "^0.8.3",
@@ -0,0 +1,25 @@
define [
"base"
], (App) ->
DEF_MIN_LENGTH = 20
_decodeHTMLEntities = (str) ->
str.replace /&#(\d+);/g, (match, dec) ->
String.fromCharCode dec;
_getWrappedWordsString = (baseStr, wrapperElName, minLength) ->
minLength = minLength || DEF_MIN_LENGTH
words = baseStr.split ' '
wordsWrapped = for word in words
if _decodeHTMLEntities(word).length >= minLength
"<#{wrapperElName} class=\"break-word\">#{word}</#{wrapperElName}>"
else
word
outputStr = wordsWrapped.join ' '
App.filter "wrapLongWords", () ->
(input, minLength) ->
_getWrappedWordsString input, "span", minLength
+11 -3
View File
@@ -9,6 +9,7 @@ define [
"ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager"
"ide/references/ReferencesManager"
"ide/SafariScrollPatcher"
"ide/settings/index"
"ide/share/index"
"ide/chat/index"
@@ -40,6 +41,7 @@ define [
PdfManager
BinaryFilesManager
ReferencesManager
SafariScrollPatcher
) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage, event_tracking) ->
@@ -72,13 +74,16 @@ define [
# Tracking code.
$scope.$watch "ui.view", (newView, oldView) ->
if newView? and newView != "editor" and newView != "pdf"
event_tracking.sendCountlyOnce "ide-open-view-#{ newView }-once"
event_tracking.sendMBOnce "ide-open-view-#{ newView }-once"
$scope.$watch "ui.chatOpen", (isOpen) ->
event_tracking.sendCountlyOnce "ide-open-chat-once" if isOpen
event_tracking.sendMBOnce "ide-open-chat-once" if isOpen
$scope.$watch "ui.leftMenuShown", (isOpen) ->
event_tracking.sendCountlyOnce "ide-open-left-menu-once" if isOpen
event_tracking.sendMBOnce "ide-open-left-menu-once" if isOpen
$scope.trackHover = (feature) ->
event_tracking.sendMBOnce "ide-hover-#{feature}-once"
# End of tracking code.
window._ide = ide
@@ -135,6 +140,9 @@ define [
catch err
console.error err
if ide.browserIsSafari
ide.safariScrollPatcher = new SafariScrollPatcher($scope)
# User can append ?ft=somefeature to url to activate a feature toggle
ide.featureToggle = location?.search?.match(/^\?ft=(\w+)$/)?[1]
@@ -0,0 +1,78 @@
define [
], () ->
class SafariScrollPatcher
constructor: ($scope) ->
@isOverAce = false # Flag to control if the pointer is over Ace.
@pdfDiv = null
@aceDiv = null
# Start listening to PDF wheel events when the pointer leaves the PDF region.
# P.S. This is the problem in a nutshell: although the pointer is elsewhere,
# wheel events keep being dispatched to the PDF.
@handlePdfDivMouseLeave = () =>
@pdfDiv.addEventListener "wheel", @dispatchToAce
# Stop listening to wheel events when the pointer enters the PDF region. If
# the pointer is over the PDF, native behaviour is adequate.
@handlePdfDivMouseEnter = () =>
@pdfDiv.removeEventListener "wheel", @dispatchToAce
# Set the "pointer over Ace" flag as false, when the mouse leaves its area.
@handleAceDivMouseLeave = () =>
@isOverAce = false
# Set the "pointer over Ace" flag as true, when the mouse enters its area.
@handleAceDivMouseEnter = () =>
@isOverAce = true
# Grab the elements (pdfDiv, aceDiv) and set the "hover" event listeners.
# If elements are already defined, clear existing event listeners and do
# the process again (grab elements, set listeners).
@setListeners = () =>
@isOverAce = false
# If elements aren't null, remove existing listeners.
if @pdfDiv?
@pdfDiv.removeEventListener @handlePdfDivMouseLeave
@pdfDiv.removeEventListener @handlePdfDivMouseEnter
if @aceDiv?
@aceDiv.removeEventListener @handleAceDivMouseLeave
@aceDiv.removeEventListener @handleAceDivMouseEnter
# Grab elements.
@pdfDiv = document.querySelector ".pdfjs-viewer" # Grab the PDF div.
@aceDiv = document.querySelector ".ace_content" # Also the editor.
# Set hover-related listeners.
@pdfDiv.addEventListener "mouseleave", @handlePdfDivMouseLeave
@pdfDiv.addEventListener "mouseenter", @handlePdfDivMouseEnter
@aceDiv.addEventListener "mouseleave", @handleAceDivMouseLeave
@aceDiv.addEventListener "mouseenter", @handleAceDivMouseEnter
# Handler for wheel events on the PDF.
# If the pointer is over Ace, grab the event, prevent default behaviour
# and dispatch it to Ace.
@dispatchToAce = (e) =>
if @isOverAce
# If this is logged, the problem just happened: the event arrived
# here (the PDF wheel handler), but it should've gone to Ace.
# Small timeout - if we dispatch immediately, an exception is thrown.
window.setTimeout(() =>
# Dispatch the exact same event to Ace (this will keep values
# values e.g. `wheelDelta` consistent with user interaction).
@aceDiv.dispatchEvent e
, 1)
# Avoid scrolling the PDF, as we assume this was intended to the
# editor.
e.preventDefault()
# "loaded" event is emitted from the pdfViewer controller $scope. This means
# that the previous PDF DOM element was destroyed and a new one is available,
# so we need to grab the elements and set the listeners again.
$scope.$on "loaded", () =>
@setListeners()
@@ -3,4 +3,5 @@ define [
"ide/chat/controllers/ChatController"
"ide/chat/controllers/ChatMessageController"
"ide/chat/directives/mathjax"
"filters/wrapLongWords"
], () ->
@@ -46,7 +46,7 @@ define [
done = () =>
if options.gotoLine?
@$scope.$broadcast "editor:gotoLine", options.gotoLine
@$scope.$broadcast "editor:gotoLine", options.gotoLine, options.gotoColumn
if doc.id == @$scope.editor.open_doc_id and !options.forceReopen
@$scope.$apply () =>
@@ -23,10 +23,10 @@ define [], () ->
@storeCursorPosition(@editor.getSession())
@storeScrollTopPosition(@editor.getSession())
@$scope.$on "#{@$scope.name}:gotoLine", (editor, value) =>
if value?
@$scope.$on "#{@$scope.name}:gotoLine", (editor, line, column) =>
if line?
setTimeout () =>
@gotoLine(value)
@gotoLine(line, column)
, 10 # Hack: Must happen after @gotoStoredPosition
storeScrollTopPosition: (session) ->
@@ -53,6 +53,6 @@ define [], () ->
@editor.getSession().setScrollTop(pos.scrollTop or 0)
delete @ignoreCursorPositionChanges
gotoLine: (line) ->
@editor.gotoLine(line)
@editor.focus()
gotoLine: (line, column) ->
@editor.gotoLine(line, column)
@editor.focus()
@@ -4,7 +4,7 @@ define [
], (App) ->
App.controller "HotkeysController", ($scope, $modal, event_tracking) ->
$scope.openHotkeysModal = ->
event_tracking.sendCountly "ide-open-hotkeys-modal"
event_tracking.sendMB "ide-open-hotkeys-modal"
$modal.open {
templateUrl: "hotkeysModalTemplate"
@@ -3,7 +3,10 @@ define [
"ide/human-readable-logs/HumanReadableLogsRules"
], (LogParser, ruleset) ->
parse : (rawLog, options) ->
parsedLogEntries = LogParser.parse(rawLog, options)
if typeof rawLog is 'string'
parsedLogEntries = LogParser.parse(rawLog, options)
else
parsedLogEntries = rawLog
_getRule = (logMessage) ->
return rule for rule in ruleset when rule.regexToMatch.test logMessage
@@ -12,7 +15,12 @@ define [
ruleDetails = _getRule entry.message
if (ruleDetails?)
entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch?
if ruleDetails.ruleId?
entry.ruleId = ruleDetails.ruleId
else if ruleDetails.regexToMatch?
entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1)
if ruleDetails.newMessage?
entry.message = entry.message.replace ruleDetails.regexToMatch, ruleDetails.newMessage
entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?
@@ -88,4 +88,19 @@ define -> [
humanReadableHint: """
You have used a font command which is only available in math mode. To use this command, you must be in maths mode (E.g. $ \u2026 $ or \\begin{math} \u2026 \\end{math}). If you want to use it outside of math mode, use the text version instead: \\textrm, \\textit, etc.
"""
,
ruleId: "hint_mismatched_environment"
regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/
newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}"
humanReadableHint: """
You have used \\begin{...} without a corresponding \\end{...}.
"""
,
ruleId: "hint_mismatched_brackets"
regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/
newMessage: "Error: brackets do not match, found '$2' instead of '$1'"
humanReadableHint: """
You have used an open bracket without a corresponding close bracket.
"""
]
@@ -36,11 +36,11 @@ define [
$scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts
$scope.trackLogHintsLearnMore = () ->
event_tracking.sendCountly "logs-hints-learn-more"
event_tracking.sendMB "logs-hints-learn-more"
trackLogHintsFeedback = (isPositive, hintId) ->
event_tracking.send "log-hints", (if isPositive then "feedback-positive" else "feedback-negative"), hintId
event_tracking.sendCountly (if isPositive then "log-hints-feedback-positive" else "log-hints-feedback-negative"), { hintId }
event_tracking.sendMB (if isPositive then "log-hints-feedback-positive" else "log-hints-feedback-negative"), { hintId }
$scope.trackLogHintsNegFeedbackDetails = (hintId, feedbackOpt, feedbackOtherVal) ->
logHintsFeedback.submitFeedback hintId, feedbackOpt, feedbackOtherVal
@@ -86,11 +86,15 @@ define [
return $http.post url, {
rootDoc_id: options.rootDocOverride_id or null
draft: $scope.draft
check: if options.check then "validate" else null
_csrf: window.csrfToken
}, {params: params}
parseCompileResponse = (response) ->
# keep last url
last_pdf_url = $scope.pdf.url
# Reset everything
$scope.pdf.error = false
$scope.pdf.timedout = false
@@ -120,11 +124,23 @@ define [
if response.status == "timedout"
$scope.pdf.view = 'errors'
$scope.pdf.timedout = true
fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
fetchLogs(fileByPath)
else if response.status == "terminated"
$scope.pdf.view = 'errors'
$scope.pdf.compileTerminated = true
fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
fetchLogs(fileByPath)
else if response.status in ["validation-fail", "validation-pass"]
$scope.pdf.view = 'pdf'
$scope.pdf.compileExited = true
$scope.pdf.url = last_pdf_url
$scope.shouldShowLogs = true
fetchLogs(fileByPath, { validation: true })
else if response.status == "exited"
$scope.pdf.view = 'pdf'
$scope.pdf.compileExited = true
$scope.pdf.url = last_pdf_url
$scope.shouldShowLogs = true
fetchLogs(fileByPath)
else if response.status == "autocompile-backoff"
$scope.pdf.view = 'uncompiled'
else if response.status == "project-too-large"
@@ -134,7 +150,7 @@ define [
$scope.pdf.view = 'errors'
$scope.pdf.failure = true
$scope.shouldShowLogs = true
fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
fetchLogs(fileByPath)
else if response.status == 'clsi-maintenance'
$scope.pdf.view = 'errors'
$scope.pdf.clsiMaintenance = true
@@ -165,7 +181,7 @@ define [
qs.popupDownload = true
$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)
fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
fetchLogs(fileByPath)
IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
$scope.pdf.outputFiles = []
@@ -186,7 +202,13 @@ define [
}
fetchLogs = (logFile, blgFile) ->
fetchLogs = (fileByPath, options) ->
if options?.validation
chktexFile = fileByPath['output.chktex']
else
logFile = fileByPath['output.log']
blgFile = fileByPath['output.blg']
getFile = (name, file) ->
opts =
@@ -213,6 +235,8 @@ define [
accumulateResults = (newEntries) ->
for key in ['all', 'errors', 'warnings']
if newEntries.type?
entry.type = newEntries.type for entry in newEntries[key]
logEntries[key] = logEntries[key].concat newEntries[key]
# use the parsers for each file type
@@ -222,10 +246,24 @@ define [
all = [].concat errors, warnings, typesetting
accumulateResults {all, errors, warnings}
processChkTex = (log) ->
errors = []
warnings = []
for line in log.split("\n")
if m = line.match /^(\S+):(\d+):(\d+): (Error|Warning): (.*)/
result = { file:m[1], line:m[2], column:m[3], level:m[4].toLowerCase(), message: "#{m[4]}: #{m[5]}"}
if result.level is 'error'
errors.push result
else
warnings.push result
all = [].concat errors, warnings
logHints = HumanReadableLogs.parse {type: "Validation", all, errors, warnings}
accumulateResults logHints
processBiber = (log) ->
{errors, warnings} = BibLogParser.parse(log, {})
all = [].concat errors, warnings
accumulateResults {all, errors, warnings}
accumulateResults {type: "BibTeX", all, errors, warnings}
# output the results
handleError = () ->
@@ -248,19 +286,35 @@ define [
}
# retrieve the logfile and process it
response = getFile('output.log', logFile)
.success processLog
.error handleError
if logFile?
response = getFile('output.log', logFile)
.then (response) -> processLog(response.data)
if blgFile? # retrieve the blg file if present
response.success () ->
getFile('output.blg', blgFile)
# ignore errors in biber file
.success processBiber
# display the combined result
.then annotateFiles
else # otherwise just display the result
response.success annotateFiles
if blgFile? # retrieve the blg file if present
response = response.then () ->
getFile('output.blg', blgFile)
.then(
(response) -> processBiber(response.data),
() -> true # ignore errors in biber file
)
if response?
response.catch handleError
else
handleError()
if chktexFile?
getChkTex = () ->
getFile('output.chktex', chktexFile)
.then (response) -> processChkTex(response.data)
# always retrieve the chktex file if present
if response?
response = response.then getChkTex, getChkTex
else
response = getChkTex()
# display the combined result
response.finally annotateFiles
getRootDocOverride_id = () ->
doc = ide.editorManager.getCurrentDocValue()
@@ -284,7 +338,7 @@ define [
$scope.recompile = (options = {}) ->
return if $scope.pdf.compiling
event_tracking.sendCountlySampled "editor-recompile-sampled", options
event_tracking.sendMBSampled "editor-recompile-sampled", options
$scope.pdf.compiling = true
@@ -333,7 +387,7 @@ define [
$scope.toggleLogs = () ->
$scope.shouldShowLogs = !$scope.shouldShowLogs
event_tracking.sendCountlyOnce "ide-open-logs-once" if $scope.shouldShowLogs
event_tracking.sendMBOnce "ide-open-logs-once" if $scope.shouldShowLogs
$scope.showPdf = () ->
$scope.pdf.view = "pdf"
@@ -341,7 +395,7 @@ define [
$scope.toggleRawLog = () ->
$scope.pdf.showRawLog = !$scope.pdf.showRawLog
event_tracking.sendCountly "logs-view-raw" if $scope.pdf.showRawLog
event_tracking.sendMB "logs-view-raw" if $scope.pdf.showRawLog
$scope.openClearCacheModal = () ->
modalInstance = $modal.open(
@@ -376,7 +430,7 @@ define [
$scope.startFreeTrial = (source) ->
ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source)
event_tracking.sendCountly "subscription-start-trial", { source }
event_tracking.sendMB "subscription-start-trial", { source }
window.open("/user/subscription/new?planCode=student_free_trial_7_days")
$scope.startedFreeTrial = true
@@ -499,12 +553,14 @@ define [
App.controller "PdfLogEntryController", ["$scope", "ide", "event_tracking", ($scope, ide, event_tracking) ->
$scope.openInEditor = (entry) ->
event_tracking.sendCountlyOnce "logs-jump-to-location-once"
event_tracking.sendMBOnce "logs-jump-to-location-once"
entity = ide.fileTreeManager.findEntityByPath(entry.file)
return if !entity? or entity.type != "doc"
if entry.line?
line = entry.line
ide.editorManager.openDoc(entity, gotoLine: line)
if entry.column?
column = entry.column
ide.editorManager.openDoc(entity, gotoLine: line, gotoColumn: column)
]
App.controller 'ClearCacheModalController', ["$scope", "$modalInstance", ($scope, $modalInstance) ->
@@ -8,7 +8,7 @@ define [
for key in Object.keys(data)
changedSetting = key
changedSettingVal = data[key]
event_tracking.sendCountly "setting-changed", { changedSetting, changedSettingVal }
event_tracking.sendMB "setting-changed", { changedSetting, changedSettingVal }
# End of tracking code.
data._csrf = window.csrfToken
@@ -20,7 +20,7 @@ define [
for key in Object.keys(data)
changedSetting = key
changedSettingVal = data[key]
event_tracking.sendCountly "project-setting-changed", { changedSetting, changedSettingVal}
event_tracking.sendMB "project-setting-changed", { changedSetting, changedSettingVal}
# End of tracking code.
data._csrf = window.csrfToken
@@ -32,7 +32,7 @@ define [
for key in Object.keys(data)
changedSetting = key
changedSettingVal = data[key]
event_tracking.sendCountly "project-admin-setting-changed", { changedSetting, changedSettingVal }
event_tracking.sendMB "project-admin-setting-changed", { changedSetting, changedSettingVal }
# End of tracking code.
data._csrf = window.csrfToken
@@ -4,7 +4,7 @@ define [
App.controller "ShareController", ["$scope", "$modal", "ide", "projectInvites", "projectMembers", "event_tracking",
($scope, $modal, ide, projectInvites, projectMembers, event_tracking) ->
$scope.openShareProjectModal = () ->
event_tracking.sendCountlyOnce "ide-open-share-modal-once"
event_tracking.sendMBOnce "ide-open-share-modal-once"
$modal.open(
templateUrl: "shareProjectModalTemplate"
@@ -117,6 +117,9 @@ define [
diff.error = true
else
diff.deleted = true
diff.restoreInProgress = false
diff.restoreDeletedSuccess = false
diff.restoredDocNewId = null
restoreDeletedDoc: (doc) ->
url = "/project/#{@$scope.project_id}/doc/#{doc.id}/restore"
@@ -1,13 +1,21 @@
define [
"base"
], (App) ->
App.controller "TrackChangesDiffController", ($scope, $modal, ide) ->
App.controller "TrackChangesDiffController", ($scope, $modal, ide, event_tracking) ->
$scope.restoreDeletedDoc = () ->
ide.trackChangesManager.restoreDeletedDoc(
$scope.trackChanges.diff.doc
)
event_tracking.sendMB "track-changes-restore-deleted"
$scope.trackChanges.diff.restoreInProgress = true
ide.trackChangesManager
.restoreDeletedDoc(
$scope.trackChanges.diff.doc
)
.success (response) ->
$scope.trackChanges.diff.restoredDocNewId = response.doc_id
$scope.trackChanges.diff.restoreInProgress = false
$scope.trackChanges.diff.restoreDeletedSuccess = true
$scope.openRestoreDiffModal = () ->
event_tracking.sendMB "track-changes-restore-modal"
$modal.open {
templateUrl: "trackChangesRestoreDiffModalTemplate"
controller: "TrackChangesRestoreDiffModalController"
@@ -15,13 +23,17 @@ define [
diff: () -> $scope.trackChanges.diff
}
App.controller "TrackChangesRestoreDiffModalController", ($scope, $modalInstance, diff, ide) ->
$scope.backToEditorAfterRestore = () ->
ide.editorManager.openDoc({ id: $scope.trackChanges.diff.restoredDocNewId })
App.controller "TrackChangesRestoreDiffModalController", ($scope, $modalInstance, diff, ide, event_tracking) ->
$scope.state =
inflight: false
$scope.diff = diff
$scope.restore = () ->
event_tracking.sendMB "track-changes-restored"
$scope.state.inflight = true
ide.trackChangesManager
.restoreDiff(diff)
@@ -78,7 +78,7 @@ define [
$scope.recalculateSelectedUpdates()
]
App.controller "TrackChangesListItemController", ["$scope", ($scope) ->
App.controller "TrackChangesListItemController", ["$scope", "event_tracking", ($scope, event_tracking) ->
$scope.$watch "update.selectedFrom", (selectedFrom, oldSelectedFrom) ->
if selectedFrom
for update in $scope.trackChanges.updates
@@ -92,6 +92,7 @@ define [
$scope.recalculateSelectedUpdates()
$scope.select = () ->
event_tracking.sendMB "track-changes-view-change"
$scope.update.selectedTo = true
$scope.update.selectedFrom = true
@@ -6,7 +6,7 @@ define [
$scope.buttonClass = "btn-primary"
$scope.startFreeTrial = (source, couponCode) ->
event_tracking.sendCountly "subscription-start-trial", { source }
event_tracking.sendMB "subscription-start-trial", { source }
w = window.open()
sixpack.convert "track-changes-discount", ->
@@ -24,7 +24,7 @@ define [
url :"/learn/kb/#{page_underscored}"
name : hit._highlightResult.pageName.value
event_tracking.sendCountly "contact-form-suggestions-shown" if results.hits.length
event_tracking.sendMB "contact-form-suggestions-shown" if results.hits.length
$scope.$applyAsync () ->
$scope.suggestions = suggestions
@@ -60,7 +60,7 @@ define [
$scope.suggestions = [];
$scope.clickSuggestionLink = (url) ->
event_tracking.sendCountly "contact-form-suggestions-clicked", { url }
event_tracking.sendMB "contact-form-suggestions-clicked", { url }
$scope.close = () ->
$modalInstance.close()
+20 -10
View File
@@ -2,9 +2,14 @@ define [
"base"
"modules/localStorage"
], (App) ->
CACHE_KEY = "countlyEvents"
CACHE_KEY = "mbEvents"
App.factory "event_tracking", (localStorage) ->
send = (category, action, attributes = {})->
ga('send', 'event', category, action)
event_name = "#{action}-#{category}"
Intercom?("trackEvent", event_name, attributes)
App.factory "event_tracking", ($http, localStorage) ->
_getEventCache = () ->
eventCache = localStorage CACHE_KEY
@@ -29,18 +34,23 @@ define [
send: (category, action, label, value)->
ga('send', 'event', category, action, label, value)
sendCountly: (key, segmentation) ->
eventData = { key }
eventData.segmentation = segmentation if segmentation?
Countly?.q.push([ "add_event", eventData ])
sendMB: (key, segmentation = {}) ->
$http {
url: "/event/#{key}",
method: "POST",
data: segmentation
headers: {
"X-CSRF-Token": window.csrfToken
}
}
sendCountlySampled: (key, segmentation) ->
@sendCountly key, segmentation if Math.random() < .01
sendMBSampled: (key, segmentation) ->
@sendMB key, segmentation if Math.random() < .01
sendCountlyOnce: (key, segmentation) ->
sendMBOnce: (key, segmentation) ->
if ! _eventInCache(key)
_addEventToCache(key)
@sendCountly key, segmentation
@sendMB key, segmentation
}
# App.directive "countlyTrack", () ->
@@ -51,7 +51,7 @@ define [
.done()
pricing.on "change", =>
event_tracking.sendCountly "subscription-form", { plan : pricing.items.plan.code }
event_tracking.sendMB "subscription-form", { plan : pricing.items.plan.code }
$scope.planName = pricing.items.plan.name
$scope.price = pricing.price
@@ -125,7 +125,7 @@ define [
state: $scope.data.state
postal_code: $scope.data.postal_code
event_tracking.sendCountly "subscription-form-submitted", {
event_tracking.sendMB "subscription-form-submitted", {
currencyCode : postData.subscriptionDetails.currencyCode,
plan_code : postData.subscriptionDetails.plan_code,
coupon_code : postData.subscriptionDetails.coupon_code,
@@ -135,7 +135,7 @@ define [
$http.post("/user/subscription/create", postData)
.success (data, status, headers)->
sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
event_tracking.sendCountly "subscription-submission-success"
event_tracking.sendMB "subscription-submission-success"
window.location.href = "/user/subscription/thank-you"
.error (data, status, headers)->
$scope.processing = false
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -129,6 +129,10 @@
}
}
.break-word {
word-break: break-all;
}
.editor-dark {
.chat {
.new-message {
@@ -267,6 +267,10 @@
}
}
.diff-deleted {
padding-top: 15px;
}
.editor-dark {
#trackChanges {
aside.change-list {
@@ -19,6 +19,7 @@ describe "AuthenticationController", ->
"../../infrastructure/Metrics": @Metrics = { inc: sinon.stub() }
"../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() }
"../User/UserHandler": @UserHandler = {setupLoginData:sinon.stub()}
"../Analytics/AnalyticsManager": @AnalyticsManager = { recordEvent: sinon.stub() }
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"settings-sharelatex": {}
"../User/UserSessionsManager": @UserSessionsManager =
@@ -100,6 +101,11 @@ describe "AuthenticationController", ->
.calledWith(email: @email.toLowerCase(), user_id: @user._id.toString(), "successful log in")
.should.equal true
it "should track the login event", ->
@AnalyticsManager.recordEvent
.calledWith(@user._id, "user-logged-in")
.should.equal true
describe 'when the user is not authenticated', ->
beforeEach ->
@@ -174,6 +174,7 @@ describe "ClsiManager", ->
timeout : 100
imageName: @image
draft: false
check: undefined
rootResourcePath: "main.tex"
resources: [{
path: "main.tex"
@@ -322,6 +322,11 @@ describe 'ReferencesHandler', ->
result = @handler._findBibDocIds(@fakeProject)
expect(result).to.deep.equal @expectedIds
it 'should not error with a non array of folders from dirty data', ->
@fakeProject.rootFolder[0].folders[0].folders = {}
result = @handler._findBibDocIds(@fakeProject)
expect(result).to.deep.equal @expectedIds
describe '_findBibFileIds', ->
beforeEach ->
@@ -33,6 +33,7 @@ describe "UserRegistrationHandler", ->
"crypto": @crypto = {}
"../Email/EmailHandler": @EmailHandler
"../Security/OneTimeTokenHandler": @OneTimeTokenHandler
"../Analytics/AnalyticsManager": @AnalyticsManager = { recordEvent: sinon.stub() }
"settings-sharelatex": @settings = {siteUrl: "http://sl.example.com"}
@passingRequest = {email:"something@email.com", password:"123"}
@@ -132,6 +133,13 @@ describe "UserRegistrationHandler", ->
@NewsLetterManager.subscribe.calledWith(@user).should.equal true
done()
it "should track the registration event", (done)->
@handler.registerNewUser @passingRequest, (err)=>
@AnalyticsManager.recordEvent
.calledWith(@user._id, "user-registered")
.should.equal true
done()
it "should call the ReferalAllocator", (done)->
done()