Henry Oswald 10 лет назад
Родитель
Сommit
5c5888b5dc

+ 16 - 0
services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee

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

+ 19 - 0
services/web/app/coffee/Features/Notifications/NotificationsController.coffee

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

+ 50 - 0
services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee

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

+ 8 - 1
services/web/app/coffee/Features/Project/ProjectController.coffee

@@ -9,6 +9,7 @@ Project = require('../../models/Project').Project
 User = require('../../models/User').User
 TagsHandler = require("../Tags/TagsHandler")
 SubscriptionLocator = require("../Subscription/SubscriptionLocator")
+NotificationsHandler = require("../Notifications/NotificationsHandler")
 LimitationsManager = require("../Subscription/LimitationsManager")
 _ = require("underscore")
 Settings = require("settings-sharelatex")
@@ -127,6 +128,8 @@ module.exports = ProjectController =
 		async.parallel {
 			tags: (cb)->
 				TagsHandler.getAllTags user_id, cb
+			notifications: (cb)->
+				NotificationsHandler.getUserNotifications user_id, cb
 			projects: (cb)->
 				Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
 			hasSubscription: (cb)->
@@ -139,6 +142,9 @@ module.exports = ProjectController =
 					return next(err)
 				logger.log results:results, user_id:user_id, "rendering project list"
 				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]
 				user = results.user
 				ProjectController._injectProjectOwners projects, (error, projects) ->
@@ -149,6 +155,7 @@ module.exports = ProjectController =
 						priority_title: true
 						projects: projects
 						tags: tags
+						notifications: notifications
 						user: user
 						hasSubscription: results.hasSubscription[0]
 					}
@@ -317,4 +324,4 @@ do generateThemeList = () ->
 	for file in files
 		if file.slice(-2) == "js" and file.match(/^theme-/)
 			cleanName = file.slice(0,-3).slice(6)
-			THEME_LIST.push cleanName
+			THEME_LIST.push cleanName

+ 8 - 6
services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee

@@ -1,3 +1,4 @@
+NotificationsBuilder = require("../Notifications/NotificationsBuilder")
 async = require("async")
 _ = require("underscore")
 settings = require("settings-sharelatex")
@@ -7,17 +8,18 @@ _s = require("underscore.string")
 module.exports = SubscriptionDomainHandler =
 
 
-	getLicenceUserCanJoin: (user, callback)->
+	getLicenceUserCanJoin: (user)->
 		licence = SubscriptionDomainHandler._findDomainLicence(user.email)
-		if licence?
-			callback null, licence
-		else
-			callback()
+		return licence
 
 	attemptToJoinGroup: (user, callback)->
 		licence = SubscriptionDomainHandler._findDomainLicence(user.email)
 		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
 			callback "user not verified"
 

+ 5 - 1
services/web/app/coffee/router.coffee

@@ -16,6 +16,7 @@ ReferalController = require('./Features/Referal/ReferalController')
 ReferalMiddleware = require('./Features/Referal/ReferalMiddleware')
 AuthenticationController = require('./Features/Authentication/AuthenticationController')
 TagsController = require("./Features/Tags/TagsController")
+NotificationsController = require("./Features/Notifications/NotificationsController")
 CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter')
 UserInfoController = require('./Features/User/UserInfoController')
 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.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
 		apiRouter.get  '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
 
@@ -232,4 +236,4 @@ module.exports = class Router
 			logger.error err: req.body.error, meta: req.body.meta, "client side error"
 			res.sendStatus(204)
 
-		webRouter.get '*', ErrorController.notFound
+		webRouter.get '*', ErrorController.notFound

+ 4 - 1
services/web/app/views/project/list.jade

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

+ 15 - 0
services/web/app/views/project/list/notifications.jade

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

+ 2 - 0
services/web/config/settings.defaults.coffee

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

+ 1 - 0
services/web/public/coffee/main/project-list/index.coffee

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

+ 17 - 0
services/web/public/coffee/main/project-list/notifications-controller.coffee

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

+ 1 - 0
services/web/public/coffee/main/project-list/project-list.coffee

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

+ 7 - 0
services/web/public/stylesheets/app/project-list.less

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

+ 43 - 0
services/web/test/UnitTests/coffee/Notifications/NotificationsControllerTests.coffee

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

+ 72 - 0
services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee

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

+ 7 - 0
services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee

@@ -33,6 +33,8 @@ describe "ProjectController", ->
 			userHasSubscriptionOrIsGroupMember: sinon.stub()
 		@TagsHandler =
 			getAllTags: sinon.stub()
+		@NotificationsHandler =
+			getUserNotifications: sinon.stub()
 		@ProjectModel =
 			findAllUsersProjects: sinon.stub()
 			findPopulatedById: sinon.stub()
@@ -60,6 +62,7 @@ describe "ProjectController", ->
 			"../Subscription/SubscriptionLocator": @SubscriptionLocator
 			"../Subscription/LimitationsManager": @LimitationsManager
 			"../Tags/TagsHandler":@TagsHandler
+			"../Notifications/NotificationsHandler":@NotificationsHandler
 			'../../models/Project': Project:@ProjectModel
 			"../../models/User":User:@UserModel
 			"../../managers/SecurityManager":@SecurityManager
@@ -78,6 +81,8 @@ describe "ProjectController", ->
 				user: @user
 			body:
 				projectName: @projectName 
+			i18n:
+				translate:->
 		@res = 
 			locals:
 				jsPath:"js path here"
@@ -198,6 +203,7 @@ describe "ProjectController", ->
 
 		beforeEach ->
 			@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"}]
 			@collabertions = [{lastUpdated:5, _id:5, 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)
 			@TagsHandler.getAllTags.callsArgWith(1, null, @tags, {})
+			@NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {})
 			@ProjectModel.findAllUsersProjects.callsArgWith(2, null, @projects, @collabertions, @readOnly)
 
 		it "should render the project/list page", (done)->