From d990ae1a1a24080621993b9a4c3a34c72b4313ce Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 25 Jul 2018 13:59:21 -0500 Subject: [PATCH 01/20] Style for portals --- .../web/public/stylesheets/app/portals.less | 161 ++++++++++++++++++ .../components/embed-responsive.less | 26 +++ .../public/stylesheets/components/icons.less | 9 + .../stylesheets/core/_common-variables.less | 9 +- .../public/stylesheets/core/ol-variables.less | 22 +++ services/web/public/stylesheets/ol-style.less | 5 +- 6 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 services/web/public/stylesheets/app/portals.less create mode 100644 services/web/public/stylesheets/components/embed-responsive.less create mode 100644 services/web/public/stylesheets/components/icons.less diff --git a/services/web/public/stylesheets/app/portals.less b/services/web/public/stylesheets/app/portals.less new file mode 100644 index 0000000000..ae6fcdf230 --- /dev/null +++ b/services/web/public/stylesheets/app/portals.less @@ -0,0 +1,161 @@ +.content-portal { + padding-top: @navbar-height!important; + + /* + Begin Header + */ + .banner-image { + background-size: cover; + background-position: 50% 50%; + background-repeat: no-repeat; + height: 375px; + } + + .image-fill { + display: inline-block; + height: 100%; + vertical-align: middle; + } + + .institution-logo { + left: 50%; + margin-left: -100px; + padding: 0; + position: absolute; + div { + background-color: @white; + box-shadow: 1px 11px 22px -9px @black-alpha-strong; + display: inline-block; + height: 125px; + overflow: hidden; + position: absolute; + text-align: center; + top: -110px; + white-space: nowrap; + width: @btn-portal-width; + } + img { + max-height: 75px; + max-width: 150px; + vertical-align: middle; + } + } + + .portal-name { + background-color: @ol-blue-gray-0; + padding-bottom: @line-height-computed; //- center header when no tabs + padding-top: @padding-md; + text-align: center; + width: 100%; + } + // End Header + + /* + Begin Layout + */ + .button-pull, + .content-pull { + float: left; + } + + .button-pull { + text-align: right; + > a.btn { + white-space: normal; + width: @btn-portal-width; + text-align: center; + } + } + .content-pull { + padding-right: @padding-sm; + width: calc(~"100% - "@btn-portal-width); + } + // End Layout + + /* + Begin Card + */ + .card { + margin-bottom: @margin-md; + } + // End Card + + /* + Begin Actions + */ + .portal-actions { + i { + margin-bottom: @margin-sm; + } + } + // End Actions + + /* + Begin Print + */ + .print { + .hidden-print { + display: none; + } + } + // End Print + + /* + Begin Tabs + */ + .nav-tabs { + // Overrides for nav.less + background-color: @ol-blue-gray-0; + border: 0!important; + margin-bottom: @margin-md; + margin-top: -@line-height-computed; //- adjusted for portal-name + padding: @padding-lg 0 @padding-md; + text-align: center; + + a { + color: @link-color; + &:hover { + background-color: transparent!important; + border: 0!important; + color: @link-hover-color!important; + } + } + + li { + display: inline-block; + float: none; + a { + border: 0; + } + } + + li.active > a { + background-color: transparent!important; + border: 0; + border-bottom: 1px solid @accent-color-secondary!important; + color: @accent-color-secondary; + &:hover { + color: @accent-color-secondary!important; + } + } + } + + .tab-content:extend(.container) { + background-color: transparent!important; + border: none!important; + } + // End Tabs + + @media (max-width: @screen-size-sm-max) { + .content-pull { + padding: 0; + width: auto; + } + + .button-pull { + > a.btn { + width: auto; + } + } + } +} diff --git a/services/web/public/stylesheets/components/embed-responsive.less b/services/web/public/stylesheets/components/embed-responsive.less new file mode 100644 index 0000000000..997f100449 --- /dev/null +++ b/services/web/public/stylesheets/components/embed-responsive.less @@ -0,0 +1,26 @@ +.embed-responsive { + display: block; + height: 0; + overflow: hidden; + padding: 0; + position: relative; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} +.embed-responsive-16by9 { + padding-bottom: 56.25% !important; +} +.embed-responsive-4by3 { + padding-bottom: 75% !important; +} \ No newline at end of file diff --git a/services/web/public/stylesheets/components/icons.less b/services/web/public/stylesheets/components/icons.less new file mode 100644 index 0000000000..fa808ad5ae --- /dev/null +++ b/services/web/public/stylesheets/components/icons.less @@ -0,0 +1,9 @@ +// Colors +.icon-accent { + color: @accent-color-secondary; +} + +// Sizes +.icon-lg { + font-size: @font-size-h1; +} \ No newline at end of file diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index fefe12da73..db6fbf3a7e 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -62,9 +62,12 @@ // //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). -@margin-sm: 10px; -@margin-md: 20px; -@margin-lg: 30px; +@margin-xs: 5px; +@margin-sm: 10px; +@margin-md: 20px; +@margin-lg: 30px; +@margin-xl: 40px; +@margin-xxl: 50px; @padding-base-vertical: 5px; @padding-base-horizontal: 16px; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 0e05ccdd68..d3c21dff6a 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -6,6 +6,7 @@ @footer-height: 50px; // Styleguide colors +@ol-blue-gray-0 : #f4f5f8; @ol-blue-gray-1 : #E4E8EE; @ol-blue-gray-2 : #9DA7B7; @ol-blue-gray-3 : #5D6879; @@ -21,6 +22,8 @@ @ol-dark-red : #A6312B; @ol-type-color : @ol-blue-gray-3; +@accent-color-primary: @ol-green; +@accent-color-secondary: @ol-dark-green; // Navbar customization @navbar-title-color : @ol-blue-gray-1; @@ -65,8 +68,14 @@ @btn-info-bg : @ol-blue; @btn-info-border : transparent; +// Padding @padding-xs-horizontal : 8px; +@padding-sm: 10px; +@padding-md: 20px; +@padding-lg: 30px; +@padding-xl: 40px; + // Alerts @alert-padding : 15px; @alert-border-radius : @border-radius-base; @@ -167,6 +176,9 @@ @folders-tag-menu-hover : rgba(0, 0, 0, .1); @folders-tag-menu-active-hover : rgba(0, 0, 0, .1); +// Portal +@btn-portal-width : 200px; + // Project table @structured-list-line-height : 2.5; @structured-list-link-color : @ol-blue; @@ -273,6 +285,9 @@ @log-line-no-color : #FFF; @log-hints-color : @ol-blue-gray-4; +// Portals +@black-alpha-strong : rgba(0,0,0,0.8); + // v2 History @history-base-font-size : @font-size-small; @@ -284,6 +299,12 @@ @history-toolbar-bg-color : @editor-toolbar-bg; @history-toolbar-color : #FFF; +// Screens +// added -size to not conflict with common_variables +@screen-size-sm-max : 767px; +@screen-size-md-min : 768px; +@screen-size-md-max : 991px; + // System messages @sys-msg-background : @ol-blue; @sys-msg-color : #FFF; @@ -300,6 +321,7 @@ @gray-light: #a4a4a4; @gray-lighter: #cfcfcf; @gray-lightest: #f0f0f0; +@white: #ffffff; @blue: #405ebf; @blueDark: #040D2D; diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less index 03e2bb3ed6..774e70a2ab 100644 --- a/services/web/public/stylesheets/ol-style.less +++ b/services/web/public/stylesheets/ol-style.less @@ -6,8 +6,11 @@ @import "app/ol-style-guide.less"; @import "_style_includes.less"; @import "_ol_style_includes.less"; +@import "components/embed-responsive.less"; +@import "components/icons.less"; // Pages @import "app/about.less"; @import "app/blog-posts.less"; -@import "app/cms-page.less"; \ No newline at end of file +@import "app/cms-page.less"; +@import "app/portals.less"; \ No newline at end of file From c7941ac00e474cab6dede5003ef1f09e80ce3f61 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Wed, 25 Jul 2018 14:39:40 -0500 Subject: [PATCH 02/20] Links mixins --- services/web/app/views/_mixins_links.pug | 75 ++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 services/web/app/views/_mixins_links.pug diff --git a/services/web/app/views/_mixins_links.pug b/services/web/app/views/_mixins_links.pug new file mode 100644 index 0000000000..18b208270c --- /dev/null +++ b/services/web/app/views/_mixins_links.pug @@ -0,0 +1,75 @@ +mixin linkAdvisors(linkText, linkClass, tracked) + //- To Do: verify path + //- To Do: track + a(href="/advisors" + class=linkClass ? linkClass : '' + eventTrackingGa=track ? 'advisors' : null + ) + | #{linkText ? linkText : 'advisor programme'} + +mixin linkBenefits(linkText, linkClass) + //- To Do: verify path + a(href="/benefits" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'benefits'} + +mixin linkBlog(linkText, linkClass, slug) + if slug + a(href="/blog/#{slug}" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'blog'} + +mixin linkContact(linkText, linkClass) + a(href="/contact" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'contact'} + +mixin linkEducation(linkText, linkClass) + //- To Do: verify path + a(href="/plans" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'teaching toolkit'} + +mixin linkEmail(linkText, linkClass, email) + //- To Do: env var? + - var emailDomain = 'overleaf.com' + a(href="mailto:#{email ? email : 'contact'}@#{emailDomain}" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'email'} + +mixin linkInvite(linkText, linkClass) + a(href="/user/bonus" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'invite your friends'} + +mixin linkPlansAndPricing(linkText, linkClass) + //- To Do: verify path + a(href="/plans" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'plans and pricing'} + +mixin linkPrintNewTab(linkText, linkClass, icon, track) + //- To Do: track + a(href='?media=print' + class=linkClass ? linkClass : '' + eventTrackingGa=track ? 'print' : null + target="_BLANK" + ) + if icon + i(class="fa fa-print") + |   + | #{linkText ? linkText : 'print'} + +mixin linkSignIn(linkText, linkClass) + a(href="/login" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'sign in'} + +mixin linkSignUp(linkText, linkClass) + a(href="/register" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'sign up'} + +mixin linkTweet(linkText, linkClass, tweetText, track) + //- twitter-share-button is required by twitter + //- To Do: track + a(class="twitter-share-button " + linkClass + href="https://twitter.com/intent/tweet?text=" + tweetText + target="_BLANK" + ) #{linkText ? linkText : 'tweet'} + +mixin linkUniversities(linkText, linkClass) + //- To Do: verify path + a(href="/universities" class=linkClass ? linkClass : '') + | #{linkText ? linkText : 'universities'} From 17a63258c6ee4ea24e5d10bad4c3bf85d91fdf01 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Sat, 4 Aug 2018 17:30:24 +0100 Subject: [PATCH 03/20] changed newsletter to use mailchimp --- .../Newsletter/NewsletterManager.coffee | 51 ++++++++++++------- services/web/config/settings.defaults.coffee | 6 +-- services/web/package.json | 1 + 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 8d7eee43bf..7baa317632 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -1,37 +1,52 @@ async = require('async') -Request = require('request') logger = require 'logger-sharelatex' Settings = require 'settings-sharelatex' +crypto = require('crypto') +Mailchimp = require('mailchimp-api-v3') +mailchimp = new Mailchimp(Settings.mailchimp?.api_key) if Settings.mailchimp? module.exports = subscribe: (user, callback = () ->)-> - if !Settings.markdownmail? + if !Settings.mailchimp? logger.warn "No newsletter provider configured so not subscribing user" return callback() - logger.log user:user, email:user.email, "trying to subscribe user to the mailing list" options = buildOptions(user, true) - Request.post options, (err, response, body)-> - logger.log body:body, user:user, "finished attempting to subscribe the user to the news letter" + logger.log options:options, user:user, email:user.email, "trying to subscribe user to the mailing list" + mailchimp.request options, (err)-> + if err? + logger.err err:err, "error subscribing person to newsletter" + else + logger.log user:user, "finished subscribing user to the newsletter" callback(err) unsubscribe: (user, callback = () ->)-> - if !Settings.markdownmail? + if !Settings.mailchimp? logger.warn "No newsletter provider configured so not unsubscribing user" return callback() logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list" options = buildOptions(user, false) - Request.post options, (err, response, body)-> - logger.log err:err, body:body, email:user.email, "compled newsletter unsubscribe attempt" + mailchimp.request options, (err)-> + if err? + logger.err err:err, "error unsubscribing person to newsletter" + else + logger.log user:user, "finished unsubscribing user to the newsletter" callback(err) +hashEmail = (email)-> + crypto.createHash('md5').update(email.toLowerCase()).digest("hex") + buildOptions = (user, is_subscribed)-> - options = - json: - secret_token: Settings.markdownmail.secret - name: "#{user.first_name} #{user.last_name}" - email: user.email - subscriber_list_id: Settings.markdownmail.list_id - is_subscribed: is_subscribed - url: "https://www.markdownmail.io/lists/subscribe" - timeout: 30 * 1000 - return options \ No newline at end of file + status = if is_subscribed then "subscribed" else "unsubscribed" + subscriber_hash = hashEmail(user.email) + opts = + method: "PUT" + path: "/lists/#{Settings.mailchimp?.list_id}/members/#{subscriber_hash}" + body: + status_if_new: status + status: status + email_address:user.email + merge_fields: + FNAME: user.first_name + LNAME: user.last_name + return opts + diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 1306ce91c2..827f4f2959 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -277,10 +277,10 @@ module.exports = settings = # Third party services # -------------------- # - # ShareLaTeX's regular newsletter is managed by Markdown mail. Add your + # ShareLaTeX's regular newsletter is managed by mailchimp. Add your # credentials here to integrate with this. - # markdownmail: - # secret: "" + # mailchimp: + # api_key: "" # list_id: "" # # Fill in your unique token from various analytics services to enable diff --git a/services/web/package.json b/services/web/package.json index ed0eb06455..1dce008612 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -59,6 +59,7 @@ "lodash": "^4.13.1", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master", "lynx": "0.1.1", + "mailchimp-api-v3": "^1.12.0", "marked": "^0.3.5", "method-override": "^2.3.3", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.7.1", From fa37caef58d84d645b40aa57e8d2dc1c6b2fabb5 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 6 Aug 2018 12:37:18 +0100 Subject: [PATCH 04/20] remove null check on mailchimp at top of file I didn't like the if statment being after the require, mailchimp could also be null and called elsewhere by acident --- .../app/coffee/Features/Newsletter/NewsletterManager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 7baa317632..4850554303 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -3,7 +3,6 @@ logger = require 'logger-sharelatex' Settings = require 'settings-sharelatex' crypto = require('crypto') Mailchimp = require('mailchimp-api-v3') -mailchimp = new Mailchimp(Settings.mailchimp?.api_key) if Settings.mailchimp? module.exports = subscribe: (user, callback = () ->)-> @@ -12,6 +11,7 @@ module.exports = return callback() options = buildOptions(user, true) logger.log options:options, user:user, email:user.email, "trying to subscribe user to the mailing list" + mailchimp = new Mailchimp(Settings.mailchimp?.api_key) mailchimp.request options, (err)-> if err? logger.err err:err, "error subscribing person to newsletter" @@ -25,6 +25,7 @@ module.exports = return callback() logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list" options = buildOptions(user, false) + mailchimp = new Mailchimp(Settings.mailchimp?.api_key) mailchimp.request options, (err)-> if err? logger.err err:err, "error unsubscribing person to newsletter" From 6cf1f71604923bd042c505e248be845f6c724fff Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 6 Aug 2018 17:43:03 +0100 Subject: [PATCH 05/20] add newsletter checkbox to user register forms --- .../coffee/Features/User/UserRegistrationHandler.coffee | 5 +++-- services/web/public/stylesheets/app/homepage.less | 4 ++++ .../unit/coffee/User/UserRegistrationHandlerTests.coffee | 8 +++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee index df7fe93218..848434271f 100644 --- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee @@ -1,4 +1,4 @@ -sanitize = require('sanitizer') +resanitize = require('sanitizer') User = require("../../models/User").User UserCreator = require("./UserCreator") UserGetter = require("./UserGetter") @@ -54,7 +54,8 @@ module.exports = UserRegistrationHandler = (cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb (cb)-> AuthenticationManager.setUserPassword user._id, userDetails.password, cb (cb)-> - NewsLetterManager.subscribe user, -> + if userDetails.subscribeToNewsletter == "true" + NewsLetterManager.subscribe user, -> cb() #this can be slow, just fire it off ], (err)-> logger.log user: user, "registered" diff --git a/services/web/public/stylesheets/app/homepage.less b/services/web/public/stylesheets/app/homepage.less index d502855b72..22efbc847d 100644 --- a/services/web/public/stylesheets/app/homepage.less +++ b/services/web/public/stylesheets/app/homepage.less @@ -47,7 +47,11 @@ } .form-group { margin-left: @line-height-computed / 2; + input[type="checkbox"] { + margin-right: 5px; + } } + } .screenshot { height: 550px; diff --git a/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee b/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee index e9a6c568dd..f8bcce30ce 100644 --- a/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee +++ b/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee @@ -132,11 +132,17 @@ describe "UserRegistrationHandler", -> @AuthenticationManager.setUserPassword.calledWith(@user._id, @passingRequest.password).should.equal true done() - it "should add the user to the news letter manager", (done)-> + it "should add the user to the newsletter if accepted terms", (done)-> + @passingRequest.subscribeToNewsletter = "true" @handler.registerNewUser @passingRequest, (err)=> @NewsLetterManager.subscribe.calledWith(@user).should.equal true done() + it "should not add the user to the newsletter if not accepted terms", (done)-> + @handler.registerNewUser @passingRequest, (err)=> + @NewsLetterManager.subscribe.calledWith(@user).should.equal false + done() + it "should track the registration event", (done)-> @handler.registerNewUser @passingRequest, (err)=> @AnalyticsManager.recordEvent From 88f2b3670bbe65f602a2b7b1c93be262a2c07b33 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 7 Aug 2018 16:48:35 +0100 Subject: [PATCH 06/20] remove unneed style --- services/web/public/stylesheets/app/homepage.less | 4 ---- 1 file changed, 4 deletions(-) diff --git a/services/web/public/stylesheets/app/homepage.less b/services/web/public/stylesheets/app/homepage.less index 22efbc847d..d502855b72 100644 --- a/services/web/public/stylesheets/app/homepage.less +++ b/services/web/public/stylesheets/app/homepage.less @@ -47,11 +47,7 @@ } .form-group { margin-left: @line-height-computed / 2; - input[type="checkbox"] { - margin-right: 5px; - } } - } .screenshot { height: 550px; From 6208e9f2d0cbf673ab1b69c0e2a48b915c2a9e4f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 7 Aug 2018 21:38:31 +0100 Subject: [PATCH 07/20] add changeEmail function to newsletter manager not actually called --- .../Newsletter/NewsletterManager.coffee | 30 ++++++++++++++----- .../coffee/Features/User/UserUpdater.coffee | 1 + 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 4850554303..0c1a98a6b1 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -4,14 +4,19 @@ Settings = require 'settings-sharelatex' crypto = require('crypto') Mailchimp = require('mailchimp-api-v3') + +if !Settings.mailchimp?.api_key? + logger.warn "No newsletter provider configured so not chaning email on user" + mailchimp = + request: (opts, cb)-> cb() +else + mailchimp = new Mailchimp(Settings.mailchimp?.api_key) + module.exports = + subscribe: (user, callback = () ->)-> - if !Settings.mailchimp? - logger.warn "No newsletter provider configured so not subscribing user" - return callback() options = buildOptions(user, true) logger.log options:options, user:user, email:user.email, "trying to subscribe user to the mailing list" - mailchimp = new Mailchimp(Settings.mailchimp?.api_key) mailchimp.request options, (err)-> if err? logger.err err:err, "error subscribing person to newsletter" @@ -20,12 +25,8 @@ module.exports = callback(err) unsubscribe: (user, callback = () ->)-> - if !Settings.mailchimp? - logger.warn "No newsletter provider configured so not unsubscribing user" - return callback() logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list" options = buildOptions(user, false) - mailchimp = new Mailchimp(Settings.mailchimp?.api_key) mailchimp.request options, (err)-> if err? logger.err err:err, "error unsubscribing person to newsletter" @@ -33,6 +34,19 @@ module.exports = logger.log user:user, "finished unsubscribing user to the newsletter" callback(err) + changeEmail: (oldEmail, newEmail, callback = ()->)-> + options = buildOptions({email:oldEmail}) + delete options.body.status + options.body.email_address = newEmail + mailchimp.request options, (err)-> + # if the user has unsubscribed mailchimp will error on email address change + if err? and err?.message.indexOf("could not be validated") == -1 + logger.err err:err, "error changing email in newsletter" + return callback(err) + else + logger.log "finished changing email in the newsletter" + return callback() + hashEmail = (email)-> crypto.createHash('md5').update(email.toLowerCase()).digest("hex") diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 8d5e3658f9..7d828366e7 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -11,6 +11,7 @@ EmailHelper = require "../Helpers/EmailHelper" Errors = require "../Errors/Errors" Settings = require "settings-sharelatex" request = require 'request' +NewsletterManager = require "../Newsletter/NewsletterManager" module.exports = UserUpdater = updateUser: (query, update, callback = (error) ->) -> From 687637eec71bacd2aa8b8a92ced6330aa2d9c728 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Aug 2018 13:15:51 +0100 Subject: [PATCH 08/20] change email address in newsletter when changing default email put mongo_id into mailchimp merge fields --- .../Newsletter/NewsletterManager.coffee | 2 +- .../coffee/Features/User/UserUpdater.coffee | 22 ++++++++++------ .../unit/coffee/User/UserUpdaterTests.coffee | 26 ++++++++++++++++--- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 0c1a98a6b1..431eb214c3 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -4,7 +4,6 @@ Settings = require 'settings-sharelatex' crypto = require('crypto') Mailchimp = require('mailchimp-api-v3') - if !Settings.mailchimp?.api_key? logger.warn "No newsletter provider configured so not chaning email on user" mailchimp = @@ -63,5 +62,6 @@ buildOptions = (user, is_subscribed)-> merge_fields: FNAME: user.first_name LNAME: user.last_name + MONGO_ID:user._id return opts diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 7d828366e7..5770c7b834 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -100,15 +100,21 @@ module.exports = UserUpdater = setDefaultEmailAddress: (userId, email, callback) -> email = EmailHelper.parseEmail(email) return callback(new Error('invalid email')) if !email? - query = _id: userId, 'emails.email': email - update = $set: email: email - @updateUser query, update, (error, res) -> - if error? - logger.err error:error, 'problem setting default emails' + UserGetter.getUserEmail userId, (error, oldEmail) => + if err? return callback(error) - if res.n == 0 # TODO: Check n or nMatched? - return callback(new Error('Default email does not belong to user')) - callback() + query = _id: userId, 'emails.email': email + update = $set: email: email + @updateUser query, update, (error, res) -> + if error? + logger.err error:error, 'problem setting default emails' + return callback(error) + else if res.n == 0 # TODO: Check n or nMatched? + return callback(new Error('Default email does not belong to user')) + else + NewsletterManager.changeEmail oldEmail, email, callback + + updateV1AndSetDefaultEmailAddress: (userId, email, callback) -> @updateEmailAddressInV1 userId, email, (error) => diff --git a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee index f2ac951727..17f691edba 100644 --- a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee +++ b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee @@ -18,21 +18,27 @@ describe "UserUpdater", -> getUserEmail: sinon.stub() getUserByAnyEmail: sinon.stub() ensureUniqueEmailAddress: sinon.stub() - @logger = err: sinon.stub(), log: -> + @logger = + err: sinon.stub() + log: -> + warn: -> @addAffiliation = sinon.stub().yields() @removeAffiliation = sinon.stub().callsArgWith(2, null) @refreshFeatures = sinon.stub().yields() + @NewsletterManager = + changeEmail:sinon.stub() @UserUpdater = SandboxedModule.require modulePath, requires: "logger-sharelatex": @logger + "../../infrastructure/mongojs":@mongojs + "metrics-sharelatex": timeAsyncMethod: sinon.stub() "./UserGetter": @UserGetter '../Institutions/InstitutionsAPI': addAffiliation: @addAffiliation removeAffiliation: @removeAffiliation '../Subscription/FeaturesUpdater': refreshFeatures: @refreshFeatures - "../../infrastructure/mongojs":@mongojs - "metrics-sharelatex": timeAsyncMethod: sinon.stub() "settings-sharelatex": @settings = {} "request": @request = {} + "../Newsletter/NewsletterManager": @NewsletterManager @stubbedUser = _id: "3131231" @@ -174,6 +180,10 @@ describe "UserUpdater", -> done() describe 'setDefaultEmailAddress', -> + beforeEach -> + @UserGetter.getUserEmail.callsArgWith(1, null, @stubbedUser.email) + @NewsletterManager.changeEmail.callsArgWith(2, null) + it 'set default', (done)-> @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1) @@ -185,6 +195,16 @@ describe "UserUpdater", -> ).should.equal true done() + it 'set changed the email in newsletter', (done)-> + @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1) + + @UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=> + should.not.exist(err) + @NewsletterManager.changeEmail.calledWith( + @stubbedUser.email, @newEmail + ).should.equal true + done() + it 'handle error', (done)-> @UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope')) From c68366155e0e2e56c47ba9c0c9492b4aa0d440f3 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Aug 2018 14:14:56 +0100 Subject: [PATCH 09/20] remove sanitize = require('sanitizer') not used anywhere --- .../web/app/coffee/Features/User/UserRegistrationHandler.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee index 848434271f..1291142dab 100644 --- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee @@ -1,4 +1,3 @@ -resanitize = require('sanitizer') User = require("../../models/User").User UserCreator = require("./UserCreator") UserGetter = require("./UserGetter") From df161d3ece9f11673e7cdc04fd6ea6ec36e02528 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Aug 2018 14:32:36 +0100 Subject: [PATCH 10/20] change newsletter log to info on process boot --- .../app/coffee/Features/Newsletter/NewsletterManager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee index 431eb214c3..5fb2e00eb7 100644 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee @@ -5,10 +5,11 @@ crypto = require('crypto') Mailchimp = require('mailchimp-api-v3') if !Settings.mailchimp?.api_key? - logger.warn "No newsletter provider configured so not chaning email on user" + logger.info "Using newsletter provider: none" mailchimp = request: (opts, cb)-> cb() else + logger.info "Using newsletter provider: mailchimp" mailchimp = new Mailchimp(Settings.mailchimp?.api_key) module.exports = From 7830a5f69de7bd197c943e03b60acff8bd7ba143 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 9 Aug 2018 13:16:42 +0100 Subject: [PATCH 11/20] Update copy in v1 import modal now that more features are supported --- services/web/app/views/project/list/modals.pug | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 2bbde60e7b..aaacf660a6 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -345,13 +345,11 @@ script(type="text/ng-template", id="v1ImportModalTemplate") i.fa.fa-flask .v1-import-col h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental - p We are still working hard to bring some Overleaf v1 features to the v2 editor. In v2 there is: + p We are still working hard to bring some Overleaf v1 features to the v2 editor. In v2: ul - li No Journals and Services menu to submit directly to our partners yet - li No Rich Text (WYSIWYG) mode yet - li No linked files (to URLs or to files in other Overleaf projects) yet - li No Zotero and CiteULike integrations yet - li No labelled versions yet + li You may not be able to access all of your Labelled versions yet + li There are no Zotero and CiteULike integrations yet + li Some Journals and Services in the Submit menu don't support direct submissions yet p.row-spaced-small | If you currently use the Overleaf Git bridge with your v1 project, you can migrate your project to the Overleaf v2 GitHub integration. | From e80b52509b8899e4f5f5539f5a979abcd498cc49 Mon Sep 17 00:00:00 2001 From: Nate Stemen Date: Thu, 9 Aug 2018 08:53:49 -0400 Subject: [PATCH 12/20] fix silly paren issue for index var --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 3be4369213..8fde8595cd 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -162,7 +162,8 @@ define [ cursorPosition = @editor.getCursorPosition() end = change.end {lineUpToCursor, commandFragment} = Helpers.getContext(@editor, end) - if (i = lineUpToCursor.indexOf('%') > -1 and lineUpToCursor[i-1] != '\\') + if ((i = lineUpToCursor.indexOf('%')) > -1 and lineUpToCursor[i-1] != '\\') + console.log lineUpToCursor, i return lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\" lastTwoChars = lineUpToCursor.slice(-2) From 197e7ce8abfd522cd647a0619e0e543252306dd3 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Fri, 10 Aug 2018 13:40:25 +0200 Subject: [PATCH 13/20] refactor institutions getter --- .../Institutions/InstitutionsFeatures.coffee | 11 +++-- .../Institutions/InstitutionsGetter.coffee | 14 ++++++ .../InstitutionsFeaturesTests.coffee | 38 ++++++---------- .../InstitutionsGetterTests.coffee | 44 +++++++++++++++++++ 4 files changed, 77 insertions(+), 30 deletions(-) create mode 100644 services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee create mode 100644 services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee index 5c91058146..4ce233406b 100644 --- a/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee +++ b/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee @@ -1,4 +1,4 @@ -UserGetter = require '../User/UserGetter' +InstitutionsGetter = require './InstitutionsGetter' PlansLocator = require '../Subscription/PlansLocator' Settings = require 'settings-sharelatex' logger = require 'logger-sharelatex' @@ -13,11 +13,10 @@ module.exports = InstitutionsFeatures = hasLicence: (userId, callback = (error, hasLicence) ->) -> - UserGetter.getUserFullEmails userId, (error, emailsData) -> + InstitutionsGetter.getConfirmedInstitutions userId, (error, institutions) -> return callback error if error? - affiliation = emailsData.find (emailData) -> - licence = emailData.affiliation?.institution?.licence - emailData.confirmedAt? and licence? and licence != 'free' + hasLicence = institutions.some (institution) -> + institution.licence and institution.licence != 'free' - callback(null, !!affiliation) + callback(null, hasLicence) diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee new file mode 100644 index 0000000000..e5941dc777 --- /dev/null +++ b/services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee @@ -0,0 +1,14 @@ +UserGetter = require '../User/UserGetter' +logger = require 'logger-sharelatex' + +module.exports = InstitutionsGetter = + getConfirmedInstitutions: (userId, callback = (error, institutions) ->) -> + UserGetter.getUserFullEmails userId, (error, emailsData) -> + return callback error if error? + + confirmedInstitutions = emailsData.filter (emailData) -> + emailData.confirmedAt? and emailData.affiliation?.institution? + .map (emailData) -> + emailData.affiliation?.institution + + callback(null, confirmedInstitutions) diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee index 2304f2e5b7..8811f72c35 100644 --- a/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee +++ b/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee @@ -8,11 +8,11 @@ modulePath = require('path').join __dirname, '../../../../app/js/Features/Instit describe 'InstitutionsFeatures', -> beforeEach -> - @UserGetter = getUserFullEmails: sinon.stub() + @InstitutionsGetter = getConfirmedInstitutions: sinon.stub() @PlansLocator = findLocalPlanInSettings: sinon.stub() @institutionPlanCode = 'institution_plan_code' @InstitutionsFeatures = SandboxedModule.require modulePath, requires: - '../User/UserGetter': @UserGetter + './InstitutionsGetter': @InstitutionsGetter '../Subscription/PlansLocator': @PlansLocator 'settings-sharelatex': institutionPlanCode: @institutionPlanCode 'logger-sharelatex': @@ -23,47 +23,37 @@ describe 'InstitutionsFeatures', -> describe "hasLicence", -> it 'should handle error', (done)-> - @UserGetter.getUserFullEmails.yields(new Error('Nope')) + @InstitutionsGetter.getConfirmedInstitutions.yields(new Error('Nope')) @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> expect(error).to.exist done() - it 'should return false if user has no affiliations', (done) -> - @UserGetter.getUserFullEmails.yields(null, []) - @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> - expect(error).to.not.exist - expect(hasLicence).to.be.false - done() - it 'should return false if user has no confirmed affiliations', (done) -> - affiliations = [ - { confirmedAt: null, affiliation: institution: { licence: 'pro_plus' } } - ] - @UserGetter.getUserFullEmails.yields(null, affiliations) + institutions = [] + @InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions) @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> expect(error).to.not.exist expect(hasLicence).to.be.false done() it 'should return false if user has no paid affiliations', (done) -> - affiliations = [ - { confirmedAt: new Date(), affiliation: institution: { licence: 'free' } } + institutions = [ + { licence: 'free' } ] - @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions) @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> expect(error).to.not.exist expect(hasLicence).to.be.false done() it 'should return true if user has confirmed paid affiliation', (done)-> - affiliations = [ - { confirmedAt: new Date(), affiliation: institution: { licence: 'pro_plus' } } - { confirmedAt: new Date(), affiliation: institution: { licence: 'free' } } - { confirmedAt: null, affiliation: institution: { licence: 'pro' } } - { confirmedAt: null, affiliation: institution: { licence: null } } - { confirmedAt: new Date(), affiliation: institution: {} } + institutions = [ + { licence: 'pro_plus' } + { licence: 'free' } + { licence: 'pro' } + { licence: null } ] - @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions) @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> expect(error).to.not.exist expect(hasLicence).to.be.true diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee new file mode 100644 index 0000000000..62c57cdaa9 --- /dev/null +++ b/services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee @@ -0,0 +1,44 @@ +SandboxedModule = require('sandboxed-module') +require('chai').should() +expect = require('chai').expect +sinon = require('sinon') +modulePath = require('path').join __dirname, '../../../../app/js/Features/Institutions/InstitutionsGetter.js' + +describe 'InstitutionsGetter', -> + beforeEach -> + @UserGetter = getUserFullEmails: sinon.stub() + @InstitutionsGetter = SandboxedModule.require modulePath, requires: + '../User/UserGetter': @UserGetter + 'logger-sharelatex': + log:-> console.log(arguments) + err:-> + + @userId = '12345abcde' + + describe "getConfirmedInstitutions", -> + it 'filters unconfirmed emails', (done) -> + @userEmails = [ + { confirmedAt: null, affiliation: institution: { id: 123 } } + { confirmedAt: new Date(), affiliation: institution: { id: 456 } } + { confirmedAt: new Date(), affiliation: null } + { confirmedAt: new Date(), affiliation: institution: null } + ] + @UserGetter.getUserFullEmails.yields(null, @userEmails) + @InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) -> + expect(error).to.not.exist + institutions.length.should.equal 1 + institutions[0].id.should.equal 456 + done() + + it 'should handle empty response', (done) -> + @UserGetter.getUserFullEmails.yields(null, []) + @InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) -> + expect(error).to.not.exist + institutions.length.should.equal 0 + done() + + it 'should handle error', (done) -> + @UserGetter.getUserFullEmails.yields(new Error('Nope')) + @InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) -> + expect(error).to.exist + done() From 0f8fe53bc90fccb4ff0ef1720aaf0c8e7a620b93 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 1 Aug 2018 09:37:14 +0100 Subject: [PATCH 14/20] Allow confirmed_at date to be specified --- services/web/app/coffee/Features/User/UserUpdater.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 8d5e3658f9..544f37a82e 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -152,7 +152,10 @@ module.exports = UserUpdater = else return callback new Error("non-success code from v1: #{response.statusCode}") - confirmEmail: (userId, email, callback) -> + confirmEmail: (userId, email, confirmedAt, callback) -> + if arguments.length == 3 + callback = confirmedAt + confirmedAt = new Date() email = EmailHelper.parseEmail(email) return callback(new Error('invalid email')) if !email? logger.log {userId, email}, 'confirming user email' @@ -166,7 +169,7 @@ module.exports = UserUpdater = 'emails.email': email update = $set: - 'emails.$.confirmedAt': new Date() + 'emails.$.confirmedAt': confirmedAt @updateUser query, update, (error, res) -> return callback(error) if error? logger.log {res, userId, email}, "tried to confirm email" From 19dfe5fc8bfe2778a253cfb41540e34ed3413cfc Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 13 Aug 2018 10:22:23 +0100 Subject: [PATCH 15/20] intial version of user setting for texlive imageName --- .../coffee/Features/Editor/EditorController.coffee | 7 +++++++ .../coffee/Features/Project/ProjectController.coffee | 5 +++++ .../Features/Project/ProjectEditorHandler.coffee | 3 ++- .../Features/Project/ProjectOptionsHandler.coffee | 10 ++++++++++ services/web/app/views/project/editor/left-menu.pug | 9 +++++++++ services/web/config/settings.defaults.coffee | 11 +++++++++++ .../settings/controllers/SettingsController.coffee | 11 +++++++++++ 7 files changed, 55 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index 846042312d..7dad02bde9 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -163,6 +163,13 @@ module.exports = EditorController = EditorRealTimeController.emitToRoom project_id, 'compilerUpdated', compiler callback() + setImageName : (project_id, imageName, callback = (err) ->) -> + ProjectOptionsHandler.setImageName project_id, imageName, (err) -> + return callback(err) if err? + logger.log imageName:imageName, project_id:project_id, "setting imageName" + EditorRealTimeController.emitToRoom project_id, 'imageNameUpdated', imageName + callback() + setSpellCheckLanguage : (project_id, languageCode, callback = (err) ->) -> ProjectOptionsHandler.setSpellCheckLanguage project_id, languageCode, (err) -> return callback(err) if err? diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index bb16855885..f3d12bf5c5 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -49,6 +49,10 @@ module.exports = ProjectController = jobs.push (callback) -> editorController.setCompiler project_id, req.body.compiler, callback + if req.body.imageName? + jobs.push (callback) -> + editorController.setImageName project_id, req.body.imageName, callback + if req.body.name? jobs.push (callback) -> editorController.renameProject project_id, req.body.name, callback @@ -347,6 +351,7 @@ module.exports = ProjectController = useV2History: !!project.overleaf?.history?.display richTextEnabled: Features.hasFeature('rich-text') showTestControls: req.query?.tc == 'true' || user.isAdmin + allowedImageNames: Settings.allowedImageNames || [] timer.done() _buildProjectList: (allProjects, v1Projects = [])-> diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 410dbf0351..f81c59c399 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -1,5 +1,5 @@ _ = require("underscore") - +Path = require 'path' module.exports = ProjectEditorHandler = trackChangesAvailable: false @@ -20,6 +20,7 @@ module.exports = ProjectEditorHandler = members: [] invites: invites tokens: project.tokens + imageName: if project.imageName? then Path.basename(project.imageName) else undefined if !result.invites? result.invites = [] diff --git a/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee index 0a0b02e40e..456b164683 100644 --- a/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee @@ -17,6 +17,16 @@ module.exports = if callback? callback() + setImageName : (project_id, imageName, callback = ()->)-> + logger.log project_id:project_id, imageName:imageName, "setting the imageName" + imageName = imageName.toLowerCase() + if ! _.some(settings.allowedImageNames, (allowed) -> imageName is allowed.imageName) + return callback() + conditions = {_id:project_id} + update = {imageName: settings.imageRoot + '/' + imageName} + Project.update conditions, update, {}, (err)-> + if callback? + callback() setSpellCheckLanguage: (project_id, languageCode, callback = ()->)-> logger.log project_id:project_id, languageCode:languageCode, "setting the spell check language" diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index 2cedc0b37e..bbfa4c480c 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -188,6 +188,15 @@ aside#left-menu.full-size( option(value="pdfjs") #{translate("built_in")} option(value="native") #{translate("native")} + if (getSessionUser() && getSessionUser().isAdmin && typeof(allowedImageNames) !== 'undefined' && allowedImageNames.length > 0) + .form-controls(ng-show="permissions.write") + label(for="imageName") #{translate("TeXLive")} + select( + name="imageName" + ng-model="project.imageName" + ) + each image in allowedImageNames + option(value=image.imageName) #{image.imageDesc} h4 #{translate("hotkeys")} ul.list-unstyled.nav diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 1306ce91c2..d6e6ecb690 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -472,3 +472,14 @@ module.exports = settings = autoCompile: everyone: 100 standard: 25 + + # currentImage: "texlive-full:2017.1" + # imageRoot: "" # without any trailing slash + + # allowedImageNames: [ + # {imageName: 'texlive-full:2017.1', imageDesc: 'TeXLive 2017'} + # {imageName: 'wl_texlive:2018.1', imageDesc: 'Legacy OL TeXLive 2015'} + # {imageName: 'texlive-full:2016.1', imageDesc: 'Legacy SL TeXLive 2016'} + # {imageName: 'texlive-full:2015.1', imageDesc: 'Legacy SL TeXLive 2015'} + # {imageName: 'texlive-full:2014.2', imageDesc: 'Legacy SL TeXLive 2014.2'} + # ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee index 9920372aab..2e8c09739e 100644 --- a/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee +++ b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee @@ -67,6 +67,11 @@ define [ if oldCompiler? and compiler != oldCompiler settings.saveProjectSettings({compiler: compiler}) + $scope.$watch "project.imageName", (imageName, oldImageName) => + return if @ignoreUpdates + if oldImageName? and imageName != oldImageName + settings.saveProjectSettings({imageName: imageName}) + $scope.$watch "project.rootDoc_id", (rootDoc_id, oldRootDoc_id) => return if @ignoreUpdates # don't save on initialisation, Angular passes oldRootDoc_id as @@ -83,6 +88,12 @@ define [ $scope.project.compiler = compiler delete @ignoreUpdates + ide.socket.on "imageNameUpdated", (imageName) => + @ignoreUpdates = true + $scope.$apply () => + $scope.project.imageName = imageName + delete @ignoreUpdates + ide.socket.on "spellCheckLanguageUpdated", (languageCode) => @ignoreUpdates = true $scope.$apply () => From c2828c8ca539031230306f405a1d5b5410917aed Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 13 Aug 2018 10:48:11 +0100 Subject: [PATCH 16/20] add unit tests --- .../coffee/Editor/EditorControllerTests.coffee | 14 ++++++++++++++ .../Project/ProjectControllerTests.coffee | 12 ++++++++++++ .../Project/ProjectOptionsHandlerTests.coffee | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee index 703a03d14f..f1bf3792a2 100644 --- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee @@ -32,6 +32,7 @@ describe "EditorController", -> '../Project/ProjectEntityUpdateHandler' : @ProjectEntityUpdateHandler = {} '../Project/ProjectOptionsHandler' : @ProjectOptionsHandler = setCompiler: sinon.stub().yields() + setImageName: sinon.stub().yields() setSpellCheckLanguage: sinon.stub().yields() '../Project/ProjectDetailsHandler': @ProjectDetailsHandler = setProjectDescription: sinon.stub().yields() @@ -377,6 +378,19 @@ describe "EditorController", -> .calledWith(@project_id, "compilerUpdated", @compiler) .should.equal true + describe "setImageName", -> + beforeEach -> + @imageName = "texlive-1234.5" + @EditorController.setImageName @project_id, @imageName, @callback + + it "should send the new imageName and project id to the project options handler", -> + @ProjectOptionsHandler.setImageName + .calledWith(@project_id, @imageName) + .should.equal true + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "imageNameUpdated", @imageName) + .should.equal true + describe "setSpellCheckLanguage", -> beforeEach -> @languageCode = "fr" diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index 1acd1c8128..46e54501c9 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -145,6 +145,18 @@ describe "ProjectController", -> done() @ProjectController.updateProjectSettings @req, @res + it "should update the imageName", (done) -> + @EditorController.setImageName = sinon.stub().callsArg(2) + @req.body = + imageName: @imageName = "texlive-1234.5" + @res.sendStatus = (code) => + @EditorController.setImageName + .calledWith(@project_id, @imageName) + .should.equal true + code.should.equal 204 + done() + @ProjectController.updateProjectSettings @req, @res + it "should update the spell check language", (done) -> @EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2) @req.body = diff --git a/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee index 825822c236..435dba9dbc 100644 --- a/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee @@ -19,6 +19,11 @@ describe 'creating a project', -> {name: "English", code: "en"} {name: "French", code: "fr"} ] + imageRoot: "docker-repo/subdir" + allowedImageNames: [ + {imageName: "texlive-0000.0", imageDesc: "test image 0"} + {imageName: "texlive-1234.5", imageDesc: "test image 1"} + ] 'logger-sharelatex': log:-> err:-> @@ -37,6 +42,19 @@ describe 'creating a project', -> @projectModel.update.called.should.equal false done() + describe 'Setting the imageName', -> + it 'should perform and update on mongo', (done)-> + @handler.setImageName project_id, "texlive-1234.5", (err)=> + args = @projectModel.update.args[0] + args[0]._id.should.equal project_id + args[1].imageName.should.equal "docker-repo/subdir/texlive-1234.5" + done() + @projectModel.update.args[0][3]() + + it 'should not perform and update on mongo if it is not a reconised compiler', (done)-> + @handler.setImageName project_id, "something", (err)=> + @projectModel.update.called.should.equal false + done() describe "setting the spellCheckLanguage", -> From bfb2f636cdf50f0b5cf91019b7c51892af1893aa Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Mon, 13 Aug 2018 14:59:39 +0200 Subject: [PATCH 17/20] enable affiliations UI --- .../web/app/coffee/Features/User/UserPagesController.coffee | 1 - services/web/app/views/user/settings.pug | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 2a7ed62d02..5e6ea7d62b 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -68,7 +68,6 @@ module.exports = shouldAllowEditingDetails: shouldAllowEditingDetails languages: Settings.languages, accountSettingsTabActive: true - showAffiliationsUI: (req.query?.aff == "true") or false sessionsPage: (req, res, next) -> user = AuthenticationController.getSessionUser(req) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index a43e81c12c..e4da83c1db 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -9,7 +9,7 @@ block content .page-header h1 #{translate("account_settings")} .account-settings(ng-controller="AccountSettingsController", ng-cloak) - if locals.showAffiliationsUI && hasFeature('affiliations') + if hasFeature('affiliations') include settings/user-affiliations form-messages(for="settingsForm") @@ -22,7 +22,7 @@ block content h3 #{translate("update_account_info")} form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate) input(type="hidden", name="_csrf", value=csrfToken) - if !(locals.showAffiliationsUI && hasFeature('affiliations')) + if !hasFeature('affiliations') if !externalAuthenticationSystemUsed() .form-group label(for='email') #{translate("email")} From 4a65a526cec540290941165f1762dda51f196a57 Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Mon, 13 Aug 2018 17:03:48 +0200 Subject: [PATCH 18/20] require minilist package --- services/web/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/package.json b/services/web/package.json index ed0eb06455..ae0a51a3c4 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -98,7 +98,8 @@ "v8-profiler": "^5.2.3", "valid-url": "^1.0.9", "xml2js": "0.2.0", - "yauzl": "^2.8.0" + "yauzl": "^2.8.0", + "minimist": "1.2.0" }, "devDependencies": { "autoprefixer": "^6.6.1", From 2c9977d26ed642453246da96e8ada574e30189ab Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Thu, 9 Aug 2018 11:02:48 -0500 Subject: [PATCH 19/20] Add analytics to link mixins --- services/web/app/views/_mixins_links.pug | 63 +++++++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/_mixins_links.pug b/services/web/app/views/_mixins_links.pug index 18b208270c..bad56ffc25 100644 --- a/services/web/app/views/_mixins_links.pug +++ b/services/web/app/views/_mixins_links.pug @@ -1,9 +1,19 @@ -mixin linkAdvisors(linkText, linkClass, tracked) +mixin linkAdvisors(linkText, linkClass, track) //- To Do: verify path - //- To Do: track + - var gaCategory = track && track.category ? track.category : 'All' + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null a(href="/advisors" class=linkClass ? linkClass : '' - eventTrackingGa=track ? 'advisors' : null + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation ) | #{linkText ? linkText : 'advisor programme'} @@ -32,8 +42,23 @@ mixin linkEmail(linkText, linkClass, email) a(href="mailto:#{email ? email : 'contact'}@#{emailDomain}" class=linkClass ? linkClass : '') | #{linkText ? linkText : 'email'} -mixin linkInvite(linkText, linkClass) - a(href="/user/bonus" class=linkClass ? linkClass : '') +mixin linkInvite(linkText, linkClass, track) + - var gaCategory = track && track.category ? track.category : 'All' + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null + + a(href="/user/bonus" + class=linkClass ? linkClass : '' + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation + ) | #{linkText ? linkText : 'invite your friends'} mixin linkPlansAndPricing(linkText, linkClass) @@ -42,10 +67,21 @@ mixin linkPlansAndPricing(linkText, linkClass) | #{linkText ? linkText : 'plans and pricing'} mixin linkPrintNewTab(linkText, linkClass, icon, track) - //- To Do: track + - var gaCategory = track && track.category ? track.category : null + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null + a(href='?media=print' class=linkClass ? linkClass : '' - eventTrackingGa=track ? 'print' : null + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation target="_BLANK" ) if icon @@ -63,8 +99,19 @@ mixin linkSignUp(linkText, linkClass) mixin linkTweet(linkText, linkClass, tweetText, track) //- twitter-share-button is required by twitter - //- To Do: track + - var gaCategory = track && track.category ? track.category : 'All' + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null a(class="twitter-share-button " + linkClass + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation href="https://twitter.com/intent/tweet?text=" + tweetText target="_BLANK" ) #{linkText ? linkText : 'tweet'} From af6e7758d38b6edff684674075bbad519446d39b Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 15 Aug 2018 15:26:22 +0100 Subject: [PATCH 20/20] configure backend group name via config files --- services/web/app/coffee/Features/Compile/ClsiManager.coffee | 4 ++-- .../web/app/coffee/Features/Compile/CompileController.coffee | 2 +- services/web/app/coffee/router.coffee | 2 +- services/web/config/settings.defaults.coffee | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 8d5c5b2baa..b0f7a01fa1 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -7,8 +7,8 @@ ProjectGetter = require("../Project/ProjectGetter") ProjectEntityHandler = require("../Project/ProjectEntityHandler") logger = require "logger-sharelatex" Url = require("url") -ClsiCookieManager = require("./ClsiCookieManager")() -NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")("newBackendcloud") +ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName) +NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi_new?.backendGroupName) ClsiStateManager = require("./ClsiStateManager") _ = require("underscore") async = require("async") diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index d79eb5d389..8a3f92ac66 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -9,7 +9,7 @@ Settings = require "settings-sharelatex" AuthenticationController = require "../Authentication/AuthenticationController" UserGetter = require "../User/UserGetter" RateLimiter = require("../../infrastructure/RateLimiter") -ClsiCookieManager = require("./ClsiCookieManager")() +ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName) Path = require("path") module.exports = CompileController = diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index f48882a634..b590e58f5d 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -22,7 +22,7 @@ UserPagesController = require('./Features/User/UserPagesController') DocumentController = require('./Features/Documents/DocumentController') CompileManager = require("./Features/Compile/CompileManager") CompileController = require("./Features/Compile/CompileController") -ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")() +ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")(Settings.apis.clsi?.backendGroupName) HealthCheckController = require("./Features/HealthCheck/HealthCheckController") ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController" FileStoreController = require("./Features/FileStore/FileStoreController") diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 1306ce91c2..802183357c 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -135,6 +135,7 @@ module.exports = settings = url: "http://#{process.env['FILESTORE_HOST'] or 'localhost'}:3009" clsi: url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013" + backendGroupName: undefined templates: url: "http://#{process.env['TEMPLATES_HOST'] or 'localhost'}:3007" githubSync: