Merge branch 'pr/221'

This commit is contained in:
Henry Oswald 2016-02-11 11:44:45 +00:00
commit 5c5888b5dc
16 changed files with 275 additions and 9 deletions

View file

@ -0,0 +1,16 @@
NotificationsHandler = require("./NotificationsHandler")
module.exports =
groupPlan: (user, licence)->
key : "join-sub-#{licence.subscription_id}"
create: (callback = ->)->
messageOpts =
groupName: licence.name
subscription_id: licence.subscription_id
NotificationsHandler.createNotification user._id, key, "joinSubscriptionInvite", messageOpts, callback
read: (callback = ->)->
NotificationsHandler.markAsReadWithKey user._id, key, callback

View file

@ -0,0 +1,19 @@
NotificationsHandler = require("./NotificationsHandler")
logger = require("logger-sharelatex")
_ = require("underscore")
module.exports =
getAllUnreadNotifications: (req, res)->
NotificationsHandler.getUserNotifications req.session.user._id, (err, unreadNotifications)->
unreadNotifications = _.map unreadNotifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
res.send(unreadNotifications)
markNotificationAsRead: (req, res)->
user_id = req.session.user._id
notification_id = req.params.notification_id
NotificationsHandler.markAsRead user_id, notification_id, ->
res.send()
logger.log user_id:user_id, notification_id:notification_id, "mark notification as read"

View file

@ -0,0 +1,50 @@
settings = require("settings-sharelatex")
request = require("request")
logger = require("logger-sharelatex")
oneSecond = 1000
module.exports =
getUserNotifications: (user_id, callback)->
opts =
uri: "#{settings.apis.notifications.url}/user/#{user_id}"
json: true
timeout: 2000
request.get opts, (err, res, unreadNotifications)->
statusCode = if res? then res.statusCode else 500
if err? or statusCode != 200
e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
logger.err err:err, "something went wrong getting notifications"
callback(null, [])
else
if !unreadNotifications?
unreadNotifications = []
callback(null, unreadNotifications)
createNotification: (user_id, key, templateKey, messageOpts, callback)->
opts =
uri: "#{settings.apis.notifications.url}/user/#{user_id}"
timeout: 1000
json: {
key:key
messageOpts:messageOpts
templateKey:templateKey
}
request.post opts, callback
markAsReadWithKey: (user_id, key, callback)->
opts =
uri: "#{settings.apis.notifications.url}/user/#{user_id}"
timeout: 1000
json: {
key:key
}
request.del opts, callback
markAsRead: (user_id, notification_id, callback)->
opts =
uri: "#{settings.apis.notifications.url}/user/#{user_id}/notification/#{notification_id}"
timeout:oneSecond
logger.log user_id:user_id, notification_id:notification_id, "send mark notification to notifications api"
request.del opts, callback

View file

@ -9,6 +9,7 @@ Project = require('../../models/Project').Project
User = require('../../models/User').User User = require('../../models/User').User
TagsHandler = require("../Tags/TagsHandler") TagsHandler = require("../Tags/TagsHandler")
SubscriptionLocator = require("../Subscription/SubscriptionLocator") SubscriptionLocator = require("../Subscription/SubscriptionLocator")
NotificationsHandler = require("../Notifications/NotificationsHandler")
LimitationsManager = require("../Subscription/LimitationsManager") LimitationsManager = require("../Subscription/LimitationsManager")
_ = require("underscore") _ = require("underscore")
Settings = require("settings-sharelatex") Settings = require("settings-sharelatex")
@ -127,6 +128,8 @@ module.exports = ProjectController =
async.parallel { async.parallel {
tags: (cb)-> tags: (cb)->
TagsHandler.getAllTags user_id, cb TagsHandler.getAllTags user_id, cb
notifications: (cb)->
NotificationsHandler.getUserNotifications user_id, cb
projects: (cb)-> projects: (cb)->
Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
hasSubscription: (cb)-> hasSubscription: (cb)->
@ -139,6 +142,9 @@ module.exports = ProjectController =
return next(err) return next(err)
logger.log results:results, user_id:user_id, "rendering project list" logger.log results:results, user_id:user_id, "rendering project list"
tags = results.tags[0] tags = results.tags[0]
notifications = require("underscore").map results.notifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2] projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
user = results.user user = results.user
ProjectController._injectProjectOwners projects, (error, projects) -> ProjectController._injectProjectOwners projects, (error, projects) ->
@ -149,6 +155,7 @@ module.exports = ProjectController =
priority_title: true priority_title: true
projects: projects projects: projects
tags: tags tags: tags
notifications: notifications
user: user user: user
hasSubscription: results.hasSubscription[0] hasSubscription: results.hasSubscription[0]
} }

