diff --git a/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee b/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee new file mode 100644 index 0000000000..1fc7ee4d12 --- /dev/null +++ b/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee @@ -0,0 +1,29 @@ +BetaProgramHandler = require './BetaProgramHandler' +UserLocator = require "../User/UserLocator" +Settings = require "settings-sharelatex" +logger = require 'logger-sharelatex' + + +module.exports = BetaProgramController = + + optIn: (req, res, next) -> + user_id = req?.session?.user?._id + logger.log {user_id}, "user opting in to beta program" + if !user_id + return next(new Error("no user id in session")) + BetaProgramHandler.optIn user_id, (err) -> + if err + return next(err) + return res.redirect "/beta/opt-in" + + optInPage: (req, res, next)-> + user_id = req.session?.user?._id + logger.log {user_id}, "showing beta opt-in page for user" + UserLocator.findById user_id, (err, user)-> + if err + logger.err {err, user_id}, "error fetching user" + return next(err) + res.render 'beta_program/opt_in', + title:'sharelatex_beta_program' + user: user, + languages: Settings.languages, diff --git a/services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee b/services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee new file mode 100644 index 0000000000..2851ff3fe8 --- /dev/null +++ b/services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee @@ -0,0 +1,18 @@ +User = require("../../models/User").User +logger = require 'logger-sharelatex' +metrics = require("../../infrastructure/Metrics") + +module.exports = BetaProgramHandler = + + optIn: (user_id, callback=(err)->) -> + User.findById user_id, (err, user) -> + if err + logger.err {err, user_id}, "problem adding user to beta" + return callback(err) + metrics.inc "beta-program.opt-in" + user.betaProgram = true + user.save (err) -> + if err + logger.err {err, user_id}, "problem adding user to beta" + return callback(err) + return callback(null) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 80c52c5a24..38f501cafa 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -38,6 +38,7 @@ InactiveProjectController = require("./Features/InactiveData/InactiveProjectCont ContactRouter = require("./Features/Contacts/ContactRouter") ReferencesController = require('./Features/References/ReferencesController') AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear') +BetaProgramController = require('./Features/BetaProgram/BetaProgramController') logger = require("logger-sharelatex") _ = require("underscore") @@ -197,6 +198,9 @@ module.exports = class Router webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll + webRouter.get "/beta/opt-in", AuthenticationController.requireLogin(), BetaProgramController.optInPage + webRouter.post "/beta/opt-in", AuthenticationController.requireLogin(), BetaProgramController.optIn + #Admin Stuff webRouter.get '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index webRouter.get '/admin/user', AuthorizationMiddlewear.ensureUserIsSiteAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon diff --git a/services/web/app/views/beta_program/opt_in.jade b/services/web/app/views/beta_program/opt_in.jade new file mode 100644 index 0000000000..5d1f7705f8 --- /dev/null +++ b/services/web/app/views/beta_program/opt_in.jade @@ -0,0 +1,40 @@ +extends ../layout + +block content + .content.content-alt + .container.beta-opt-in-wrapper + .row + .col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 + .card + .page-header.text-centered + h1 + | #{translate("sharelatex_beta_program")} + .beta-opt-in + .container-fluid + .row + .col-md-12 + p.text-centered #{translate("beta_program_benefits")} + p.text-centered + | #{translate("beta_program_badge_description")} + span.beta-feature-badge + p.text-centered + | #{translate("beta_program_current_beta_features_description")} + ul.list-unstyled.text-center + li + i.fa.fa-fw.fa-book + |  #{translate("mendeley_integration")} + .row.text-centered + .col-md-12 + if user.betaProgram + p #{translate("beta_program_already_participating")} + .form-group + a(href="/project").btn.btn-info #{translate("back_to_your_projects")} + else + form(method="post", action="/beta/opt-in", novalidate) + .form-group + input(type="hidden", name="_csrf", value=csrfToken) + button.btn.btn-primary( + type="submit" + ) + span #{translate("beta_program_opt_in_action")} + diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.jade index 52ea62d505..05fa19542f 100644 --- a/services/web/app/views/project/editor/left-menu.jade +++ b/services/web/app/views/project/editor/left-menu.jade @@ -15,7 +15,7 @@ aside#left-menu.full-size( | #{translate("source")} li a( - ng-href="{{pdf.url}}" + ng-href="{{pdf.downloadUrl || pdf.url}}" target="_blank" ng-if="pdf.url" ) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index f77753213a..d71c918561 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -46,7 +46,7 @@ div.full-size.pdf(ng-controller="PdfController") ) {{ pdf.logEntries.errors.length + pdf.logEntries.warnings.length }} a( - ng-href="{{pdf.url}}" + ng-href="{{pdf.downloadUrl || pdf.url}}" target="_blank" ng-if="pdf.url" tooltip="#{translate('download_pdf')}" diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index c6b4faf9ab..2ad6691b8b 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -104,6 +104,7 @@ define [ qs_args = ("#{k}=#{v}" for k, v of qs) $scope.pdf.qs = if qs_args.length then "?" + qs_args.join("&") else "" $scope.pdf.url += $scope.pdf.qs + $scope.pdf.downloadUrl = "/Project/#{$scope.project_id}/output/output.pdf" + $scope.pdf.qs fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) diff --git a/services/web/public/stylesheets/app/beta-program.less b/services/web/public/stylesheets/app/beta-program.less new file mode 100644 index 0000000000..f2141d711d --- /dev/null +++ b/services/web/public/stylesheets/app/beta-program.less @@ -0,0 +1,21 @@ +.beta-opt-in-wrapper { + min-height: 400px; +} + +.beta-opt-in { + .form-group { + margin-top: 15px; + } +} + +.beta-feature-badge { + &:extend(.label); + &:extend(.label-warning); + vertical-align: 11%; + padding-bottom: 4px; + padding-top: 2px; + margin-left: 12px; + &:before { + content: "β"; + } +} \ No newline at end of file diff --git a/services/web/public/stylesheets/style.less b/services/web/public/stylesheets/style.less index 4e9823631c..b516adff3e 100755 --- a/services/web/public/stylesheets/style.less +++ b/services/web/public/stylesheets/style.less @@ -58,6 +58,7 @@ // ShareLaTeX app classes @import "app/base.less"; @import "app/account-settings.less"; +@import "app/beta-program.less"; @import "app/about-page.less"; @import "app/project-list.less"; @import "app/editor.less"; diff --git a/services/web/test/UnitTests/coffee/BetaProgram/BetaProgramControllerTests.coffee b/services/web/test/UnitTests/coffee/BetaProgram/BetaProgramControllerTests.coffee new file mode 100644 index 0000000000..86a6ad3801 --- /dev/null +++ b/services/web/test/UnitTests/coffee/BetaProgram/BetaProgramControllerTests.coffee @@ -0,0 +1,102 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +sinon = require('sinon') +modulePath = path.join __dirname, "../../../../app/js/Features/BetaProgram/BetaProgramController" +expect = require("chai").expect + +describe "BetaProgramController", -> + + beforeEach -> + @user = + _id: @user_id = "a_simple_id" + email: "user@example.com" + features: {} + betaProgram: false + @req = + query: {} + session: + user: @user + @BetaProgramController = SandboxedModule.require modulePath, requires: + "./BetaProgramHandler": @BetaProgramHandler = { + optIn: sinon.stub() + }, + "../User/UserLocator": @UserLocator = { + findById: sinon.stub() + }, + "settings-sharelatex": @settings = { + languages: {} + } + "logger-sharelatex": @logger = { + log: sinon.stub() + err: sinon.stub() + error: sinon.stub() + } + @res = + send: sinon.stub() + redirect: sinon.stub() + render: sinon.stub() + @next = sinon.stub() + + describe "optIn", -> + + beforeEach -> + @BetaProgramHandler.optIn.callsArgWith(1, null) + + it "should redirect to '/beta/opt-in'", () -> + @BetaProgramController.optIn @req, @res, @next + @res.redirect.callCount.should.equal 1 + @res.redirect.firstCall.args[0].should.equal "/beta/opt-in" + + it "should not call next with an error", () -> + @BetaProgramController.optIn @req, @res, @next + @next.callCount.should.equal 0 + + it "should not call next with an error", () -> + @BetaProgramController.optIn @req, @res, @next + @next.callCount.should.equal 0 + + it "should call BetaProgramHandler.optIn", () -> + @BetaProgramController.optIn @req, @res, @next + @BetaProgramHandler.optIn.callCount.should.equal 1 + + describe "when BetaProgramHandler.opIn produces an error", -> + + beforeEach -> + @BetaProgramHandler.optIn.callsArgWith(1, new Error('woops')) + + it "should not redirect to '/'", () -> + @BetaProgramController.optIn @req, @res, @next + @res.redirect.callCount.should.equal 0 + + it "should produce an error", () -> + @BetaProgramController.optIn @req, @res, @next + @next.callCount.should.equal 1 + @next.firstCall.args[0].should.be.instanceof Error + + describe "optInPage", -> + + beforeEach -> + @UserLocator.findById.callsArgWith(1, null, @user) + + it "should render the opt-in page", () -> + @BetaProgramController.optInPage @req, @res, @next + @res.render.callCount.should.equal 1 + args = @res.render.firstCall.args + args[0].should.equal 'beta_program/opt_in' + + + describe "when UserLocator.findById produces an error", -> + + beforeEach -> + @UserLocator.findById.callsArgWith(1, new Error('woops')) + + it "should not render the opt-in page", () -> + @BetaProgramController.optInPage @req, @res, @next + @res.render.callCount.should.equal 0 + + it "should produce an error", () -> + @BetaProgramController.optInPage @req, @res, @next + @next.callCount.should.equal 1 + @next.firstCall.args[0].should.be.instanceof Error diff --git a/services/web/test/UnitTests/coffee/BetaProgram/BetaProgramHandlerTests.coffee b/services/web/test/UnitTests/coffee/BetaProgram/BetaProgramHandlerTests.coffee new file mode 100644 index 0000000000..e8b74a9f06 --- /dev/null +++ b/services/web/test/UnitTests/coffee/BetaProgram/BetaProgramHandlerTests.coffee @@ -0,0 +1,65 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +modulePath = path.join __dirname, '../../../../app/js/Features/BetaProgram/BetaProgramHandler' +sinon = require("sinon") +expect = require("chai").expect + + +describe 'BetaProgramHandler', -> + + beforeEach -> + @user_id = "some_id" + @user = + _id: @user_id + email: 'user@example.com' + features: {} + betaProgram: false + save: sinon.stub().callsArgWith(0, null) + @handler = SandboxedModule.require modulePath, requires: + "../../models/User": { + User: + findById: sinon.stub().callsArgWith(1, null, @user) + }, + "logger-sharelatex": @logger = { + log: sinon.stub() + err: sinon.stub() + }, + "../../infrastructure/Metrics": @logger = { + inc: sinon.stub() + } + + + describe "optIn", -> + + beforeEach -> + @call = (callback) => + @handler.optIn @user_id, callback + + it "should set betaProgram = true on user object", (done) -> + @call (err) => + @user.betaProgram.should.equal true + done() + + it "should call user.save", (done) -> + @call (err) => + @user.save.callCount.should.equal 1 + done() + + it "should not produce an error", (done) -> + @call (err) => + expect(err).to.equal null + expect(err).to.not.be.instanceof Error + done() + + describe "when user.save produces an error", -> + + beforeEach -> + @user.save.callsArgWith(0, new Error('woops')) + + it "should produce an error", (done) -> + @call (err) => + expect(err).to.not.equal null + expect(err).to.be.instanceof Error + done()