Просмотр исходного кода

Merge branch 'master' into pr-latex-linter-onboarding

Paulo Reis 10 лет назад
Родитель
Сommit
97b92928dc
33 измененных файлов с 840 добавлено и 321 удалено
  1. 0 21
      services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
  2. 49 20
      services/web/app/coffee/Features/Email/EmailBuilder.coffee
  3. 18 6
      services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
  4. 2 3
      services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
  5. 19 5
      services/web/app/coffee/Features/User/UserController.coffee
  6. 4 0
      services/web/app/coffee/infrastructure/LoggerSerializers.coffee
  7. 1 1
      services/web/app/coffee/router.coffee
  8. 3 2
      services/web/app/views/layout.jade
  9. 76 32
      services/web/app/views/project/editor/history.jade
  10. 6 5
      services/web/app/views/subscriptions/dashboard.jade
  11. 137 125
      services/web/app/views/subscriptions/plans.jade
  12. 1 1
      services/web/app/views/subscriptions/successful_subscription.jade
  13. 18 2
      services/web/app/views/user/settings.jade
  14. 9 4
      services/web/public/coffee/ide.coffee
  15. 29 2
      services/web/public/coffee/ide/connection/ConnectionManager.coffee
  16. 33 7
      services/web/public/coffee/ide/editor/directives/aceEditor.coffee
  17. 9 1
      services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
  18. 4 4
      services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
  19. 6 1
      services/web/public/coffee/ide/references/ReferencesManager.coffee
  20. 21 5
      services/web/public/coffee/main/account-settings.coffee
  21. 29 9
      services/web/public/coffee/main/account-upgrade.coffee
  22. 5 2
      services/web/public/coffee/main/new-subscription.coffee
  23. 189 12
      services/web/public/coffee/main/plans.coffee
  24. 26 11
      services/web/public/coffee/main/subscription-dashboard.coffee
  25. BIN
      services/web/public/img/teasers/history/teaser-history.png
  26. 28 8
      services/web/public/js/ace-1.2.5/mode-latex.js
  27. 1 1
      services/web/public/js/ace-1.2.5/worker-latex.js
  28. 8 1
      services/web/public/stylesheets/app/account-settings.less
  29. 2 0
      services/web/public/stylesheets/app/editor.less
  30. 13 0
      services/web/public/stylesheets/app/editor/history.less
  31. 0 24
      services/web/test/UnitTests/coffee/Email/EmailBuilderTests.coffee
  32. 23 1
      services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
  33. 71 5
      services/web/test/UnitTests/coffee/User/UserControllerTests.coffee

+ 0 - 21
services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee

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

+ 49 - 20
services/web/app/coffee/Features/Email/EmailBuilder.coffee

@@ -7,10 +7,18 @@ settings = require("settings-sharelatex")
 
 
 templates = {}
 templates = {}
 
 
+
 templates.registered =
 templates.registered =
 	subject:  _.template "Activate your #{settings.appName} Account"
 	subject:  _.template "Activate your #{settings.appName} Account"
 	layout: PersonalEmailLayout
 	layout: PersonalEmailLayout
 	type: "notification"
 	type: "notification"
+	plainTextTemplate: _.template """
+Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".
+
+Click here to set your password and log in: <%= setNewPasswordUrl %>
+
+If you have any questions or problems, please contact #{settings.adminEmail}
+"""
 	compiledTemplate: _.template """
 	compiledTemplate: _.template """
 <p>Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".</p>
 <p>Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".</p>
 
 
@@ -19,10 +27,24 @@ templates.registered =
 <p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
 <p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
 """
 """
 
 
+
 templates.canceledSubscription =
 templates.canceledSubscription =
 	subject:  _.template "ShareLaTeX thoughts"
 	subject:  _.template "ShareLaTeX thoughts"
 	layout: PersonalEmailLayout
 	layout: PersonalEmailLayout
 	type:"lifecycle"
 	type:"lifecycle"
+	plainTextTemplate: _.template """
+Hi <%= first_name %>,
+
+I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?:
+
+https://sharelatex.typeform.com/to/f5lBiZ
+
+Thank you in advance.
+
+Henry
+
+ShareLaTeX Co-founder
+"""
 	compiledTemplate: _.template '''
 	compiledTemplate: _.template '''
 <p>Hi <%= first_name %>,</p>
 <p>Hi <%= first_name %>,</p>
 
 
@@ -36,10 +58,26 @@ ShareLaTeX Co-founder
 </p>
 </p>
 '''
 '''
 
 
+
 templates.passwordResetRequested =
 templates.passwordResetRequested =
 	subject:  _.template "Password Reset - #{settings.appName}"
 	subject:  _.template "Password Reset - #{settings.appName}"
 	layout: NotificationEmailLayout
 	layout: NotificationEmailLayout
 	type:"notification"
 	type:"notification"
+	plainTextTemplate: _.template """
+Password Reset
+
+We got a request to reset your #{settings.appName} password.
+
+Click this link to reset your password: <%= setNewPasswordUrl %>
+
+If you ignore this message, your password won't be changed.
+
+If you didn't request a password reset, let us know.
+
+Thank you
+
+#{settings.appName} - <%= siteUrl %>
+"""
 	compiledTemplate: _.template """
 	compiledTemplate: _.template """
 <h2>Password Reset</h2>
 <h2>Password Reset</h2>
 <p>
 <p>
@@ -66,26 +104,6 @@ If you didn't request a password reset, let us know.
 <p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
 <p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
 """
 """
 
 
-templates.projectSharedWithYou =
-	subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
-	layout: NotificationEmailLayout
-	type:"notification"
-	compiledTemplate: _.template """
-<p>Hi, <%= owner.email %> wants to share <a href="<%= project.url %>">'<%= project.name %>'</a> with you</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="<%= project.url %>" 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>
-		</div>
-	</div>
-</center>
-<p> Thank you</p>
-<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
-"""
 
 
 templates.projectInvite =
 templates.projectInvite =
 	subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
 	subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
@@ -113,10 +131,20 @@ Thank you
 <p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
 <p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
 """
 """
 
 
+
 templates.completeJoinGroupAccount =
 templates.completeJoinGroupAccount =
 	subject: _.template "Verify Email to join <%= group_name %> group"
 	subject: _.template "Verify Email to join <%= group_name %> group"
 	layout: NotificationEmailLayout
 	layout: NotificationEmailLayout
 	type:"notification"
 	type:"notification"
+	plainTextTemplate: _.template """
+Hi, please verify your email to join the <%= group_name %> and get your free premium account
+
+Click this link to verify now: <%= completeJoinUrl %>
+
+Thank You
+
+#{settings.appName} - <%= siteUrl %>
+"""
 	compiledTemplate: _.template """
 	compiledTemplate: _.template """
 <p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p>
 <p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p>
 <center>
 <center>
@@ -134,6 +162,7 @@ templates.completeJoinGroupAccount =
 <p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
 <p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
 """
 """
 
 
+
 module.exports =
 module.exports =
 	templates: templates
 	templates: templates
 
 

+ 18 - 6
services/web/app/coffee/Features/Subscription/SubscriptionController.coffee

@@ -8,6 +8,7 @@ Settings   = require 'settings-sharelatex'
 logger     = require('logger-sharelatex')
 logger     = require('logger-sharelatex')
 GeoIpLookup = require("../../infrastructure/GeoIpLookup")
 GeoIpLookup = require("../../infrastructure/GeoIpLookup")
 SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
 SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
+UserGetter = require "../User/UserGetter"
 
 
 module.exports = SubscriptionController =
 module.exports = SubscriptionController =
 
 
@@ -21,14 +22,25 @@ module.exports = SubscriptionController =
 		if req.query.v?
 		if req.query.v?
 			viewName = "#{viewName}_#{req.query.v}"
 			viewName = "#{viewName}_#{req.query.v}"
 		logger.log viewName:viewName, "showing plans page"
 		logger.log viewName:viewName, "showing plans page"
+		currentUser = null
 		GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
 		GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
 			return next(err) if err?
 			return next(err) if err?
-			res.render viewName,
-				title: "plans_and_pricing"
-				plans: plans
-				baseUrl: baseUrl
-				gaExperiments: Settings.gaExperiments.plansPage
-				recomendedCurrency:recomendedCurrency
+			render = () ->
+				res.render viewName,
+					title: "plans_and_pricing"
+					plans: plans
+					baseUrl: baseUrl
+					gaExperiments: Settings.gaExperiments.plansPage
+					recomendedCurrency:recomendedCurrency
+					shouldABTestPlans: currentUser == null or (currentUser?.signUpDate? and currentUser.signUpDate >= (new Date('2016-10-27')))
+			user_id = AuthenticationController.getLoggedInUserId(req)
+			if user_id?
+				UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->
+					return next(err) if err?
+					currentUser = user
+					render()
+			else
+				render()
 
 
 	#get to show the recurly.js page
 	#get to show the recurly.js page
 	paymentPage: (req, res, next) ->
 	paymentPage: (req, res, next) ->

+ 2 - 3
services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee

@@ -44,7 +44,7 @@ module.exports =
 		allPlans = {}
 		allPlans = {}
 		plans.forEach (plan)->
 		plans.forEach (plan)->
 			allPlans[plan.planCode] = plan
 			allPlans[plan.planCode] = plan
-			
+
 		result =
 		result =
 			allPlans: allPlans
 			allPlans: allPlans
 
 
@@ -54,7 +54,7 @@ module.exports =
 
 
 		result.studentAccounts = _.filter plans, (plan)->
 		result.studentAccounts = _.filter plans, (plan)->
 			plan.planCode.indexOf("student") != -1
 			plan.planCode.indexOf("student") != -1
-			
+
 		result.groupMonthlyPlans = _.filter plans, (plan)->
 		result.groupMonthlyPlans = _.filter plans, (plan)->
 			plan.groupPlan and !plan.annual
 			plan.groupPlan and !plan.annual
 
 
@@ -68,4 +68,3 @@ module.exports =
 			!plan.groupPlan and plan.annual and plan.planCode.indexOf("student") == -1
 			!plan.groupPlan and plan.annual and plan.planCode.indexOf("student") == -1
 
 
 		return result
 		return result
-

+ 19 - 5
services/web/app/coffee/Features/User/UserController.coffee

@@ -15,12 +15,26 @@ settings = require "settings-sharelatex"
 
 
 module.exports = UserController =
 module.exports = UserController =
 
 
-	deleteUser: (req, res)->
+	tryDeleteUser: (req, res, next) ->
 		user_id = AuthenticationController.getLoggedInUserId(req)
 		user_id = AuthenticationController.getLoggedInUserId(req)
-		UserDeleter.deleteUser user_id, (err)->
-			if !err?
+		password = req.body.password
+		logger.log {user_id}, "trying to delete user account"
+		if !password? or password == ''
+			logger.err {user_id}, 'no password supplied for attempt to delete account'
+			return res.sendStatus(403)
+		AuthenticationManager.authenticate {_id: user_id}, password, (err, user) ->
+			if err?
+				logger.err {user_id}, 'error authenticating during attempt to delete account'
+				return next(err)
+			if !user
+				logger.err {user_id}, 'auth failed during attempt to delete account'
+				return res.sendStatus(403)
+			UserDeleter.deleteUser user_id, (err) ->
+				if err?
+					logger.err {user_id}, "error while deleting user account"
+					return next(err)
 				req.session?.destroy()
 				req.session?.destroy()
-			res.sendStatus(200)
+				res.sendStatus(200)
 
 
 	unsubscribe: (req, res)->
 	unsubscribe: (req, res)->
 		user_id = AuthenticationController.getLoggedInUserId(req)
 		user_id = AuthenticationController.getLoggedInUserId(req)
@@ -143,7 +157,7 @@ module.exports = UserController =
 									type:'success'
 									type:'success'
 									text:'Your password has been changed'
 									text:'Your password has been changed'
 			else
 			else
-				logger.log user: user, "current password wrong"
+				logger.log user_id: user_id, "current password wrong"
 				res.send
 				res.send
 					message:
 					message:
 					  type:'error'
 					  type:'error'

+ 4 - 0
services/web/app/coffee/infrastructure/LoggerSerializers.coffee

@@ -1,5 +1,7 @@
 module.exports =
 module.exports =
 	user: (user) ->
 	user: (user) ->
+		if !user?
+			return null
 		if !user._id?
 		if !user._id?
 			user = {_id : user}
 			user = {_id : user}
 		return {
 		return {
@@ -10,6 +12,8 @@ module.exports =
 		}
 		}
 	
 	
 	project: (project) ->
 	project: (project) ->
+		if !project?
+			return null
 		if !project._id?
 		if !project._id?
 			project = {_id: project}
 			project = {_id: project}
 		return {
 		return {

+ 1 - 1
services/web/app/coffee/router.coffee

@@ -92,7 +92,7 @@ module.exports = class Router
 		webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions
 		webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions
 
 
 		webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
 		webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
-		webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser
+		webRouter.post '/user/delete', AuthenticationController.requireLogin(), UserController.tryDeleteUser
 
 
 		webRouter.get  '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
 		webRouter.get  '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
 		apiRouter.get  '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
 		apiRouter.get  '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo

+ 3 - 2
services/web/app/views/layout.jade

@@ -51,8 +51,6 @@ html(itemscope, itemtype='http://schema.org/Product')
 		script(type="text/javascript").
 		script(type="text/javascript").
 			window.csrfToken = "#{csrfToken}";
 			window.csrfToken = "#{csrfToken}";
 			
 			
-		block scripts
-
 		script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
 		script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
 		script(type="text/javascript").
 		script(type="text/javascript").
 			var noCdnKey = "nocdn=true"
 			var noCdnKey = "nocdn=true"
@@ -61,6 +59,9 @@ html(itemscope, itemtype='http://schema.org/Product')
 			if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
 			if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
 				window.location.search += '&'+noCdnKey;
 				window.location.search += '&'+noCdnKey;
 			}
 			}
+
+		block scripts
+
 		script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
 		script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
 
 
 		script.
 		script.

+ 76 - 32
services/web/app/views/project/editor/history.jade

@@ -1,41 +1,85 @@
 div#history(ng-show="ui.view == 'history'")
 div#history(ng-show="ui.view == 'history'")
 	span(ng-controller="HistoryPremiumPopup")
 	span(ng-controller="HistoryPremiumPopup")
-		.upgrade-prompt(ng-show="!project.features.versioning")
-			.message(ng-show="project.owner._id == user.id")
-				p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
-				p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
-				ul.list-unstyled
-					li 
-						i.fa.fa-check &nbsp;
-						| #{translate("unlimited_projects")}
-					
-					li
-						i.fa.fa-check &nbsp;
-						| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
-					
-					li 
-						i.fa.fa-check &nbsp;
-						| #{translate("full_doc_history")}
-					
-					li 
-						i.fa.fa-check &nbsp;
-						| #{translate("sync_to_dropbox")}
+		.upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'")
+			
+			div(sixpack-switch="teaser-history")
+				.message(
+					sixpack-default
+					ng-show="project.owner._id == user.id"
+				)
+					p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
+					p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
+					ul.list-unstyled
+						li 
+							i.fa.fa-check &nbsp;
+							| #{translate("unlimited_projects")}
+						
+						li
+							i.fa.fa-check &nbsp;
+							| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
+						
+						li 
+							i.fa.fa-check &nbsp;
+							| #{translate("full_doc_history")}
+						
+						li 
+							i.fa.fa-check &nbsp;
+							| #{translate("sync_to_dropbox")}
 
 
-					li 
-						i.fa.fa-check &nbsp;
-						| #{translate("sync_to_github")}
+						li 
+							i.fa.fa-check &nbsp;
+							| #{translate("sync_to_github")}
 
 
-					li  
-						i.fa.fa-check &nbsp;
-						|#{translate("compile_larger_projects")}
+						li  
+							i.fa.fa-check &nbsp;
+							|#{translate("compile_larger_projects")}
+					p.text-center(ng-controller="FreeTrialModalController")
+						a.btn.btn-success(
+							href
+							ng-class="buttonClass"
+							ng-click="startFreeTrial('history')"
+							sixpack-convert="teaser-history"
+						) #{translate("start_free_trial")}
 
 
-				p.text-center(ng-controller="FreeTrialModalController")
-					a.btn.btn-success(
-						href
-						ng-class="buttonClass"
-						ng-click="startFreeTrial('history')"
-					) #{translate("start_free_trial")}
+				.message.message-wider(
+					sixpack-when="focused"
+					ng-show="project.owner._id == user.id"
+				)
+					header.message-header
+						h3 History
 
 
+					.message-body
+						h4.teaser-title See who changed what. Go back to previous versions.
+						img.teaser-img(
+							src="/img/teasers/history/teaser-history.png"
+							alt="History"
+						)
+						p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
+						.row
+							.col-md-8.col-md-offset-2
+								ul.list-unstyled
+									li 
+										i.fa.fa-check &nbsp;
+										| Catch up with your collaborators changes
+									
+									li
+										i.fa.fa-check &nbsp;
+										| See changes over any time period
+									
+									li 
+										i.fa.fa-check &nbsp;
+										| Revert your documents to previous versions
+									
+									li 
+										i.fa.fa-check &nbsp;
+										| Restore deleted files
+						p.text-center(ng-controller="FreeTrialModalController")
+							a.btn.btn-success(
+								href
+								ng-class="buttonClass"
+								ng-click="startFreeTrial('history')"
+								sixpack-convert="teaser-history"
+							) Try it for free
 
 
 			.message(ng-show="project.owner._id != user.id")
 			.message(ng-show="project.owner._id != user.id")
 				p #{translate("ask_proj_owner_to_upgrade_for_history")}
 				p #{translate("ask_proj_owner_to_upgrade_for_history")}

+ 6 - 5
services/web/app/views/subscriptions/dashboard.jade

@@ -12,8 +12,8 @@ block scripts
 
 
 mixin printPlan(plan)
 mixin printPlan(plan)
 	-if (!plan.hideFromUsers)
 	-if (!plan.hideFromUsers)
-		tr(ng-controller="ChangePlanFormController")
-			td(ng-init="plan=#{JSON.stringify(plan)}")
+		tr(ng-controller="ChangePlanFormController", ng-init="plan=#{JSON.stringify(plan)}", ng-show="shouldShowPlan(plan.planCode)")
+			td
 				strong #{plan.name}
 				strong #{plan.name}
 			td {{refreshPrice(plan.planCode)}}
 			td {{refreshPrice(plan.planCode)}}
 				-if (plan.annual)
 				-if (plan.annual)
@@ -46,8 +46,8 @@ block content
 							| &nbsp;
 							| &nbsp;
 							| #{translate("your_billing_details_were_saved")}
 							| #{translate("your_billing_details_were_saved")}
 					.card(ng-if="view == 'overview'")
 					.card(ng-if="view == 'overview'")
-						.page-header
-							h1 #{translate("your_subscription")}
+						.page-header(x-current-plan="#{subscription.planCode}")
+							h1 #{translate("your_subscription")} 
 
 
 						- if (subscription && user._id+'' == subscription.admin_id+'')
 						- if (subscription && user._id+'' == subscription.admin_id+'')
 							case subscription.state
 							case subscription.state
@@ -56,7 +56,8 @@ block content
 									
 									
 								when "active"
 								when "active"
 									p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})} 
 									p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})} 
