Skip to content

Commit

Permalink
Merge pull request #2064 from RachelDau/issue/2009-notification-added
Browse files Browse the repository at this point in the history
Issue/2009 Notifications Added
  • Loading branch information
FrenjaminBanklin authored May 9, 2024
2 parents 7455481 + ad39599 commit 281573a
Show file tree
Hide file tree
Showing 31 changed files with 2,052 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@ const userFunctions = ['setCurrentUser', 'getCurrentUser', 'requireCurrentUser']

jest.mock('test_node')
jest.mock('../server/models/user')
jest.mock('../server/viewer/viewer_notification_state')

const viewerNotificationState = require('../server/viewer/viewer_notification_state')

jest.mock('../server/viewer/viewer_state', () => ({
get: jest.fn()
}))

const viewerState = require('../server/viewer/viewer_state')

describe('current user middleware', () => {
beforeAll(() => {})
afterAll(() => {})
beforeEach(() => {
jest.clearAllMocks()
mockArgs = (() => {
const res = {}
const req = { session: {} }
const res = {
cookie: jest.fn()
}
const req = {
session: {}
}
const mockJson = jest.fn().mockImplementation(() => {
return true
})
Expand Down Expand Up @@ -210,4 +224,76 @@ describe('current user middleware', () => {
})
return expect(req.saveSessionPromise()).rejects.toEqual('mock-error')
})
test('getNotifications sets notifications in cookies when notifications are available', async () => {
expect.assertions(6)

const { req, res } = mockArgs
const User = oboRequire('server/models/user')
const mockUser = new User({ id: 8, lastLogin: '2019-01-01' })
User.fetchById = jest.fn().mockResolvedValue(mockUser)
req.currentUserId = mockUser.id
req.currentUser = mockUser
req.currentUser.lastLogin = mockUser.lastLogin
jest.useFakeTimers('modern')
jest.setSystemTime(new Date(2024, 3, 1)) //mock the date so that runtime does not affect the date/time

const mockNotifications = [
{ id: 1, title: 'Notification 1', text: 'Message 1' },
{ id: 2, title: 'Notification 2', text: 'Message 2' }
]
//simulate what would be added to the cookie
const mockNotificationsToCookie = [
{ title: 'Notification 1', text: 'Message 1' },
{ title: 'Notification 2', text: 'Message 2' }
]

viewerState.get.mockResolvedValueOnce(req.currentUserId)
viewerNotificationState.getRecentNotifications.mockResolvedValueOnce(
mockNotifications.map(n => ({ id: n.id }))
)
viewerNotificationState.getNotifications.mockResolvedValueOnce(mockNotifications)

return req.getNotifications(req, res).then(() => {
const today = new Date()
expect(viewerState.get).toHaveBeenCalledWith(8)
expect(viewerNotificationState.getRecentNotifications).toHaveBeenCalled()
expect(viewerNotificationState.getNotifications).toHaveBeenCalledWith([1, 2])

expect(res.cookie).toHaveBeenCalledWith(
'notifications',
JSON.stringify(mockNotificationsToCookie)
)
expect(req.currentUser.lastLogin).toStrictEqual(today)
expect(viewerNotificationState.setLastLogin).toHaveBeenCalledWith(8, today)

jest.useRealTimers()
})
})
test('getNotifications returns empty when there are no notifications', async () => {
expect.assertions(6)
const { req, res } = mockArgs
const User = oboRequire('server/models/user')
const mockUser = new User({ id: 8, lastLogin: '2019-01-01' })
User.fetchById = jest.fn().mockResolvedValue(mockUser)
req.currentUserId = mockUser.id
req.currentUser = mockUser
req.currentUser.lastLogin = mockUser.lastLogin
jest.useFakeTimers('modern')
jest.setSystemTime(new Date(2024, 3, 1)) //mock the date so that runtime does not affect the date/time

viewerState.get.mockResolvedValueOnce(req.currentUserId)
viewerNotificationState.getRecentNotifications.mockResolvedValueOnce(null)

return req.getNotifications(req, res).then(() => {
const today = new Date()
expect(viewerState.get).toHaveBeenCalledWith(8)
expect(viewerNotificationState.getRecentNotifications).toHaveBeenCalled() //With(req.currentUser.lastLogin)
expect(viewerNotificationState.getNotifications).not.toHaveBeenCalled()
expect(res.cookie).not.toHaveBeenCalled()
expect(req.currentUser.lastLogin).toStrictEqual(today)
expect(viewerNotificationState.setLastLogin).toHaveBeenCalledWith(8, today)

jest.useRealTimers()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const db = require('../server/db')
const {
getNotifications,
getRecentNotifications,
setLastLogin
} = require('../server/viewer/viewer_notification_state')

jest.mock('../server/db')
describe('db', () => {
beforeEach(() => {
jest.resetAllMocks()
jest.resetModules()
})

test('returns notifications when passed ids', () => {
const fakeNotifications = [
{ title: 'Notification 1', text: 'This is notification 1' },
{ title: 'Notification 2', text: 'This is notification 2' }
]
db.manyOrNone.mockResolvedValue(fakeNotifications)

return getNotifications([1, 2]).then(result => {
expect(result).toEqual(fakeNotifications)
expect(db.manyOrNone).toHaveBeenCalledWith(expect.any(String), { ids: [1, 2] })
})
})

test('returns undefined when passed ids as 0', () => {
return expect(getNotifications(0)).toBeUndefined()
})

test('returns notifications created after a given date', () => {
const fakeNotifications = [{ id: 1 }, { id: 2 }]
db.manyOrNone.mockResolvedValue(fakeNotifications)

return getRecentNotifications('2022-01-01').then(result => {
expect(result).toEqual(fakeNotifications)
expect(db.manyOrNone).toHaveBeenCalledWith(expect.any(String), { date: '2022-01-01' })
})
})

test('should insert a new record if the user does not exist', () => {
db.none.mockResolvedValue()

const userId = 1
const today = '2023-09-13'

return setLastLogin(userId, today).then(() => {
expect(db.none).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO users'), {
userId,
today
})
})
})

test('should handle other errors from db.none', () => {
const errorMessage = 'Database error'
db.none.mockRejectedValue(new Error(errorMessage))

const userId = 1
const today = '2023-09-13'

return expect(setLastLogin(userId, today)).rejects.toThrow(errorMessage)
})
})
35 changes: 35 additions & 0 deletions packages/app/obojobo-express/server/express_current_user.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const User = oboRequire('server/models/user')
const GuestUser = oboRequire('server/models/guest_user')
const logger = oboRequire('server/logger')
const viewerNotificationState = oboRequire('server/viewer/viewer_notification_state')
const viewerState = oboRequire('server/viewer/viewer_state')

const setCurrentUser = (req, user) => {
if (!(user instanceof User)) throw new Error('Invalid User for Current user')
Expand Down Expand Up @@ -56,11 +58,44 @@ const saveSessionPromise = req => {
})
}

//retrieve notifications from the database and set them in the cookie
const getNotifications = async (req, res) => {
return Promise.all([viewerState.get(req.currentUserId)])
.then(() => viewerNotificationState.getRecentNotifications(req.currentUser.lastLogin))
.then(result => {
if (result) {
return result.map(notifications => notifications.id)
}
return [0]
})
.then(ids => {
if (ids.some(id => id !== 0)) {
return viewerNotificationState.getNotifications(ids.filter(id => id !== 0))
}
})
.then(result => {
if (result) {
const parsedNotifications = result.map(notifications => ({
title: notifications.title,
text: notifications.text
}))
res.cookie('notifications', JSON.stringify(parsedNotifications))
}
return 0
})
.then(() => {
const today = new Date()
req.currentUser.lastLogin = today
viewerNotificationState.setLastLogin(req.currentUser.id, today)
})
}

module.exports = (req, res, next) => {
req.setCurrentUser = setCurrentUser.bind(this, req)
req.getCurrentUser = getCurrentUser.bind(this, req)
req.requireCurrentUser = requireCurrentUser.bind(this, req)
req.resetCurrentUser = resetCurrentUser.bind(this, req)
req.saveSessionPromise = saveSessionPromise.bind(this, req)
req.getNotifications = getNotifications.bind(this, req, res)
next()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict'

var dbm
var type
var seed

/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function(options, seedLink) {
dbm = options.dbmigrate
type = dbm.dataType
seed = seedLink
}

exports.up = function(db) {
return db.addColumn('users', 'last_login', {
type: 'timestamp WITH TIME ZONE',
notNull: true,
defaultValue: new String('now()')
})
}

exports.down = function(db) {
return db.removeColumn('users', 'last_login')
}

exports._meta = {
version: 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict'

var dbm
var type
var seed

/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function(options, seedLink) {
dbm = options.dbmigrate
type = dbm.dataType
seed = seedLink
}

exports.up = function(db) {
return db.createTable('notifications', {
id: {
type: 'bigserial',
primaryKey: true,
notNull: true
},
created_at: {
type: 'timestamp WITH TIME ZONE',
notNull: true,
defaultValue: new String('now()')
},
text: { type: 'string', notNull: true },
title: { type: 'string', notNull: true }
})
}

exports.down = function(db) {
return db.dropTable('notifications')
}

exports._meta = {
version: 1
}
3 changes: 3 additions & 0 deletions packages/app/obojobo-express/server/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class User {
email = null,
username = null,
createdAt = Date.now(),
lastLogin = Date.now(),
roles = [],
perms = null
} = {}) {
Expand All @@ -27,6 +28,7 @@ class User {
this.email = email
this.username = username
this.createdAt = createdAt
this.lastLogin = lastLogin
this.roles = roles
this.perms = [
...new Set(
Expand All @@ -47,6 +49,7 @@ class User {
email: result.email,
username: result.username,
createdAt: result.created_at,
lastLogin: result.last_login,
roles: result.roles,
perms: result.perms
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const db = oboRequire('server/db')

function getNotifications(ids) {
if (ids !== 0) {
return db.manyOrNone(
`
SELECT title,text
FROM notifications
WHERE id IN ($[ids:csv])
ORDER BY id ASC
`,
{
ids
}
)
}
}

function getRecentNotifications(date) {
return db.manyOrNone(
`
SELECT id
FROM notifications
WHERE created_at >= $[date]
ORDER BY created_at ASC
`,
{
date
}
)
}

function setLastLogin(userId, today) {
return db.none(
`
INSERT INTO users (id, last_login)
VALUES ($[userId], $[today])
ON CONFLICT (id) DO UPDATE
SET last_login = EXCLUDED.last_login
`,
{
userId,
today
}
)
}

module.exports = {
getNotifications,
getRecentNotifications,
setLastLogin
}
2 changes: 2 additions & 0 deletions packages/app/obojobo-repository/client/css/_defaults.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ $color-reward: #ffe65d;
$color-reward-text: #947d00;
$color-obojobo-blue: #0d4fa7;
$color-preview: #af1b5c;
$color-notification: #af1b5c;
$color-notification-focus: #fbdae6;

$size-spacing-vertical-big: 40px;
$size-spacing-vertical-half: $size-spacing-vertical-big / 2;
Expand Down
6 changes: 5 additions & 1 deletion packages/app/obojobo-repository/server/routes/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ const renderDashboard = (req, res, options) => {
let moduleCount = 0
let pageTitle = 'Dashboard'

return getUserModuleCount(req.currentUser.id)
return req
.getNotifications(req, res)
.then(() => {
return getUserModuleCount(req.currentUser.id)
})
.then(count => {
moduleCount = count
return CollectionSummary.fetchByUserId(req.currentUser.id)
Expand Down
Loading

0 comments on commit 281573a

Please sign in to comment.