View file

@ -1,3 +1,4 @@
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
async = require("async") async = require("async")
_ = require("underscore") _ = require("underscore")
settings = require("settings-sharelatex") settings = require("settings-sharelatex")
@ -7,17 +8,18 @@ _s = require("underscore.string")
module.exports = SubscriptionDomainHandler = module.exports = SubscriptionDomainHandler =
getLicenceUserCanJoin: (user, callback)-> getLicenceUserCanJoin: (user)->
licence = SubscriptionDomainHandler._findDomainLicence(user.email) licence = SubscriptionDomainHandler._findDomainLicence(user.email)
if licence? return licence
callback null, licence
else
callback()
attemptToJoinGroup: (user, callback)-> attemptToJoinGroup: (user, callback)->
licence = SubscriptionDomainHandler._findDomainLicence(user.email) licence = SubscriptionDomainHandler._findDomainLicence(user.email)
if licence? and user.emailVerified if licence? and user.emailVerified
SubscriptionGroupHandler.addUserToGroup licence.adminUser_id, user.email, callback SubscriptionGroupHandler.addUserToGroup licence.adminUser_id, user.email, (err)->
if err?
logger.err err:err, "error adding user to group"
return callback(err)
NotificationsBuilder.groupPlan(user, licence).read()
else else
callback "user not verified" callback "user not verified"

View file

@ -16,6 +16,7 @@ ReferalController = require('./Features/Referal/ReferalController')
ReferalMiddleware = require('./Features/Referal/ReferalMiddleware') ReferalMiddleware = require('./Features/Referal/ReferalMiddleware')
AuthenticationController = require('./Features/Authentication/AuthenticationController') AuthenticationController = require('./Features/Authentication/AuthenticationController')
TagsController = require("./Features/Tags/TagsController") TagsController = require("./Features/Tags/TagsController")
NotificationsController = require("./Features/Notifications/NotificationsController")
CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter') CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter')
UserInfoController = require('./Features/User/UserInfoController') UserInfoController = require('./Features/User/UserInfoController')
UserController = require("./Features/User/UserController") UserController = require("./Features/User/UserController")
@ -138,6 +139,9 @@ module.exports = class Router
webRouter.post '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.addProjectToTag webRouter.post '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.addProjectToTag
webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.removeProjectFromTag webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.removeProjectFromTag
webRouter.get '/notifications', AuthenticationController.requireLogin(), NotificationsController.getAllUnreadNotifications
webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
# Deprecated in favour of /internal/project/:project_id but still used by versioning # Deprecated in favour of /internal/project/:project_id but still used by versioning
apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails

View file