-										a(href, ng-click="changePlan = true") !{translate("change_plan")}.
+										span(ng-show="!isNextGenPlan")
+											a(href, ng-click="changePlan = true") !{translate("change_plan")}.
 									p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + subscription.price + "</strong>", collectionDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
 									p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + subscription.price + "</strong>", collectionDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
 									p.pull-right
 									p.pull-right
 									p
 									p

+ 137 - 125
services/web/app/views/subscriptions/plans.jade

@@ -3,6 +3,7 @@ block scripts
 	script(type='text/javascript').
 	script(type='text/javascript').
 		window.recomendedCurrency = '#{recomendedCurrency}'
 		window.recomendedCurrency = '#{recomendedCurrency}'
 		window.abCurrencyFlag = '#{abCurrencyFlag}'
 		window.abCurrencyFlag = '#{abCurrencyFlag}'
+		window.shouldABTestPlans = #{shouldABTestPlans || false}
 
 
 	script(type='text/javascript').
 	script(type='text/javascript').
 		(function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; 
 		(function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; 
@@ -56,133 +57,144 @@ block content
 										ng-click="changeCurreny(currency)"
 										ng-click="changeCurreny(currency)"
 									) {{currency}} ({{value['symbol']}})
 									) {{currency}} ({{value['symbol']}})
 
 
-				.row(ng-cloak)
-					.col-md-10.col-md-offset-1
-						.row
-							.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
-								.col-md-4
-									.card.card-first
-										.card-header
-											h2 #{translate("personal")} 
-										.circle #{translate("free")} 
-										ul.list-unstyled
-											li #{translate("one_collaborator")}
-											li &nbsp;
-											li &nbsp;
-											li &nbsp;
-											li
-												br
-												a.btn.btn-info(
-													href="/register"
-													style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
-												) #{translate("sign_up_now")}
-								.col-md-4
-									.card.card-highlighted
-										.card-header
-											h2 #{translate("collaborator")}
-										.circle
-											span(ng-if="ui.view == 'monthly'")
-												| {{plans[currencyCode]['collaborator']['monthly']}}
-												span.small /mo
-											span(ng-if="ui.view == 'annual'")
-												| {{plans[currencyCode]['collaborator']['annual']}}
-												span.small /yr
-										ul.list-unstyled
-											li 
-												strong #{translate("collabs_per_proj", {collabcount:10})}
-											li #{translate("full_doc_history")}
-											li #{translate("sync_to_dropbox")}
-											li #{translate("sync_to_github")}
-											li
-												br
-												a.btn.btn-info(
+				div(ng-show="showPlans")
+					.row(ng-cloak)
+						.col-md-10.col-md-offset-1
+							.row
+								.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
+									.col-md-4
+										.card.card-first
+											.card-header
+												h2 #{translate("personal")} 
+											.circle #{translate("free")} 
+											ul.list-unstyled
+												li #{translate("one_collaborator")}
+												li &nbsp;
+												li &nbsp;
+												li &nbsp;
+												li
+													br
+													a.btn.btn-info(
+														href="/register"
+														style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
+													) #{translate("sign_up_now")}
+									.col-md-4
+										.card.card-highlighted
+											.card-header
+												h2 #{translate("collaborator")}
+											.circle
+												span(ng-if="ui.view == 'monthly'")
+													| {{plans[currencyCode]['collaborator']['monthly']}}
+													span.small /mo
+												span(ng-if="ui.view == 'annual'")
+													| {{plans[currencyCode]['collaborator']['annual']}}
+													span.small /yr
+											ul.list-unstyled
+												li 
+													strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:10})}
+													strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:8})}
+													strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:12})}
+												li #{translate("full_doc_history")}
+												li #{translate("sync_to_dropbox")}
+												li #{translate("sync_to_github")}
+												li
+													br
+													a.btn.btn-info(
+
+														ng-href="#{baseUrl}/user/subscription/new?planCode=collaborator{{ (ui.view == 'annual' ? '-annual' : '') + (plansVariant == 'default' ? planQueryString : '_'+plansVariant)}}&currency={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
+													)
+														span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
+														span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
+									.col-md-4
+										.card.card-last
+											.card-header
+												h2 #{translate("professional")}
+											.circle 
+												span(ng-if="ui.view == 'monthly'")
+													| {{plans[currencyCode]['professional']['monthly']}}
+													span.small /mo
+												span(ng-if="ui.view == 'annual'")
+													| {{plans[currencyCode]['professional']['annual']}}
+													span.small /yr
+											ul.list-unstyled
+												li
+													strong #{translate("unlimited_collabs")}
+												li #{translate("full_doc_history")}
+												li #{translate("sync_to_dropbox")}
+												li #{translate("sync_to_github")}
+												li
+													br
+													a.btn.btn-info(
+														ng-href="#{baseUrl}/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('professional')"
+													)
+														span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
+														span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
+
+								.card-group.text-centered(ng-if="ui.view == 'student'")
+									.col-md-4
+										.card.card-first
+											.card-header
+												h2 #{translate("personal")}
+											.circle #{translate("free")}
+											ul.list-unstyled
+												li #{translate("one_collaborator")}
+												li &nbsp;
+												li &nbsp;
+												li &nbsp;
+												li
+													br
+													a.btn.btn-info(
+														href="/register"
+														style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
+													) #{translate("sign_up_now")}
+
+									.col-md-4
+										.card.card-highlighted
+											.card-header
+												h2 #{translate("student")}
+											.circle
+												span
+													| {{plans[currencyCode]['student']['monthly']}}
+													span.small /mo
+											ul.list-unstyled
+												li
+													strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})}
+													strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})}
+													strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})}
+												li #{translate("full_doc_history")}
+												li #{translate("sync_to_dropbox")}
+												li #{translate("sync_to_github")}
+												li
+													br
+													a.btn.btn-info(
+														ng-href="#{baseUrl}/user/subscription/new?planCode=student{{ plansVariant == 'default' ? planQueryString : '_'+plansVariant }}&currency={{currencyCode}}",
+														ng-click="signUpNowClicked('student')"
+													) #{translate("start_free_trial")}
+
+									.col-md-4
+										.card.card-last
+											.card-header
+												h2 #{translate("student")} (#{translate("annual")})
+											.circle
+												span
+													| {{plans[currencyCode]['student']['annual']}}
+													span.small /yr
+											ul.list-unstyled
+												li
+													strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})}
+													strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})}
+													strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})}
+												li #{translate("full_doc_history")}
+												li #{translate("sync_to_dropbox")}
+												li #{translate("sync_to_github")}
+												li
+													br
+													a.btn.btn-info(
+														ng-href="#{baseUrl}/user/subscription/new?planCode=student-annual{{ plansVariant == 'default' ? '' : '_'+plansVariant }}&currency={{currencyCode}}",
+														ng-click="signUpNowClicked('student')"
+													) #{translate("buy_now")}
 
 
-													ng-href="#{baseUrl}/user/subscription/new?planCode=collaborator{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
-												)
-													span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
-													span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
-								.col-md-4
-									.card.card-last
-										.card-header
-											h2 #{translate("professional")}
-										.circle 
-											span(ng-if="ui.view == 'monthly'")
-												| {{plans[currencyCode]['professional']['monthly']}}
-												span.small /mo
-											span(ng-if="ui.view == 'annual'")
-												| {{plans[currencyCode]['professional']['annual']}}
-												span.small /yr
-										ul.list-unstyled
-											li
-												strong #{translate("unlimited_collabs")}
-											li #{translate("full_doc_history")}
-											li #{translate("sync_to_dropbox")}
-											li #{translate("sync_to_github")}
-											li
-												br
-												a.btn.btn-info(
-													ng-href="#{baseUrl}/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('professional')"
-												)
-													span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
-													span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
 
 
-							.card-group.text-centered(ng-if="ui.view == 'student'")
-								.col-md-4
-									.card.card-first
-										.card-header
-											h2 #{translate("personal")}
-										.circle #{translate("free")}
-										ul.list-unstyled
-											li #{translate("one_collaborator")}
-											li &nbsp;
-											li &nbsp;
-											li &nbsp;
-											li
-												br
-												a.btn.btn-info(
-													href="/register"
-													style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
-												) #{translate("sign_up_now")}
-								
-								.col-md-4
-									.card.card-highlighted
-										.card-header
-											h2 #{translate("student")}
-										.circle
-											span
-												| {{plans[currencyCode]['student']['monthly']}}
-												span.small /mo
-										ul.list-unstyled
-											li
-												strong #{translate("collabs_per_proj", {collabcount:6})}
-											li #{translate("full_doc_history")}
-											li #{translate("sync_to_dropbox")}
-											li #{translate("sync_to_github")}
-											li
-												br
-												a.btn.btn-info(
-													ng-href="#{baseUrl}/user/subscription/new?planCode=student{{planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('student')"
-												) #{translate("start_free_trial")}
-								
-								.col-md-4
-									.card.card-last
-										.card-header
-											h2 #{translate("student")} (#{translate("annual")})
-										.circle
-											span
-												| {{plans[currencyCode]['student']['annual']}}
-												span.small /yr
-										ul.list-unstyled
-											li
-												strong #{translate("collabs_per_proj", {collabcount:6})}
-											li #{translate("full_doc_history")}
-											li #{translate("sync_to_dropbox")}
-											li #{translate("sync_to_github")}
-											li
-												br
-												a.btn.btn-info(
-													ng-href="#{baseUrl}/user/subscription/new?planCode=student-annual&currency={{currencyCode}}", ng-click="signUpNowClicked('student')"
-												) #{translate("buy_now")}
 
 
 				.row.row-spaced(ng-cloak)
 				.row.row-spaced(ng-cloak)
 					p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}
 					p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}

+ 1 - 1
services/web/app/views/subscriptions/successful_subscription.jade

@@ -2,7 +2,7 @@ extends ../layout
 
 
 block content
 block content
 	.content.content-alt
 	.content.content-alt
