1
0
Fork 0
mirror of https://github.com/overleaf/overleaf.git synced 2025-04-08 14:21:54 +00:00

Merge branch 'master' into ns-remove-wufoo-log-links

This commit is contained in:
Nate Stemen 2018-08-24 09:18:48 -04:00
commit c034b0654b
21 changed files with 290 additions and 63 deletions

View file

@ -1,6 +1,7 @@
AnalyticsManager = require "./AnalyticsManager"
Errors = require "../Errors/Errors"
AuthenticationController = require("../Authentication/AuthenticationController")
InstitutionsAPI = require("../Institutions/InstitutionsAPI")
GeoIpLookup = require '../../infrastructure/GeoIpLookup'
module.exports = AnalyticsController =
@ -23,6 +24,14 @@ module.exports = AnalyticsController =
AnalyticsManager.recordEvent user_id, req.params.event, req.body, (error) ->
respondWith(error, res, next)
licences: (req, res, next) ->
{resource_id, start_date, end_date, lag} = req.query
InstitutionsAPI.getInstitutionLicences resource_id, start_date, end_date, lag, (error, licences) ->
if error?
next(error)
else
res.send licences
respondWith = (error, res, next) ->
if error instanceof Errors.ServiceNotConfiguredError
# ignore, no-op

View file

@ -23,4 +23,4 @@ module.exports =
publicApiRouter.use '/analytics/uniExternalCollaboration',
AuthenticationController.httpAuth,
AnalyticsProxy.call('/uniExternalCollaboration')
AnalyticsProxy.call('/uniExternalCollaboration')

View file

@ -9,15 +9,22 @@ module.exports = InstitutionsAPI =
method: 'GET'
path: "/api/v2/institutions/#{institutionId.toString()}/affiliations"
defaultErrorMessage: "Couldn't get institution affiliations"
}, callback
}, (error, body) -> callback(error, body or [])
getInstitutionLicences: (institutionId, startDate, endDate, lag, callback = (error, body) ->) ->
makeAffiliationRequest {
method: 'GET'
path: "/api/v2/institutions/#{institutionId.toString()}/institution_licences"
body: {start_date: startDate, end_date: endDate, lag}
defaultErrorMessage: "Couldn't get institution licences"
}, callback
getUserAffiliations: (userId, callback = (error, body) ->) ->
makeAffiliationRequest {
method: 'GET'
path: "/api/v2/users/#{userId.toString()}/affiliations"
defaultErrorMessage: "Couldn't get user affiliations"
}, callback
}, (error, body) -> callback(error, body or [])
addAffiliation: (userId, email, affiliationOptions, callback) ->
@ -25,11 +32,11 @@ module.exports = InstitutionsAPI =
callback = affiliationOptions
affiliationOptions = {}
{ university, department, role } = affiliationOptions
{ university, department, role, confirmedAt } = affiliationOptions
makeAffiliationRequest {
method: 'POST'
path: "/api/v2/users/#{userId.toString()}/affiliations"
body: { email, university, department, role }
body: { email, university, department, role, confirmedAt }
defaultErrorMessage: "Couldn't create affiliation"
}, callback

View file

@ -166,7 +166,7 @@ module.exports = UserUpdater =
email = EmailHelper.parseEmail(email)
return callback(new Error('invalid email')) if !email?
logger.log {userId, email}, 'confirming user email'
addAffiliation userId, email, (error) =>
addAffiliation userId, email, {confirmedAt: confirmedAt}, (error) =>
if error?
logger.err error: error, 'problem adding affiliation while confirming email'
return callback(error)

View file