@ -9,7 +9,8 @@ block content
script(type="text/javascript"). script(type="text/javascript").
window.data = { window.data = {
projects: !{JSON.stringify(projects).replace(/\//g, '\\/')}, projects: !{JSON.stringify(projects).replace(/\//g, '\\/')},
tags: !{JSON.stringify(tags).replace(/\//g, '\\/')} tags: !{JSON.stringify(tags).replace(/\//g, '\\/')},
notifications: !{JSON.stringify(notifications).replace(/\//g, '\\/')}
}; };
window.algolia = { window.algolia = {
institutions: { institutions: {
@ -20,12 +21,14 @@ block content
.content.content-alt(ng-controller="ProjectPageController") .content.content-alt(ng-controller="ProjectPageController")
.container .container
.row(ng-cloak) .row(ng-cloak)
span(ng-show="first_sign_up == 'default' || projects.length > 0") span(ng-show="first_sign_up == 'default' || projects.length > 0")
aside.col-md-2.col-xs-3 aside.col-md-2.col-xs-3
include ./list/side-bar include ./list/side-bar
.col-md-10.col-xs-9 .col-md-10.col-xs-9
include ./list/notifications
include ./list/project-list include ./list/project-list
span(ng-if="first_sign_up == 'minimial' && projects.length == 0") span(ng-if="first_sign_up == 'minimial' && projects.length == 0")

View file

@ -0,0 +1,15 @@
span(ng-controller="NotificationsController").userNotifications
ul.list-unstyled.notifications-list(
ng-if="notifications.length > 0",
ng-cloak
)
li.notification_entry(
ng-repeat="unreadNotification in notifications",
)
.row(ng-hide="unreadNotification.hide")
.col-xs-12
.alert.alert-warning
span(ng-bind-html="unreadNotification.html")
button(ng-click="dismiss(unreadNotification)").close.pull-right
span(aria-hidden="true") ×
span.sr-only #{translate("close")}

View file

@ -108,6 +108,8 @@ module.exports =
url: "" url: ""
references: references:
url: "http://localhost:3040" url: "http://localhost:3040"
notifications:
url: "http://localhost:3042"
templates: templates:
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2" user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"

View file

@ -2,6 +2,7 @@ define [
"main/project-list/project-list" "main/project-list/project-list"
"main/project-list/modal-controllers" "main/project-list/modal-controllers"
"main/project-list/tag-controllers" "main/project-list/tag-controllers"
"main/project-list/notifications-controller"
"main/project-list/queued-http" "main/project-list/queued-http"
"main/project-list/left-hand-menu-promo-controller" "main/project-list/left-hand-menu-promo-controller"
], () -> ], () ->

View file

@ -0,0 +1,17 @@
define [
"base"
], (App) ->
App.controller "NotificationsController", ($scope, $http) ->
for notification in $scope.notifications
notification.hide = false
$scope.dismiss = (notification) ->
$http({
url: "/notifications/#{notification._id}"
method: "DELETE"
headers:
"X-Csrf-Token": window.csrfToken
})
.success (data) ->
notification.hide = true

View file

@ -5,6 +5,7 @@ define [
App.controller "ProjectPageController", ($scope, $modal, $q, $window, queuedHttp, event_tracking, $timeout, sixpack) -> App.controller "ProjectPageController", ($scope, $modal, $q, $window, queuedHttp, event_tracking, $timeout, sixpack) ->
$scope.projects = window.data.projects $scope.projects = window.data.projects
$scope.tags = window.data.tags $scope.tags = window.data.tags
$scope.notifications = window.data.notifications
$scope.allSelected = false $scope.allSelected = false
$scope.selectedProjects = [] $scope.selectedProjects = []
$scope.filter = "all" $scope.filter = "all"

View file

@ -28,6 +28,13 @@
} }
} }
.userNotifications {
ul {
margin-bottom:0px;
}
}
ul.folders-menu { ul.folders-menu {
margin: 0; margin: 0;
.subdued { .subdued {

View file

@ -0,0 +1,43 @@
SandboxedModule = require('sandboxed-module')
assert = require('assert')
require('chai').should()
sinon = require('sinon')
modulePath = require('path').join __dirname, '../../../../app/js/Features/Notifications/NotificationsController.js'
describe 'NotificationsController', ->
user_id = "123nd3ijdks"
notification_id = "123njdskj9jlk"
beforeEach ->
@handler =
getUserNotifications: sinon.stub().callsArgWith(1)
markAsRead: sinon.stub().callsArgWith(2)
@controller = SandboxedModule.require modulePath, requires:
"./NotificationsHandler":@handler
"underscore":@underscore =
map:(arr)-> return arr
'logger-sharelatex':
log:->
err:->
@req =
params:
notification_id:notification_id
session:
user:
_id:user_id
i18n:
translate:->
it 'should ask the handler for all unread notifications', (done)->
allNotifications = [{_id: notification_id, user_id: user_id}]
@handler.getUserNotifications = sinon.stub().callsArgWith(1, null, allNotifications)
@controller.getAllUnreadNotifications @req, send:(body)=>
body.should.equal allNotifications
@handler.getUserNotifications.calledWith(user_id).should.equal true
done()
it 'should send a delete request when a delete has been received to mark a notification', (done)->
@controller.markNotificationAsRead @req, send:=>
@handler.markAsRead.calledWith(user_id, notification_id).should.equal true
done()

View file

@ -0,0 +1,72 @@
SandboxedModule = require('sandboxed-module')
assert = require('chai').assert
require('chai').should()
sinon = require('sinon')
modulePath = require('path').join __dirname, '../../../../app/js/Features/Notifications/NotificationsHandler.js'
_ = require('underscore')
describe 'NotificationsHandler', ->
user_id = "123nd3ijdks"
notification_id = "123njdskj9jlk"
notificationUrl = "notification.sharelatex.testing"
beforeEach ->
@request =
post: sinon.stub().callsArgWith(1)
del: sinon.stub().callsArgWith(1)
get: sinon.stub()
@handler = SandboxedModule.require modulePath, requires:
"settings-sharelatex": apis:{notifications:{url:notificationUrl}}
"request":@request
'logger-sharelatex':
log:->
err:->
describe "getUserNotifications", ->
it 'should get unread notifications', (done)->
stubbedNotifications = [{_id: notification_id, user_id: user_id}]
@request.get.callsArgWith(1, null, {statusCode:200}, stubbedNotifications)
@handler.getUserNotifications user_id, (err, unreadNotifications)=>
stubbedNotifications.should.deep.equal unreadNotifications
getOpts =
uri: "#{notificationUrl}/user/#{user_id}"
json:true
timeout:2000
@request.get.calledWith(getOpts).should.equal true
done()
it 'should return empty arrays if there are no notifications', ->
@request.get.callsArgWith(1, null, {statusCode:200}, null)
@handler.getUserNotifications user_id, (err, unreadNotifications)=>
unreadNotifications.length.should.equal 0
describe "markAsRead", ->
beforeEach ->
@key = "some key here"
it 'should send a delete request when a delete has been received to mark a notification', (done)->
@handler.markAsReadWithKey user_id, @key, =>
opts =
uri: "#{notificationUrl}/user/#{user_id}"
json:
key:@key
timeout:1000
@request.del.calledWith(opts).should.equal true
done()
describe "createNotification", ->
beforeEach ->
@key = "some key here"
@messageOpts = {value:12344}
@templateKey = "renderThisHtml"
it "should post the message over", (done)->
@handler.createNotification user_id, @key, @templateKey, @messageOpts, =>
args = @request.post.args[0][0]
args.uri.should.equal "#{notificationUrl}/user/#{user_id}"
args.timeout.should.equal 1000
expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts}
assert.deepEqual(args.json, expectedJson)
done()

View file

@ -33,6 +33,8 @@ describe "ProjectController", ->
userHasSubscriptionOrIsGroupMember: sinon.stub() userHasSubscriptionOrIsGroupMember: sinon.stub()
@TagsHandler = @TagsHandler =
getAllTags: sinon.stub() getAllTags: sinon.stub()
@NotificationsHandler =
getUserNotifications: sinon.stub()
@ProjectModel = @ProjectModel =
findAllUsersProjects: sinon.stub() findAllUsersProjects: sinon.stub()
findPopulatedById: sinon.stub() findPopulatedById: sinon.stub()
@ -60,6 +62,7 @@ describe "ProjectController", ->
"../Subscription/SubscriptionLocator": @SubscriptionLocator "../Subscription/SubscriptionLocator": @SubscriptionLocator
"../Subscription/LimitationsManager": @LimitationsManager "../Subscription/LimitationsManager": @LimitationsManager
"../Tags/TagsHandler":@TagsHandler "../Tags/TagsHandler":@TagsHandler
"../Notifications/NotificationsHandler":@NotificationsHandler
'../../models/Project': Project:@ProjectModel '../../models/Project': Project:@ProjectModel
"../../models/User":User:@UserModel "../../models/User":User:@UserModel
"../../managers/SecurityManager":@SecurityManager "../../managers/SecurityManager":@SecurityManager
@ -78,6 +81,8 @@ describe "ProjectController", ->
user: @user user: @user
body: body:
projectName: @projectName projectName: @projectName
i18n:
translate:->
@res = @res =
locals: locals:
jsPath:"js path here" jsPath:"js path here"
@ -198,6 +203,7 @@ describe "ProjectController", ->
beforeEach -> beforeEach ->
@tags = [{name:1, project_ids:["1","2","3"]}, {name:2, project_ids:["a","1"]}, {name:3, project_ids:["a", "b", "c", "d"]}] @tags = [{name:1, project_ids:["1","2","3"]}, {name:2, project_ids:["a","1"]}, {name:3, project_ids:["a", "b", "c", "d"]}]
@notifications = [{_id:'1',user_id:'2',templateKey:'3',messageOpts:'4',key:'5'}]
@projects = [{lastUpdated:1, _id:1, owner_ref: "user-1"}, {lastUpdated:2, _id:2, owner_ref: "user-2"}] @projects = [{lastUpdated:1, _id:1, owner_ref: "user-1"}, {lastUpdated:2, _id:2, owner_ref: "user-2"}]
@collabertions = [{lastUpdated:5, _id:5, owner_ref: "user-1"}] @collabertions = [{lastUpdated:5, _id:5, owner_ref: "user-1"}]
@readOnly = [{lastUpdated:3, _id:3, owner_ref: "user-1"}] @readOnly = [{lastUpdated:3, _id:3, owner_ref: "user-1"}]
@ -213,6 +219,7 @@ describe "ProjectController", ->
@LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, false) @LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, false)
@TagsHandler.getAllTags.callsArgWith(1, null, @tags, {}) @TagsHandler.getAllTags.callsArgWith(1, null, @tags, {})
@NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {})
@ProjectModel.findAllUsersProjects.callsArgWith(2, null, @projects, @collabertions, @readOnly) @ProjectModel.findAllUsersProjects.callsArgWith(2, null, @projects, @collabertions, @readOnly)
it "should render the project/list page", (done)-> it "should render the project/list page", (done)->