-		.container
+		.container(ng-controller="SuccessfulSubscriptionController")
 			.row
 			.row
 				.col-md-8.col-md-offset-2
 				.col-md-8.col-md-offset-2
 					.card(ng-cloak)
 					.card(ng-cloak)

+ 18 - 2
services/web/app/views/user/settings.jade

@@ -150,16 +150,32 @@ block content
 	script(type='text/ng-template', id='deleteAccountModalTemplate')
 	script(type='text/ng-template', id='deleteAccountModalTemplate')
 		.modal-header
 		.modal-header
 			h3 #{translate("delete_account")}
 			h3 #{translate("delete_account")}
-		.modal-body
-			p !{translate("delete_account_warning_message_2")}
+		div.modal-body#delete-account-modal
+			p !{translate("delete_account_warning_message_3")}
 			form(novalidate, name="deleteAccountForm")
 			form(novalidate, name="deleteAccountForm")
+				label #{translate('email')}
 				input.form-control(
 				input.form-control(
 					type="text",
 					type="text",
+					autocomplete="off",
 					placeholder="",
 					placeholder="",
 					ng-model="state.deleteText",
 					ng-model="state.deleteText",
 					focus-on="open",
 					focus-on="open",
 					ng-keyup="checkValidation()"
 					ng-keyup="checkValidation()"
 				)
 				)