@ -0,0 +1,21 @@
mixin faq_search(headerText, headerClass)
- if(typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined")
if headerText
div(class=headerClass) #{headerText}
.wiki(ng-controller="SearchWikiController")
form.project-search.form-horizontal(role="form")
.form-group.has-feedback.has-feedback-left
.col-sm-12
input.form-control(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....")
i.fa.fa-search.form-control-feedback-left
i.fa.fa-times.form-control-feedback(
ng-click="clearSearchText()",
style="cursor: pointer;",
ng-show="searchQueryText.length > 0"
)
.row
.col-md-12(ng-cloak)
a(ng-href='{{hit.url}}',ng-repeat='hit in hits').search-result.card.card-thin
span(ng-bind-html='hit.name')
div.search-result-content(ng-show="hit.content != ''", ng-bind-html='hit.content')

View file

@ -0,0 +1,74 @@
mixin paginate(pages, page_path, max_btns)
//- @param pages.current_page the current page viewed
//- @param pages.total_pages previously calculated,
//- based on total entries and entries per page
//- @param page_path the relative path, minus a trailing slash and page param
//- @param max_btns max number of buttons on either side of the current page
//- button and excludes first, prev, next, last
if pages && pages.current_page && pages.total_pages
- var max_btns = max_btns || 4
- var prev_page = Math.max(parseInt(pages.current_page, 10) - max_btns, 1)
- var next_page = parseInt(pages.current_page, 10) + 1
- var next_index = 0;
- var full_page_path = page_path + "/page/"
nav(role="navigation" aria-label="Pagination Navigation")
ul.pagination
if pages.current_page > 1
li
a(
aria-label="Go to first page"
href=page_path
) « First
li
a(
aria-label="Go to previous page"
href=full_page_path + (parseInt(pages.current_page, 10) - 1)
rel="prev"
) Prev
if pages.current_page - max_btns > 1
li
span …
while prev_page < pages.current_page
li
a(
aria-label="Go to page " + prev_page
href=full_page_path + prev_page
) #{prev_page}
- prev_page++
li(class="active")
span(
aria-label="Current Page, Page " + pages.current_page
aria-current="true"
) #{pages.current_page}
if pages.current_page < pages.total_pages
while next_page <= pages.total_pages && next_index < max_btns
li
a(
aria-label="Go to page " + next_page
href=full_page_path + next_page
) #{next_page}
- next_page++
- next_index++
if next_page <= pages.total_pages
li
span …
li
a(
aria-label="Go to next page"
href=full_page_path + (parseInt(pages.current_page, 10) + 1)
rel="next"
) Next
li
a(
aria-label="Go to last page"
href=full_page_path + pages.total_pages
) Last »

View file

@ -1,45 +1,4 @@
div#history(ng-show="ui.view == 'history'")
span
.upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'")
.message(ng-if="project.owner._id == user.id")
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
) #{translate("start_free_trial")}
.message(ng-show="project.owner._id != user.id")
p #{translate("ask_proj_owner_to_upgrade_for_history")}
p
a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
include ./history/entriesListV1
include ./history/entriesListV2

View file

@ -6,11 +6,13 @@ aside.change-list(
ng-if="!history.showOnlyLabels && !history.error"
entries="history.updates"
current-user="user"
current-user-is-owner="project.owner._id === user.id"
users="projectUsers"
load-entries="loadMore()"
load-disabled="history.loading || history.atEnd"
load-initialize="ui.view == 'history'"
is-loading="history.loading"
free-history-limit-hit="history.freeHistoryLimitHit"
on-entry-select="handleEntrySelect(selectedEntry)"
on-label-delete="handleLabelDelete(label)"
)
@ -134,6 +136,48 @@ script(type="text/ng-template", id="historyEntriesListTpl")
.loading(ng-show="$ctrl.isLoading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp; #{translate("loading")}...
.history-entries-list-upgrade-prompt(
ng-if="$ctrl.freeHistoryLimitHit && $ctrl.currentUserIsOwner"
ng-controller="FreeTrialModalController"
)
p #{translate("currently_seeing_only_24_hrs_history")}
p: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p.text-center
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
) #{translate("start_free_trial")}
p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
.history-entries-list-upgrade-prompt(
ng-if="$ctrl.freeHistoryLimitHit && !$ctrl.currentUserIsOwner"
)
p #{translate("currently_seeing_only_24_hrs_history")}
strong #{translate("ask_proj_owner_to_upgrade_for_full_history")}
script(type="text/ng-template", id="historyEntryTpl")
.history-entry(

View file

@ -6,7 +6,7 @@
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp; #{translate("loading")}...
span.history-toolbar-selected-version(
ng-show="!history.loadingFileTree && !history.showOnlyLabels && !history.error"
ng-show="!history.loadingFileTree && !history.showOnlyLabels && history.selection.updates.length && !history.error"
) #{translate("browsing_project_as_of")}&nbsp;
time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }}
span.history-toolbar-selected-version(
@ -19,13 +19,13 @@
button.history-toolbar-btn(
ng-click="showAddLabelDialog();"
ng-if="!history.showOnlyLabels"
ng-disabled="history.loadingFileTree"
ng-disabled="history.loadingFileTree || history.selection.updates.length == 0"
)
i.fa.fa-tag
| &nbsp;#{translate("history_label_this_version")}
button.history-toolbar-btn(
ng-click="toggleHistoryViewMode();"
ng-disabled="history.loadingFileTree"
ng-disabled="history.loadingFileTree || history.selection.updates.length == 0"
)
i.fa.fa-exchange
| &nbsp;#{translate("compare_to_another_version")}

View file

@ -243,7 +243,7 @@ div.full-size.pdf(ng-controller="PdfController")
dbl-click-callback="syncToCode"
)
iframe(
ng-src="{{ pdf.url }}"
ng-src="{{ pdf.url | trusted }}"
ng-if="settings.pdfViewer == 'native'"
)

