Merge branch 'master' into pr-history-labels-part-2

This commit is contained in:
Paulo Reis 2018-08-16 11:56:36 +01:00
commit 02701a996d
34 changed files with 650 additions and 91 deletions

View file

@ -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")

View file

@ -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 =

View file

@ -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?

View file

@ -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)

View file

@ -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)

View file

@ -1,37 +1,68 @@
async = require('async')
Request = require('request')
logger = require 'logger-sharelatex'
Settings = require 'settings-sharelatex'
crypto = require('crypto')
Mailchimp = require('mailchimp-api-v3')
if !Settings.mailchimp?.api_key?
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 =
subscribe: (user, callback = () ->)->
if !Settings.markdownmail?
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?
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)
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")
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
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
MONGO_ID:user._id
return opts

View file

@ -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 = [])->

View file

@ -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 = []

View file

@ -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"

View file

@ -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)

View file

@ -1,4 +1,3 @@
sanitize = require('sanitizer')
User = require("../../models/User").User
UserCreator = require("./UserCreator")
UserGetter = require("./UserGetter")
@ -54,7 +53,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"

View file

@ -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) ->) ->
@ -99,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) =>
@ -152,7 +159,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 +176,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"

View file

@ -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")

View file

@ -0,0 +1,122 @@
mixin linkAdvisors(linkText, linkClass, track)
//- To Do: verify path
- 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 : ''
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'}
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, 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)
//- To Do: verify path
a(href="/plans" class=linkClass ? linkClass : '')
| #{linkText ? linkText : 'plans and pricing'}
mixin linkPrintNewTab(linkText, linkClass, icon, 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 : ''
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
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
- 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'}
mixin linkUniversities(linkText, linkClass)
//- To Do: verify path
a(href="/universities" class=linkClass ? linkClass : '')
| #{linkText ? linkText : 'universities'}

View file

@ -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

View file

@ -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 <strong>No Journals and Services</strong> menu to submit directly to our partners yet
li <strong>No Rich Text (WYSIWYG)</strong> mode yet
li <strong>No linked files</strong> (to URLs or to files in other Overleaf projects) yet
li <strong>No Zotero and CiteULike</strong> integrations yet
li <strong>No labelled versions</strong> yet
li You may not be able to access all of your <strong>Labelled versions</strong> yet
li There are <strong>no Zotero and CiteULike</strong> integrations yet
li Some <strong>Journals and Services in the Submit menu</strong> don't support direct submissions yet
p.row-spaced-small
| If you currently use the <strong>Overleaf Git bridge</strong> with your v1 project, you can migrate your project to the Overleaf v2 GitHub integration.
|

View file

@ -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")}

View file

@ -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:
@ -277,10 +278,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
@ -472,3 +473,14 @@ module.exports = settings =
autoCompile:
everyone: 100
standard: 25
# currentImage: "texlive-full:2017.1"
# imageRoot: "<DOCKER REPOSITORY ROOT>" # 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'}
# ]

View file

@ -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",
@ -98,7 +99,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",

View file

@ -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)

View file

@ -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 () =>

View file

@ -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;
}
}
}
}

View file

@ -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;
}

View file

@ -0,0 +1,9 @@
// Colors
.icon-accent {
color: @accent-color-secondary;
}
// Sizes
.icon-lg {
font-size: @font-size-h1;
}

View file

@ -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;

View file

@ -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;
@ -288,6 +303,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;
@ -304,6 +325,7 @@
@gray-light: #a4a4a4;
@gray-lighter: #cfcfcf;
@gray-lightest: #f0f0f0;
@white: #ffffff;
@blue: #405ebf;
@blueDark: #040D2D;

View file

@ -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";
@import "app/cms-page.less";
@import "app/portals.less";

View file

@ -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"

View file

@ -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

View file

@ -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()

View file

@ -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 =

View file

@ -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", ->

View file

@ -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

View file

@ -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'))