+				label #{translate('password')}
+				input.form-control(
+					type="password",
+					autocomplete="off",
+					placeholder="",
+					ng-model="state.password",
+					ng-keyup="checkValidation()"
+				)
+			div(ng-if="state.error")
+				div.alert.alert-danger
+					| #{translate('generic_something_went_wrong')}
+			div(ng-if="state.invalidCredentials")
+				div.alert.alert-danger
+					| #{translate('email_or_password_wrong_try_again')}
 		.modal-footer
 		.modal-footer
 			button.btn.btn-default(
 			button.btn.btn-default(
 				ng-click="cancel()"
 				ng-click="cancel()"

+ 9 - 4
services/web/public/coffee/ide.coffee

@@ -67,16 +67,21 @@ define [
 			pdfLayout: 'sideBySide'
 			pdfLayout: 'sideBySide'
 		}
 		}
 		$scope.user = window.user
 		$scope.user = window.user
+
+
+		$scope.shouldABTestPlans = false
+		if $scope.user.signUpDate >= '2016-10-27'
+			$scope.shouldABTestPlans = true
+
 		$scope.settings = window.userSettings
 		$scope.settings = window.userSettings
 		$scope.anonymous = window.anonymous
 		$scope.anonymous = window.anonymous
 
 
 		$scope.chat = {}
 		$scope.chat = {}
 
 
-
 		# Only run the header AB test for newly registered users.
 		# Only run the header AB test for newly registered users.
 		_abTestStartDate = new Date(Date.UTC(2016, 8, 28))
 		_abTestStartDate = new Date(Date.UTC(2016, 8, 28))
 		_userSignUpDate = new Date(window.user.signUpDate)
 		_userSignUpDate = new Date(window.user.signUpDate)
-		
+
 		$scope.shouldABTestHeaderLabels = _userSignUpDate > _abTestStartDate
 		$scope.shouldABTestHeaderLabels = _userSignUpDate > _abTestStartDate
 		$scope.headerLabelsABVariant = ""
 		$scope.headerLabelsABVariant = ""
 
 
@@ -93,7 +98,7 @@ define [
 		# Tracking code.
 		# Tracking code.
 		$scope.$watch "ui.view", (newView, oldView) ->
 		$scope.$watch "ui.view", (newView, oldView) ->
 			if newView? and newView != "editor" and newView != "pdf"
 			if newView? and newView != "editor" and newView != "pdf"
-				event_tracking.sendMBOnce "ide-open-view-#{ newView }-once" 
+				event_tracking.sendMBOnce "ide-open-view-#{ newView }-once"
 
 
 		$scope.$watch "ui.chatOpen", (isOpen) ->
 		$scope.$watch "ui.chatOpen", (isOpen) ->
 			event_tracking.sendMBOnce "ide-open-chat-once" if isOpen
 			event_tracking.sendMBOnce "ide-open-chat-once" if isOpen
@@ -106,7 +111,7 @@ define [
 		# End of tracking code.
 		# End of tracking code.
 
 
 		window._ide = ide
 		window._ide = ide
-		
+
 		ide.validFileRegex = '^[^\*\/]*$' # Don't allow * and /
 		ide.validFileRegex = '^[^\*\/]*$' # Don't allow * and /
 
 
 		ide.project_id = $scope.project_id = window.project_id
 		ide.project_id = $scope.project_id = window.project_id

+ 29 - 2
services/web/public/coffee/ide/connection/ConnectionManager.coffee

@@ -27,6 +27,7 @@ define [], () ->
 
 
 			@connected = false
 			@connected = false
 			@userIsInactive = false
 			@userIsInactive = false
+			@gracefullyReconnecting = false
 			
 			
 			@$scope.connection = 
 			@$scope.connection = 
 				reconnecting: false
 				reconnecting: false
@@ -54,6 +55,7 @@ define [], () ->
 			@ide.socket.on "connect", () =>
 			@ide.socket.on "connect", () =>
 				sl_console.log "[socket.io connect] Connected"
 				sl_console.log "[socket.io connect] Connected"
 				@connected = true
 				@connected = true
+				@gracefullyReconnecting = false
 				@ide.pushEvent("connected")
 				@ide.pushEvent("connected")
 
 
 				@$scope.$apply () =>
 				@$scope.$apply () =>
@@ -81,7 +83,7 @@ define [], () ->
 				@$scope.$apply () =>
 				@$scope.$apply () =>
 					@$scope.connection.reconnecting = false
 					@$scope.connection.reconnecting = false
 
 
-				if !$scope.connection.forced_disconnect and !@userIsInactive
+				if !$scope.connection.forced_disconnect and !@userIsInactive and !@gracefullyReconnecting
 					@startAutoReconnectCountdown()
 					@startAutoReconnectCountdown()
 
 
 			@ide.socket.on 'forceDisconnect', (message) =>
 			@ide.socket.on 'forceDisconnect', (message) =>
@@ -97,7 +99,11 @@ define [], () ->
 				setTimeout () ->
 				setTimeout () ->
 					location.reload()
 					location.reload()
 				, 10 * 1000
 				, 10 * 1000
-				
+			
+			@ide.socket.on "reconnectGracefully", () =>
+				sl_console.log "Reconnect gracefully"
+				@reconnectGracefully()
+
 		joinProject: () ->
 		joinProject: () ->
 			sl_console.log "[joinProject] joining..."
 			sl_console.log "[joinProject] joining..."
 			@ide.socket.emit 'joinProject', {
 			@ide.socket.emit 'joinProject', {
@@ -180,3 +186,24 @@ define [], () ->
 				@$scope.$apply () =>
 				@$scope.$apply () =>
 					@$scope.connection.inactive_disconnect = true
 					@$scope.connection.inactive_disconnect = true
 
 
+		RECONNECT_GRACEFULLY_RETRY_INTERVAL: 5000 # ms
+		MAX_RECONNECT_GRACEFULLY_INTERVAL: 60 * 5 * 1000 # 5 minutes
+		reconnectGracefully: () ->
+			@reconnectGracefullyStarted ?= new Date()
+			userIsInactive = (new Date() - @lastUserAction) > @RECONNECT_GRACEFULLY_RETRY_INTERVAL
+			maxIntervalReached = (new Date() - @reconnectGracefullyStarted) > @MAX_RECONNECT_GRACEFULLY_INTERVAL
+			if userIsInactive or maxIntervalReached
+				sl_console.log "[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
+				@_reconnectGracefullyNow()
+			else
+				sl_console.log "[reconnectGracefully] User is working, will try again in 5 seconds"
+				setTimeout () =>
+					@reconnectGracefully()
+				, @RECONNECT_GRACEFULLY_RETRY_INTERVAL
+		
+		_reconnectGracefullyNow: () ->
+			@gracefullyReconnecting = true
+			@reconnectGracefullyStarted = null
+			# Clear cookie so we don't go to the same backend server
+			$.cookie("SERVERID", "", { expires: -1, path: "/" })
+			@reconnectImmediately()

+ 33 - 7
services/web/public/coffee/ide/editor/directives/aceEditor.coffee

@@ -15,9 +15,13 @@ define [
 
 
 	# set the path for ace workers if using a CDN (from editor.jade)
 	# set the path for ace workers if using a CDN (from editor.jade)
 	if window.aceWorkerPath != ""
 	if window.aceWorkerPath != ""
+		syntaxValidationEnabled = true
 		ace.config.set('workerPath', "#{window.aceWorkerPath}")
 		ace.config.set('workerPath', "#{window.aceWorkerPath}")
 	else
 	else
-		ace.config.setDefaultValue("session", "useWorker", false)
+		syntaxValidationEnabled = false
+
+	# By default, don't use workers - enable them per-session as required
+	ace.config.setDefaultValue("session", "useWorker", false)
 
 
 	# Ace loads its script itself, so we need to hook in to be able to clear
 	# Ace loads its script itself, so we need to hook in to be able to clear
 	# the cache.
 	# the cache.
@@ -202,8 +206,12 @@ define [
 					editor.setReadOnly !!value
 					editor.setReadOnly !!value
 
 
 				scope.$watch "syntaxValidation", (value) ->
 				scope.$watch "syntaxValidation", (value) ->
-					session = editor.getSession()
-					session.setOption("useWorker", value);
+					# ignore undefined settings here
+					# only instances of ace with an explicit value should set useWorker
+					# the history instance will have syntaxValidation undefined
+					if value? and syntaxValidationEnabled
+						session = editor.getSession()
+						session.setOption("useWorker", value);
 
 
 				editor.setOption("scrollPastEnd", true)
 				editor.setOption("scrollPastEnd", true)
 
 
@@ -223,14 +231,32 @@ define [
 					# see if we can lookup a suitable mode from ace
 					# see if we can lookup a suitable mode from ace
 					# but fall back to text by default
 					# but fall back to text by default
 					try
 					try
-						mode = ModeList.getModeForPath(scope.fileName).mode
+						if scope.fileName.match(/\.(Rtex|bbl)$/i)
+							# recognise Rtex and bbl as latex
+							mode = "ace/mode/latex"
+						else if scope.fileName.match(/\.(sty|cls|clo)$/)
+							# recognise some common files as tex
+							mode = "ace/mode/tex"
+						else
+							mode = ModeList.getModeForPath(scope.fileName).mode
+							# we prefer plain_text mode over text mode because ace's
+							# text mode is actually for code and has unwanted
+							# indenting (see wrapMethod in ace edit_session.js)
+							if mode is "ace/mode/text"
+								mode = "ace/mode/plain_text"
 					catch
 					catch
-						mode = "ace/mode/text"
+						mode = "ace/mode/plain_text"
 
 
-					editor.setSession(new EditSession(lines, mode))
+					# create our new session
+					session = new EditSession(lines, mode)
 
 
-					session = editor.getSession()
 					session.setUseWrapMode(true)
 					session.setUseWrapMode(true)
+					# use syntax validation only when explicitly set
+					if scope.syntaxValidation? and syntaxValidationEnabled
+						session.setOption("useWorker", scope.syntaxValidation);
+
+					# now attach session to editor
+					editor.setSession(session)
 
 
 					doc = session.getDocument()
 					doc = session.getDocument()
 					doc.on "change", onChange
 					doc.on "change", onChange

+ 9 - 1
services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee

@@ -233,8 +233,16 @@ define [
 			start = aceDelta.start
 			start = aceDelta.start
 			if !start?
 			if !start?
 				error = new Error("aceDelta had no start event.")
 				error = new Error("aceDelta had no start event.")
+				JSONstringifyWithCycles = (o) ->
+					seen = []
+					return JSON.stringify o, (k,v) ->
+						if (typeof v == 'object')
+							if ( !seen.indexOf(v) )
+								return '__cycle__'
+							seen.push(v);
+						return v
 				Raven?.captureException(error, {
 				Raven?.captureException(error, {
-					aceDelta: JSON.stringify(aceDelta)
+					aceDelta: JSONstringifyWithCycles(aceDelta)
 				})
 				})
 				throw error
 				throw error
 			linesBefore = docLines.slice(0, start.row)
 			linesBefore = docLines.slice(0, start.row)

+ 4 - 4
services/web/public/coffee/ide/pdf/controllers/PdfController.coffee

@@ -29,12 +29,12 @@ define [
 
 
 		$scope.$watch "shouldShowLogs", (shouldShow) ->
 		$scope.$watch "shouldShowLogs", (shouldShow) ->
 			if shouldShow
 			if shouldShow
-				$scope.$applyAsync () -> 
+				$scope.$applyAsync () ->
 					$scope.shouldDropUp = getFilesDropdownTopCoordAsRatio() > 0.65
 					$scope.shouldDropUp = getFilesDropdownTopCoordAsRatio() > 0.65
 
 
 		# log hints tracking
 		# log hints tracking
 		$scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts
 		$scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts
-		
+
 		$scope.trackLogHintsLearnMore = () ->
 		$scope.trackLogHintsLearnMore = () ->
 			event_tracking.sendMB "logs-hints-learn-more"
 			event_tracking.sendMB "logs-hints-learn-more"
 
 
@@ -108,7 +108,7 @@ define [
 				_csrf: window.csrfToken
 				_csrf: window.csrfToken
 			}, {params: params}
 			}, {params: params}
 
 
-		parseCompileResponse = (response) ->		
+		parseCompileResponse = (response) ->
 
 
 			# keep last url
 			# keep last url
 			last_pdf_url = $scope.pdf.url
 			last_pdf_url = $scope.pdf.url
@@ -469,7 +469,7 @@ define [
 
 
 			event_tracking.sendMB "subscription-start-trial", { source }
 			event_tracking.sendMB "subscription-start-trial", { source }
 
 
-			window.open("/user/subscription/new?planCode=student_free_trial_7_days")
+			window.open("/user/subscription/new?planCode=#{$scope.startTrialPlanCode}")
 			$scope.startedFreeTrial = true
 			$scope.startedFreeTrial = true
 
 
 	App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
 	App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->

+ 6 - 1
services/web/public/coffee/ide/references/ReferencesManager.coffee

@@ -17,8 +17,13 @@ define [
 			# When we join the project:
 			# When we join the project:
 			#   index all references files
 			#   index all references files
 			#   and don't broadcast to all clients
 			#   and don't broadcast to all clients
+			@inited = false
 			@$scope.$on 'project:joined', (e) =>
 			@$scope.$on 'project:joined', (e) =>
-				@indexAllReferences(false)
+				# We only need to grab the references when the editor first loads,
+				# not on every reconnect
+				if !@inited
+					@inited = true
+					@indexAllReferences(false)
 
 
 			setTimeout(
 			setTimeout(
 				(self) ->
 				(self) ->

+ 21 - 5
services/web/public/coffee/main/account-settings.coffee

@@ -29,10 +29,13 @@ define [
 	App.controller "DeleteAccountModalController", [
 	App.controller "DeleteAccountModalController", [
 		"$scope", "$modalInstance", "$timeout", "$http",
 		"$scope", "$modalInstance", "$timeout", "$http",
 		($scope,   $modalInstance,   $timeout,   $http) ->
 		($scope,   $modalInstance,   $timeout,   $http) ->
-			$scope.state = 
+			$scope.state =
 				isValid : false
 				isValid : false
 				deleteText: ""
 				deleteText: ""
+				password: ""
 				inflight: false
 				inflight: false
+				error: false
+				invalidCredentials: false
 
 
 			$modalInstance.opened.then () ->
 			$modalInstance.opened.then () ->
 				$timeout () ->
 				$timeout () ->
@@ -40,20 +43,33 @@ define [
 				, 700
 				, 700
 
 
 			$scope.checkValidation = ->
 			$scope.checkValidation = ->
-				$scope.state.isValid = $scope.state.deleteText == $scope.email
+				$scope.state.isValid = $scope.state.deleteText == $scope.email and $scope.state.password.length > 0
 
 
 			$scope.delete = () ->
 			$scope.delete = () ->
 				$scope.state.inflight = true
 				$scope.state.inflight = true
-
+				$scope.state.error = false
+				$scope.state.invalidCredentials = false
 				$http({
 				$http({
-						method: "DELETE"
-						url: "/user"
+						method: "POST"
+						url: "/user/delete"
 						headers:
 						headers:
 							"X-CSRF-Token": window.csrfToken
 							"X-CSRF-Token": window.csrfToken
+							"Content-Type": 'application/json'
+						data:
+							password: $scope.state.password
 					})
 					})
 					.success () ->
 					.success () ->
 						$modalInstance.close()
 						$modalInstance.close()
+						$scope.state.inflight = false
+						$scope.state.error = false
+						$scope.state.invalidCredentials = false
 						window.location = "/"
 						window.location = "/"
+					.error (data, status) ->
+						$scope.state.inflight = false
+						if status == 403
+							$scope.state.invalidCredentials = true
+						else
+							$scope.state.error = true
 
 
 			$scope.cancel = () ->
 			$scope.cancel = () ->
 				$modalInstance.dismiss('cancel')
 				$modalInstance.dismiss('cancel')

+ 29 - 9
services/web/public/coffee/main/account-upgrade.coffee

@@ -6,14 +6,34 @@ define [
 		$scope.buttonClass = "btn-primary"
 		$scope.buttonClass = "btn-primary"
 
 
 		$scope.startFreeTrial = (source, couponCode) ->
 		$scope.startFreeTrial = (source, couponCode) ->
-			event_tracking.sendMB "subscription-start-trial", { source }
+			plan = 'collaborator_free_trial_7_days'
 
 
 			w = window.open()
 			w = window.open()
-			sixpack.convert "track-changes-discount", ->
-				sixpack.participate 'in-editor-free-trial-plan', ['student', 'collaborator'], (planName, rawResponse)->
-					ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
-					url = "/user/subscription/new?planCode=#{planName}_free_trial_7_days&ssp=#{planName == 'collaborator'}"
-					if couponCode?
-						url = "#{url}&cc=#{couponCode}"
-					$scope.startedFreeTrial = true
-					w.location = url
+			go = () ->
+				ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
+				url = "/user/subscription/new?planCode=#{plan}&ssp=true"
+				if couponCode?
+					url = "#{url}&cc=#{couponCode}"
+				$scope.startedFreeTrial = true
+
+				switch source
+					when "dropbox"
+						sixpack.participate 'teaser-dropbox-text', ['default', 'dropbox-focused'], (variant) ->
+							event_tracking.sendMB "subscription-start-trial", { source, plan, variant }
+
+					when "history"
+						sixpack.participate 'teaser-history', ['default', 'focused'], (variant) ->
+							event_tracking.sendMB "subscription-start-trial", { source, plan, variant }
+
+					else
+						event_tracking.sendMB "subscription-start-trial", { source, plan }
+				
+				w.location = url
+
+			if $scope.shouldABTestPlans
+				sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)->
+					if chosenVariation in ['heron', 'ibis']
+						plan = "collaborator_#{chosenVariation}"
+					go()
+			else
+				go()

+ 5 - 2
services/web/public/coffee/main/new-subscription.coffee

@@ -5,12 +5,15 @@ define [
 
 
 	App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
 	App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
 		throw new Error("Recurly API Library Missing.")  if typeof recurly is "undefined"
 		throw new Error("Recurly API Library Missing.")  if typeof recurly is "undefined"
-	
+
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 		$scope.plans = MultiCurrencyPricing.plans
 		$scope.plans = MultiCurrencyPricing.plans
 
 
 		$scope.switchToStudent = ()->
 		$scope.switchToStudent = ()->
-			window.location = "/user/subscription/new?planCode=student_free_trial_7_days&currency=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
+			currentPlanCode = window.plan_code
+			planCode = currentPlanCode.replace('collaborator', 'student')
+			event_tracking.sendMB 'subscription-form-switch-to-student', { plan: window.plan_code }
+			window.location = "/user/subscription/new?planCode=#{planCode}&currency=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
 
 
 		event_tracking.sendMB "subscription-form", { plan : window.plan_code }
 		event_tracking.sendMB "subscription-form", { plan : window.plan_code }
 
 

+ 189 - 12
services/web/public/coffee/main/plans.coffee

@@ -5,12 +5,171 @@ define [
 
 
 
 
 	App.factory "MultiCurrencyPricing", () ->
 	App.factory "MultiCurrencyPricing", () ->
-		
+
 		currencyCode = window.recomendedCurrency
 		currencyCode = window.recomendedCurrency
 
 
 		return {
 		return {
 			currencyCode:currencyCode
 			currencyCode:currencyCode
-			plans: 
+
+			heron:
+				USD:
+					student:
+						monthly: "$6"
+						annual: "$60"
+					collaborator:
+						monthly: "$12"
+						annual: "$144"
+				EUR:
+					student:
+						monthly: "€5"
+						annual: "€50"
+					collaborator:
+						monthly: "€11"
+						annual: "€132"
+				GBP:
+					student:
+						monthly: "£5"
+						annual: "£50"
+					collaborator:
+						monthly: "£10"
+						annual: "£120"
+				SEK:
+					student:
+						monthly: "45 kr"
+						annual: "450 kr"
+					collaborator:
+						monthly: "90 kr"
+						annual: "1080 kr"
+				CAD:
+					student:
+						monthly: "$7"
+						annual: "$70"
+					collaborator:
+						monthly: "$14"
+						annual: "$168"
+				NOK:
+					student:
+						monthly: "45 kr"
+						annual: "450 kr"
+					collaborator:
+						monthly: "90 kr"
+						annual: "1080 kr"
+				DKK:
+					student:
+						monthly: "40 kr"
+						annual: "400 kr"
+					collaborator:
+						monthly: "70 kr"
+						annual: "840 kr"
+				AUD:
+					student:
+						monthly: "$8"
+						annual: "$80"
+					collaborator:
+						monthly: "$15"
+						annual: "$180"
+				NZD:
+					student:
+						monthly: "$8"
+						annual: "$80"
+					collaborator:
+						monthly: "$15"
+						annual: "$180"
+				CHF:
+					student:
+						monthly: "Fr 6"
+						annual: "Fr 60"
+					collaborator:
+						monthly: "Fr 12"
+						annual: "Fr 144"
+				SGD:
+					student:
+						monthly: "$8"
+						annual: "$80"
+					collaborator:
+						monthly: "$16"
+						annual: "$192"
+
+			ibis:
+				USD:
+					student:
+						monthly: "$10"
+						annual: "$100"
+					collaborator:
+						monthly: "$18"
+						annual: "$216"
+				EUR:
+					student:
+						monthly: "€9"
+						annual: "€90"
+					collaborator:
+						monthly: "€17"
+						annual: "€204"
+				GBP:
+					student:
+						monthly: "£7"
+						annual: "£70"
+					collaborator:
+						monthly: "£14"
+						annual: "£168"
+				SEK:
+					student:
+						monthly: "75 kr"
+						annual: "750 kr"
+					collaborator:
+						monthly: "140 kr"
+						annual: "1680 kr"
+				CAD:
+					student:
+						monthly: "$12"
+						annual: "$120"
+					collaborator:
+						monthly: "$22"
+						annual: "$264"
+				NOK:
+					student:
+						monthly: "75 kr"
+						annual: "750 kr"
+					collaborator:
+						monthly: "140 kr"
+						annual: "1680 kr"
+				DKK:
+					student:
+						monthly: "68 kr"
+						annual: "680 kr"
+					collaborator:
+						monthly: "110 kr"
+						annual: "1320 kr"
+				AUD:
+					student:
+						monthly: "$13"
+						annual: "$130"
+					collaborator:
+						monthly: "$22"
+						annual: "$264"
+				NZD:
+					student:
+						monthly: "$14"
+						annual: "$140"
+					collaborator:
+						monthly: "$22"
+						annual: "$264"
+				CHF:
+					student:
+						monthly: "Fr 10"
+						annual: "Fr 100"
+					collaborator:
+						monthly: "Fr 18"
+						annual: "Fr 216"
+				SGD:
+					student:
+						monthly: "$14"
+						annual: "$140"
+					collaborator:
+						monthly: "$25"
+						annual: "$300"
+
+			plans:
 				USD:
 				USD:
 					symbol: "$"
 					symbol: "$"
 					student:
 					student:
@@ -23,7 +182,7 @@ define [
 						monthly: "$30"
 						monthly: "$30"
 						annual: "$360"
 						annual: "$360"
 
 
-				EUR: 
+				EUR:
 					symbol: "€"
 					symbol: "€"
 					student:
 					student:
 						monthly: "€7"
 						monthly: "€7"
@@ -34,7 +193,7 @@ define [
 					professional:
 					professional:
 						monthly: "€28"
 						monthly: "€28"
 						annual: "€336"
 						annual: "€336"
-						
+
 				GBP:
 				GBP:
 					symbol: "£"
 					symbol: "£"
 					student:
 					student:
@@ -117,7 +276,7 @@ define [
 					professional:
 					professional:
 						monthly: "$35"
 						monthly: "$35"
 						annual: "$420"
 						annual: "$420"
-						
+
 				CHF:
 				CHF:
 					symbol: "Fr"
 					symbol: "Fr"
 					student:
 					student:
@@ -141,36 +300,54 @@ define [
 					professional:
 					professional:
 						monthly: "$40"
 						monthly: "$40"
 						annual: "$480"
 						annual: "$480"
+
 		}
 		}
-	
 
 
 
 
+	App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack) ->
+
+		$scope.showPlans = false
 
 
-	App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http) ->
+		$scope.plansVariant = 'default'
+		$scope.shouldABTestPlans = window.shouldABTestPlans
+
+		if $scope.shouldABTestPlans
+			sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)->
+				$scope.plansVariant = chosenVariation
+				event_tracking.sendMB 'plans-page', {plans_variant: chosenVariation}
+				if chosenVariation in ['heron', 'ibis']
+					# overwrite student plans with alternative
+					for currency, _v of $scope.plans
+						$scope.plans[currency]['student'] = MultiCurrencyPricing[chosenVariation][currency]['student']
+						$scope.plans[currency]['collaborator'] = MultiCurrencyPricing[chosenVariation][currency]['collaborator']
+				$scope.showPlans = true
+		else
+			$scope.showPlans = true
 
 
 		$scope.plans = MultiCurrencyPricing.plans
 		$scope.plans = MultiCurrencyPricing.plans
+
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 
 
 		$scope.trial_len = 7
 		$scope.trial_len = 7
+
 		$scope.planQueryString = '_free_trial_7_days'
 		$scope.planQueryString = '_free_trial_7_days'
 
 
 		$scope.ui =
 		$scope.ui =
 			view: "monthly"
 			view: "monthly"
 
 
-
 		$scope.changeCurreny = (newCurrency)->
 		$scope.changeCurreny = (newCurrency)->
 			$scope.currencyCode = newCurrency
 			$scope.currencyCode = newCurrency
 
 
 		$scope.signUpNowClicked = (plan, annual)->
 		$scope.signUpNowClicked = (plan, annual)->
+			event_tracking.sendMB 'plans-page-start-trial', {plan}
 			if $scope.ui.view == "annual"
 			if $scope.ui.view == "annual"
 				plan = "#{plan}_annual"
 				plan = "#{plan}_annual"
-			
-			event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
+			event_tracking.send   'subscription-funnel', 'sign_up_now_button', plan
 
 
 		$scope.switchToMonthly = ->
 		$scope.switchToMonthly = ->
 			$scope.ui.view = "monthly"
 			$scope.ui.view = "monthly"
 			event_tracking.send 'subscription-funnel', 'plans-page', 'monthly-prices'
 			event_tracking.send 'subscription-funnel', 'plans-page', 'monthly-prices'
-		
+
 		$scope.switchToStudent = ->
 		$scope.switchToStudent = ->
 			$scope.ui.view = "student"
 			$scope.ui.view = "student"
 			event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
 			event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
@@ -178,7 +355,7 @@ define [
 		$scope.switchToAnnual = ->
 		$scope.switchToAnnual = ->
 			$scope.ui.view = "annual"
 			$scope.ui.view = "annual"
 			event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
 			event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
-			
+
 		$scope.openGroupPlanModal = () ->
 		$scope.openGroupPlanModal = () ->
 			$modal.open {
 			$modal.open {
 				templateUrl: "groupPlanModalTemplate"
 				templateUrl: "groupPlanModalTemplate"

+ 26 - 11
services/web/public/coffee/main/subscription-dashboard.coffee

@@ -1,15 +1,21 @@
 define [
 define [
 	"base"
 	"base"
 ], (App)->
 ], (App)->
+
+	App.controller 'SuccessfulSubscriptionController', ($scope, sixpack) ->
+		sixpack.convert 'plans-1610', () ->
+
+
 	SUBSCRIPTION_URL = "/user/subscription/update"
 	SUBSCRIPTION_URL = "/user/subscription/update"
 
 
 	setupReturly = _.once ->
 	setupReturly = _.once ->
 		recurly?.configure window.recurlyApiKey
 		recurly?.configure window.recurlyApiKey
 	PRICES = {}
 	PRICES = {}
 
 
+
 	App.controller "CurrenyDropdownController", ($scope, MultiCurrencyPricing, $q)->
 	App.controller "CurrenyDropdownController", ($scope, MultiCurrencyPricing, $q)->
 
 
-		$scope.plans = MultiCurrencyPricing.plans
+		# $scope.plans = MultiCurrencyPricing.plans
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 
 
 		$scope.changeCurrency = (newCurrency)->
 		$scope.changeCurrency = (newCurrency)->
@@ -31,7 +37,7 @@ define [
 			$scope.currencyCode = MultiCurrencyPricing.currencyCode
 			$scope.currencyCode = MultiCurrencyPricing.currencyCode
 
 
 		$scope.pricing = MultiCurrencyPricing
 		$scope.pricing = MultiCurrencyPricing
-		$scope.plans = MultiCurrencyPricing.plans
+		# $scope.plans = MultiCurrencyPricing.plans
 		$scope.currencySymbol = MultiCurrencyPricing.plans[MultiCurrencyPricing.currencyCode].symbol
 		$scope.currencySymbol = MultiCurrencyPricing.plans[MultiCurrencyPricing.currencyCode].symbol
 
 
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
@@ -53,9 +59,9 @@ define [
 			price = ""
 			price = ""
 
 
 	App.controller "ConfirmChangePlanController", ($scope, $modalInstance, $http)->
 	App.controller "ConfirmChangePlanController", ($scope, $modalInstance, $http)->
-		
+
 		$scope.confirmChangePlan = ->
 		$scope.confirmChangePlan = ->
-			body = 
+			body =
 				plan_code: $scope.plan.planCode
 				plan_code: $scope.plan.planCode
 				_csrf : window.csrfToken
 				_csrf : window.csrfToken
 
 
@@ -74,7 +80,7 @@ define [
 		$scope.confirmLeaveGroup = ->
 		$scope.confirmLeaveGroup = ->
 			$scope.inflight = true
 			$scope.inflight = true
 			$http({
 			$http({
-				url: "/subscription/group/user", 
+				url: "/subscription/group/user",
 				method: "DELETE",
 				method: "DELETE",
 				params: {admin_user_id: $scope.admin_id, _csrf: window.csrfToken}
 				params: {admin_user_id: $scope.admin_id, _csrf: window.csrfToken}
 			}).success ->
 			}).success ->
@@ -87,6 +93,8 @@ define [
 
 
 
 
 	App.controller "UserSubscriptionController", ($scope, MultiCurrencyPricing, $http, sixpack, $modal) ->
 	App.controller "UserSubscriptionController", ($scope, MultiCurrencyPricing, $http, sixpack, $modal) ->
+		$scope.plans = MultiCurrencyPricing.plans
+
 		freeTrialEndDate = new Date(subscription?.trial_ends_at)
 		freeTrialEndDate = new Date(subscription?.trial_ends_at)
 
 
 		sevenDaysTime = new Date()
 		sevenDaysTime = new Date()
@@ -96,6 +104,16 @@ define [
 		freeTrialExpiresUnderSevenDays = freeTrialEndDate < sevenDaysTime
 		freeTrialExpiresUnderSevenDays = freeTrialEndDate < sevenDaysTime
 
 
 		$scope.view = 'overview'
 		$scope.view = 'overview'
+		$scope.getSuffix = (planCode) ->
+			planCode?.match(/(.*?)_(.*)/)?[2] || null
+		$scope.subscriptionSuffix = $scope.getSuffix(window?.subscription?.planCode)
+		if $scope.subscriptionSuffix == 'free_trial_7_days'
+			$scope.subscriptionSuffix = ''
+		$scope.isNextGenPlan = $scope.subscriptionSuffix in ['heron', 'ibis']
+
+		$scope.shouldShowPlan = (planCode) ->
+			$scope.getSuffix(planCode) not in ['heron', 'ibis']
+
 		isMonthlyCollab = subscription?.planCode?.indexOf("collaborator") != -1 and subscription?.planCode?.indexOf("ann") == -1
 		isMonthlyCollab = subscription?.planCode?.indexOf("collaborator") != -1 and subscription?.planCode?.indexOf("ann") == -1
 		stillInFreeTrial = freeTrialInFuture and freeTrialExpiresUnderSevenDays
 		stillInFreeTrial = freeTrialInFuture and freeTrialExpiresUnderSevenDays
 
 
@@ -118,7 +136,7 @@ define [
 					$scope.studentPrice = $scope.currencySymbol + (totalPriceExTax + taxAmmount)
 					$scope.studentPrice = $scope.currencySymbol + (totalPriceExTax + taxAmmount)
 
 
 		$scope.downgradeToStudent = ->
 		$scope.downgradeToStudent = ->
-			body = 
+			body =
 				plan_code: 'student'
 				plan_code: 'student'
 				_csrf : window.csrfToken
 				_csrf : window.csrfToken
 			$scope.inflight = true
 			$scope.inflight = true
@@ -129,7 +147,7 @@ define [
 					console.log "something went wrong changing plan"
 					console.log "something went wrong changing plan"
 
 
 		$scope.cancelSubscription = ->
 		$scope.cancelSubscription = ->
-			body = 
+			body =
 				_csrf : window.csrfToken
 				_csrf : window.csrfToken
 
 
 			$scope.inflight = true
 			$scope.inflight = true
@@ -158,7 +176,7 @@ define [
 
 
 
 
 		$scope.exendTrial = ->
 		$scope.exendTrial = ->
-			body = 
+			body =
 				_csrf : window.csrfToken
 				_csrf : window.csrfToken
 			$scope.inflight = true
 			$scope.inflight = true
 			$http.put("/user/subscription/extend", body)
 			$http.put("/user/subscription/extend", body)
@@ -166,6 +184,3 @@ define [
 					location.reload()
 					location.reload()
 				.error ->
 				.error ->
 					console.log "something went wrong changing plan"
 					console.log "something went wrong changing plan"
-
-
-

BIN
services/web/public/img/teasers/history/teaser-history.png


+ 28 - 8
services/web/public/js/ace-1.2.5/mode-latex.js

@@ -214,18 +214,21 @@ var createLatexWorker = function (session) {
     var suppressions = [];
     var suppressions = [];
     var hints = [];
     var hints = [];
     var changeHandler = null;
     var changeHandler = null;
+    var docChangePending = false;
+    var firstPass = true;
 
 
     var worker = new WorkerClient(["ace"], "ace/mode/latex_worker", "LatexWorker");
     var worker = new WorkerClient(["ace"], "ace/mode/latex_worker", "LatexWorker");
     worker.attachToDocument(doc);
     worker.attachToDocument(doc);
-
-    doc.on("change", function () {
+    var docChangeHandler = doc.on("change", function () {
+        docChangePending = true;
         if(changeHandler) {
         if(changeHandler) {
             clearTimeout(changeHandler);
             clearTimeout(changeHandler);
             changeHandler = null;
             changeHandler = null;
         }
         }
     });
     });
 
 
-    selection.on("changeCursor", function () {
+    var cursorHandler = selection.on("changeCursor", function () {
+        if (docChangePending) { return; } ;
         changeHandler = setTimeout(function () {
         changeHandler = setTimeout(function () {
             updateMarkers({cursorMoveOnly:true});
             updateMarkers({cursorMoveOnly:true});
             suppressions = [];
             suppressions = [];
@@ -307,11 +310,20 @@ var createLatexWorker = function (session) {
             }
             }
         }
         }
         if (!cursorMoveOnly || suppressedChanges) {
         if (!cursorMoveOnly || suppressedChanges) {
-            session.setAnnotations(annotations);
+            if (firstPass) {
+                if (annotations.length > 0) {
+                    var originalAnnotations = session.getAnnotations();
+                    session.setAnnotations(originalAnnotations.concat(annotations));
+                };
+                firstPass = false;
+            } else {
+                session.setAnnotations(annotations);
+            }
         };
         };
 
 
     };
     };
     worker.on("lint", function(results) {
     worker.on("lint", function(results) {
+        if(docChangePending) { docChangePending = false; };
         hints = results.data;
         hints = results.data;
         if (hints.length > 100) {
         if (hints.length > 100) {
             hints = hints.slice(0, 100); // limit to 100 errors
             hints = hints.slice(0, 100); // limit to 100 errors
@@ -319,14 +331,22 @@ var createLatexWorker = function (session) {
         updateMarkers();
         updateMarkers();
     });
     });
     worker.on("terminate", function() {
     worker.on("terminate", function() {
+        if(changeHandler) {
+            clearTimeout(changeHandler);
+            changeHandler = null;
+        }
+        doc.off("change", docChangeHandler);
+        selection.off("changeCursor", cursorHandler);
         for (var key in savedRange) {
         for (var key in savedRange) {
             var range = savedRange[key];
             var range = savedRange[key];
-            range.start.detach();
-            range.end.detach();
+            if (range.start !== cursorAnchor) { range.start.detach(); }
+            if (range.end !== cursorAnchor) { range.end.detach(); }
             session.removeMarker(range.id);
             session.removeMarker(range.id);
-            delete savedRange[key];
         }
         }
-
+        savedRange = {};
+        hints = [];
+        suppressions = [];
+        session.clearAnnotations();
     });
     });
 
 
     return worker;
     return worker;

+ 1 - 1
services/web/public/js/ace-1.2.5/worker-latex.js

@@ -1755,7 +1755,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
                     };
                     };
 
 
                     if (endToken) {
                     if (endToken) {
-                        TokenErrorFromTo(token, endToken, "invalid environment command" + text.substring(token[2], endToken[3] || endToken[2]));
+                        TokenErrorFromTo(token, endToken, "invalid environment command " + text.substring(token[2], endToken[3] || endToken[2]));
                     } else {
                     } else {
                         TokenError(token, "invalid environment command");
                         TokenError(token, "invalid environment command");
                     };
                     };

+ 8 - 1
services/web/public/stylesheets/app/account-settings.less

@@ -2,4 +2,11 @@
 	.alert {
 	.alert {
 		margin-bottom: 0;
 		margin-bottom: 0;
 	}
 	}
-}
+}
+
+#delete-account-modal {
+	.alert {
+		margin-top: 25px;
+		margin-bottom: 4px;
+	}
+}

+ 2 - 0
services/web/public/stylesheets/app/editor.less

@@ -414,11 +414,13 @@
 	}
 	}
 }
 }
 
 
+.teaser-title,
 .dropbox-teaser-title {
 .dropbox-teaser-title {
 	margin-top: 0;
 	margin-top: 0;
 	text-align: center;
 	text-align: center;
 }
 }
 
 
+.teaser-img,
 .dropbox-teaser-img {
 .dropbox-teaser-img {
 	.img-responsive;
 	.img-responsive;
 	margin-bottom: 5px;
 	margin-bottom: 5px;

+ 13 - 0
services/web/public/stylesheets/app/editor/history.less

@@ -25,6 +25,19 @@
 			background-color: white;
 			background-color: white;
 			border-radius: 8px;
 			border-radius: 8px;
 		}
 		}
+		.message-wider {
+			width: 650px;
+			margin-top: 60px;
+			padding: 0;
+		}
+
+			.message-header {
+				.modal-header;
+			}
+
+			.message-body {
+				.modal-body;
+			}
 	}
 	}
 
 
 	.diff-panel {
 	.diff-panel {

+ 0 - 24
services/web/test/UnitTests/coffee/Email/EmailBuilderTests.coffee

@@ -18,30 +18,6 @@ describe "EmailBuilder", ->
 			"settings-sharelatex":@settings
 			"settings-sharelatex":@settings
 			"logger-sharelatex": log:->
 			"logger-sharelatex": log:->
 
 
-	describe "projectSharedWithYou", ->
-		beforeEach ->
-			@opts =
-				to:"bob@bob.com"
-				first_name:"bob"
-				owner:
-					email:"sally@hally.com"
-				project:
-					url:"http://www.project.com"
-					name:"standard project"
-			@email = @EmailBuilder.buildEmail("projectSharedWithYou", @opts)
-
-		it "should insert the owner email into the template", ->
-			@email.html.indexOf(@opts.owner.email).should.not.equal -1
-			@email.subject.indexOf(@opts.owner.email).should.not.equal -1
-
-		it 'should not have text component', ->
-			expect(@email.html?).to.equal true
-			expect(@email.text?).to.equal false
-
-		it "should not have undefined in it", ->
-			@email.html.indexOf("undefined").should.equal -1
-			@email.subject.indexOf("undefined").should.equal -1
-
 	describe "projectInvite", ->
 	describe "projectInvite", ->
 		beforeEach ->
 		beforeEach ->
 			@opts =
 			@opts =

+ 23 - 1
services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee

@@ -20,7 +20,7 @@ mockSubscriptions =
 describe "SubscriptionController sanboxed", ->
 describe "SubscriptionController sanboxed", ->
 
 
 	beforeEach ->
 	beforeEach ->
-		@user = {email:"tom@yahoo.com", _id: 'one'}
+		@user = {email:"tom@yahoo.com", _id: 'one', signUpDate: new Date('2000-10-01')}
 		@activeRecurlySubscription = mockSubscriptions["subscription-123-active"]
 		@activeRecurlySubscription = mockSubscriptions["subscription-123-active"]
 
 
 		@AuthenticationController =
 		@AuthenticationController =
@@ -63,6 +63,8 @@ describe "SubscriptionController sanboxed", ->
 			getCurrencyCode:sinon.stub()
 			getCurrencyCode:sinon.stub()
 		@SubscriptionDomainHandler =
 		@SubscriptionDomainHandler =
 			getDomainLicencePage:sinon.stub()
 			getDomainLicencePage:sinon.stub()
+		@UserGetter =
+			getUser: sinon.stub().callsArgWith(2, null, @user)
 		@SubscriptionController = SandboxedModule.require modulePath, requires:
 		@SubscriptionController = SandboxedModule.require modulePath, requires:
 			'../Authentication/AuthenticationController': @AuthenticationController
 			'../Authentication/AuthenticationController': @AuthenticationController
 			'./SubscriptionHandler': @SubscriptionHandler
 			'./SubscriptionHandler': @SubscriptionHandler
@@ -76,6 +78,7 @@ describe "SubscriptionController sanboxed", ->
 				warn:->
 				warn:->
 			"settings-sharelatex": @settings
 			"settings-sharelatex": @settings
 			"./SubscriptionDomainHandler":@SubscriptionDomainHandler
 			"./SubscriptionDomainHandler":@SubscriptionDomainHandler
+			"../User/UserGetter": @UserGetter
 
 
 
 
 		@res = new MockResponse()
 		@res = new MockResponse()
@@ -92,12 +95,31 @@ describe "SubscriptionController sanboxed", ->
 			@GeoIpLookup.getCurrencyCode.callsArgWith(1, null, @stubbedCurrencyCode)
 			@GeoIpLookup.getCurrencyCode.callsArgWith(1, null, @stubbedCurrencyCode)
 			@res.callback = done
 			@res.callback = done
 			@SubscriptionController.plansPage(@req, @res)
 			@SubscriptionController.plansPage(@req, @res)
+			@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
 
 
 		it "should set the recommended currency from the geoiplookup", (done)->
 		it "should set the recommended currency from the geoiplookup", (done)->
 			@res.renderedVariables.recomendedCurrency.should.equal(@stubbedCurrencyCode)
 			@res.renderedVariables.recomendedCurrency.should.equal(@stubbedCurrencyCode)
 			@GeoIpLookup.getCurrencyCode.calledWith(@req.ip).should.equal true
 			@GeoIpLookup.getCurrencyCode.calledWith(@req.ip).should.equal true
 			done()
 			done()
 
 
+		it 'should fetch the current user', (done) ->
+			@UserGetter.getUser.callCount.should.equal 1
+			done()
+
+		it 'should decide not to AB test the plans', (done) ->
+			@res.renderedVariables.shouldABTestPlans.should.equal false
+			done()
+
+		describe 'when user is not logged in', (done) ->
+
+			beforeEach ->
+				@AuthenticationController.getLoggedInUserId.returns(null)
+
+			it 'should not fetch the current user', (done) ->
+				@UserGetter.getUser.callCount.should.equal 0
+				done()
+
+
 	describe "editBillingDetailsPage", ->
 	describe "editBillingDetailsPage", ->
 		describe "with a user with a subscription", ->
 		describe "with a user with a subscription", ->
 			beforeEach (done) ->
 			beforeEach (done) ->

+ 71 - 5
services/web/test/UnitTests/coffee/User/UserControllerTests.coffee

@@ -84,15 +84,81 @@ describe "UserController", ->
 			sendStatus: sinon.stub()
 			sendStatus: sinon.stub()
 			json: sinon.stub()
 			json: sinon.stub()
 		@next = sinon.stub()
 		@next = sinon.stub()
-	describe "deleteUser", ->
 
 
-		it "should delete the user", (done)->
+	describe 'tryDeleteUser', ->
 
 
-			@res.sendStatus = (code)=>
-				@UserDeleter.deleteUser.calledWith(@user_id)
+		beforeEach ->
+			@req.body.password = 'wat'
+			@AuthenticationController.getLoggedInUserId = sinon.stub().returns(@user._id)
+			@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user)
+			@UserDeleter.deleteUser = sinon.stub().callsArgWith(1, null)
+
+		it 'should send 200', (done) ->
+			@res.sendStatus = (code) =>
 				code.should.equal 200
 				code.should.equal 200
 				done()
 				done()
-			@UserController.deleteUser @req, @res
+			@UserController.tryDeleteUser @req, @res, @next
+
+		it 'should try to authenticate user', (done) ->
+			@res.sendStatus = (code) =>
+				@AuthenticationManager.authenticate.callCount.should.equal 1
+				@AuthenticationManager.authenticate.calledWith({_id: @user._id}, @req.body.password).should.equal true
+				done()
+			@UserController.tryDeleteUser @req, @res, @next
+
+		it 'should delete the user', (done) ->
+			@res.sendStatus = (code) =>
+				@UserDeleter.deleteUser.callCount.should.equal 1
+				@UserDeleter.deleteUser.calledWith(@user._id).should.equal true
+				done()
+			@UserController.tryDeleteUser @req, @res, @next
+
+		describe 'when no password is supplied', ->
+
+			beforeEach ->
+				@req.body.password = ''
+
+			it 'should return 403', (done) ->
+				@res.sendStatus = (code) =>
+					code.should.equal 403
+					done()
+				@UserController.tryDeleteUser @req, @res, @next
+
+		describe 'when authenticate produces an error', ->
+
+			beforeEach ->
+				@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, new Error('woops'))
+
+			it 'should call next with an error', (done) ->
+				@next = (err) =>
+					expect(err).to.not.equal null
+					expect(err).to.be.instanceof Error
+					done()
+				@UserController.tryDeleteUser @req, @res, @next
+
+		describe 'when authenticate does not produce a user', ->
+
+			beforeEach ->
+				@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, null)
+
+			it 'should return 403', (done) ->
+				@res.sendStatus = (code) =>
+					code.should.equal 403
+					done()
+				@UserController.tryDeleteUser @req, @res, @next
+
+		describe 'when deleteUser produces an error', ->
+
+			beforeEach ->
+				@UserDeleter.deleteUser = sinon.stub().callsArgWith(1, new Error('woops'))
+
+			it 'should call next with an error', (done) ->
+				@next = (err) =>
+					expect(err).to.not.equal null
+					expect(err).to.be.instanceof Error
+					done()
+				@UserController.tryDeleteUser @req, @res, @next
+
 
 
 	describe "unsubscribe", ->
 	describe "unsubscribe", ->