View file

@ -82,6 +82,8 @@ define [
viewMode: null
nextBeforeTimestamp: null
atEnd: false
userHasFullFeature: @$scope.project?.features?.versioning or false
freeHistoryLimitHit: false
selection: {
label: null
updates: []
@ -232,9 +234,11 @@ define [
@$scope.history.labels = @_sortLabelsByVersionAndDate response.labels.data
@_loadUpdates(updatesData.updates)
@$scope.history.nextBeforeTimestamp = updatesData.nextBeforeTimestamp
if !updatesData.nextBeforeTimestamp?
if !updatesData.nextBeforeTimestamp? or @$scope.history.freeHistoryLimitHit
@$scope.history.atEnd = true
@$scope.history.loading = false
if @$scope.history.updates.length == 0
@$scope.history.loadingFileTree = false
.catch (error) =>
{ status, statusText } = error
@$scope.history.error = { status, statusText }
@ -387,23 +391,34 @@ define [
_loadUpdates: (updates = []) ->
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
for update in updates or []
dateTimeNow = new Date()
timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1)
cutOffIndex = null
for update, i in updates or []
for user in update.meta.users or []
if user?
user.hue = ColorManager.getHueForUserId(user.id)
if !previousUpdate? or !moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, "day")
update.meta.first_in_day = true
update.selectedFrom = false
update.selectedTo = false
update.inSelection = false
previousUpdate = update
if !@$scope.history.userHasFullFeature and update.meta.end_ts < timestamp24hoursAgo
cutOffIndex = i or 1 # Make sure that we show at least one entry (to allow labelling).
@$scope.history.freeHistoryLimitHit = true
break
firstLoad = @$scope.history.updates.length == 0
if !@$scope.history.userHasFullFeature and cutOffIndex?
updates = updates.slice 0, cutOffIndex
@$scope.history.updates =
@$scope.history.updates.concat(updates)

View file

@ -30,6 +30,8 @@ define [
loadInitialize: "<"
isLoading: "<"
currentUser: "<"
freeHistoryLimitHit: "<"
currentUserIsOwner: "<"
onEntrySelect: "&"
onLabelDelete: "&"
controller: historyEntriesListController

View file

@ -11,6 +11,10 @@ define [
# and then again on ack.
AUTO_COMPILE_DEBOUNCE = 2000
App.filter('trusted', ['$sce', ($sce)->
return (url)-> return $sce.trustAsResourceUrl(url);
])
App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) ->
# enable per-user containers by default
perUserCompile = true
@ -212,7 +216,7 @@ define [
buildPdfDownloadUrl = (pdfDownloadDomain, path)->
#we only download builds from compiles server for security reasons
if pdfDownloadDomain? and path.indexOf("build") != -1
if pdfDownloadDomain? and path? and path.indexOf("build") != -1
return "#{pdfDownloadDomain}#{path}"
else
return path

View file

@ -19,6 +19,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: (@line-height-computed / 2);
}
.history-toolbar-time,
.history-toolbar-selected-label {
@ -33,7 +34,7 @@
.btn-xs;
padding-left: @padding-small-horizontal;
padding-right: @padding-small-horizontal;
margin-left: (@line-height-computed / 2);
margin-right: (@line-height-computed / 2);
}
.history-toolbar-entries-list {
flex: 0 0 @changesListWidth;
@ -173,6 +174,12 @@
}
}
.history-entries-list-upgrade-prompt {
background-color: #FFF;
margin-bottom: 2px;
padding: 5px 10px;
}
.history-labels-list {
.history-entries;
overflow-y: auto;

View file

@ -277,6 +277,21 @@
@chat-new-message-textarea-bg : @ol-blue-gray-1;
@chat-new-message-textarea-color : @ol-blue-gray-6;
// Pagination
@pagination-active-bg : @ol-dark-green;
@pagination-active-border : @gray-lighter;
@pagination-active-color : #FFF;
@pagination-bg : #FFF;
@pagination-border : @gray-lighter;
@pagination-color : @ol-dark-green;
@pagination-disabled-color : @gray-dark;
@pagination-disabled-bg : @gray-lightest;
@pagination-disabled-border : @gray-lighter;
@pagination-hover-color : @ol-dark-green;
@pagination-hover-bg : @gray-lightest;
@pagination-hover-border : @gray-lighter;
// PDF
@pdf-top-offset : @toolbar-small-height;
@pdf-bg : @ol-blue-gray-1;

View file

@ -8,6 +8,7 @@
@import "_ol_style_includes.less";
@import "components/embed-responsive.less";
@import "components/icons.less";
@import "components/pagination.less";
// Pages
@import "app/about.less";

View file

@ -17,9 +17,13 @@ describe 'AnalyticsController', ->
updateEditingSession: sinon.stub().callsArgWith(3)
recordEvent: sinon.stub().callsArgWith(3)
@InstitutionsAPI =
getInstitutionLicences: sinon.stub().callsArgWith(4)
@controller = SandboxedModule.require modulePath, requires:
"./AnalyticsManager":@AnalyticsManager
"../Authentication/AuthenticationController":@AuthenticationController
"../Institutions/InstitutionsAPI":@InstitutionsAPI
"logger-sharelatex":
log:->
'../../infrastructure/GeoIpLookup': @GeoIpLookup =
@ -66,3 +70,19 @@ describe 'AnalyticsController', ->
@controller.recordEvent @req, @res
@AnalyticsManager.recordEvent.calledWith(@req.sessionID, @req.params["event"], @req.body).should.equal true
done()
describe "licences", ->
beforeEach ->
@req =
query:
resource_id:1
start_date:'1514764800'
end_date:'1530662400'
resource_type:'institution'
sessionID: "sessionIDHere"
session: {}
it "should trigger institutions api to fetch licences graph data", (done)->
@controller.licences @req, @res
@InstitutionsAPI.getInstitutionLicences.calledWith(@req.query["resource_id"], @req.query["start_date"], @req.query["end_date"], @req.query["lag"]).should.equal true
done()

View file

@ -11,12 +11,12 @@ describe "InstitutionsAPI", ->
beforeEach ->
@logger = err: sinon.stub(), log: ->
settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } }
@settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } }
@request = sinon.stub()
@InstitutionsAPI = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
'settings-sharelatex': settings
'settings-sharelatex': @settings
'request': @request
@stubbedUser =
@ -40,6 +40,34 @@ describe "InstitutionsAPI", ->
should.not.exist(requestOptions.body)
body.should.equal responseBody
done()
it 'handle empty response', (done)->
@settings.apis = null
@InstitutionsAPI.getInstitutionAffiliations @institutionId, (err, body) =>
should.not.exist(err)
expect(body).to.be.a 'Array'
body.length.should.equal 0
done()
describe 'getInstitutionLicences', ->
it 'get licences', (done)->
@institutionId = 123
responseBody = {"lag":"monthly","data":[{"key":"users","values":[{"x":"2018-01-01","y":1}]}]}
@request.yields(null, { statusCode: 200 }, responseBody)
startDate = '1417392000'
endDate = '1420848000'
@InstitutionsAPI.getInstitutionLicences @institutionId, startDate, endDate, 'monthly', (err, body) =>
should.not.exist(err)
@request.calledOnce.should.equal true
requestOptions = @request.lastCall.args[0]
expectedUrl = "v1.url/api/v2/institutions/#{@institutionId}/institution_licences"
requestOptions.url.should.equal expectedUrl
requestOptions.method.should.equal 'GET'
requestOptions.body['start_date'].should.equal startDate
requestOptions.body['end_date'].should.equal endDate
requestOptions.body.lag.should.equal 'monthly'
body.should.equal responseBody
done()
describe 'getUserAffiliations', ->
it 'get affiliations', (done)->
@ -65,6 +93,14 @@ describe "InstitutionsAPI", ->
err.message.should.have.string body.errors
done()
it 'handle empty response', (done)->
@settings.apis = null
@InstitutionsAPI.getUserAffiliations @stubbedUser._id, (err, body) =>
should.not.exist(err)
expect(body).to.be.a 'Array'
body.length.should.equal 0
done()
describe 'addAffiliation', ->
beforeEach ->
@request.callsArgWith(1, null, { statusCode: 201 })
@ -74,6 +110,7 @@ describe "InstitutionsAPI", ->
university: { id: 1 }
role: 'Prof'
department: 'Math'
confirmedAt: new Date()
@InstitutionsAPI.addAffiliation @stubbedUser._id, @newEmail, affiliationOptions, (err)=>
should.not.exist(err)
@request.calledOnce.should.equal true
@ -83,11 +120,12 @@ describe "InstitutionsAPI", ->
requestOptions.method.should.equal 'POST'
body = requestOptions.body
Object.keys(body).length.should.equal 4
Object.keys(body).length.should.equal 5
body.email.should.equal @newEmail
body.university.should.equal affiliationOptions.university
body.department.should.equal affiliationOptions.department
body.role.should.equal affiliationOptions.role
body.confirmedAt.should.equal affiliationOptions.confirmedAt
done()
it 'handle error', (done)->

View file

@ -241,7 +241,7 @@ describe "UserUpdater", ->
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
should.not.exist(err)
@addAffiliation.calledOnce.should.equal true
sinon.assert.calledWith(@addAffiliation, @stubbedUser._id, @newEmail)
sinon.assert.calledWith(@addAffiliation, @stubbedUser._id, @newEmail, { confirmedAt: new Date() } )
done()
it 'handle error', (done)->
@ -264,7 +264,7 @@ describe "UserUpdater", ->
done()
it 'handle affiliation error', (done)->
@addAffiliation.callsArgWith(2, new Error('nope'))
@addAffiliation.callsArgWith(3, new Error('nope'))
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
@UserUpdater.updateUser.called.should.equal false

View file

@ -4,16 +4,21 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) ->
@scope =
$watch: sinon.stub()
$on: sinon.stub()
project:
features:
versioning: true
@ide = {}
@historyManager = new HistoryV2Manager(@ide, @scope)
it "should setup the history scope on intialization", ->
it "should setup the history scope on initialization", ->
expect(@scope.history).to.deep.equal({
isV2: true
updates: []
viewMode: null
nextBeforeTimestamp: null
atEnd: false
userHasFullFeature: true
freeHistoryLimitHit: false
selection: {
label: null
updates: []
@ -32,6 +37,12 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) ->
selectedFile: null
})
it "should setup history without full access to the feature if the project does not have versioning", ->
@scope.project.features.versioning = false
@historyManager = new HistoryV2Manager(@ide, @scope)
expect(@scope.history.userHasFullFeature).to.equal false
describe "_perDocSummaryOfUpdates", ->
it "should return the range of updates for the docs", ->
result = @historyManager._perDocSummaryOfUpdates([{