Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue/2009 Notifications Added #2064

Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8ec51cc
migration files
RachelDau Feb 23, 2023
de8b055
display notification in nav
RachelDau Feb 23, 2023
9809933
notification css file
RachelDau Feb 23, 2023
83ee351
nav-util functions and viewer app update
RachelDau Feb 23, 2023
d74d2a1
nav code cleanup
RachelDau Feb 23, 2023
015f169
nav unit tests and snapshots
RachelDau Feb 23, 2023
16cb203
nav store and migration file update
RachelDau Mar 1, 2023
1fe112e
initial commit for new notifiction state viewer
RachelDau Mar 1, 2023
a40b891
initial commit for visits file
RachelDau Mar 1, 2023
12c9af3
viewer users state added
RachelDau Mar 2, 2023
5f60125
viewer notifications state updated
RachelDau Mar 2, 2023
1eee465
last login checked for logic
RachelDau Mar 2, 2023
a33a0b0
Merge branch 'dev/31-taconite' into issue/2009-notification-added
RachelDau May 16, 2023
7591c1a
adds notifications to dashboard
RachelDau May 16, 2023
73a7500
merge dev-32-pigeonite
RachelDau Sep 18, 2023
08f91c8
fix notification implementation and tests
RachelDau Dec 11, 2023
ac6f836
Merge branch 'dev/33-serpierite' into issue/2009-notification-added
RachelDau Dec 11, 2023
b954c5d
remove file
RachelDau Dec 11, 2023
86ff23e
fix dependencies
RachelDau Dec 11, 2023
e6e3e51
code clean-up
RachelDau Dec 11, 2023
b6d4df3
Nav Clean-up code
RachelDau Dec 11, 2023
1cd175a
Visits Code Clean-up
RachelDau Dec 11, 2023
acf12dd
Visits code clean-up
RachelDau Dec 11, 2023
a6b3660
Update visits.js
RachelDau Dec 11, 2023
b298b88
fix last notification fails to exit issue
RachelDau Dec 11, 2023
251bdc9
dashboard clean-up
RachelDau Dec 12, 2023
4b98337
CSS Style update for notifications
RachelDau Dec 12, 2023
0e3102c
notification indicator and drop down menu added
RachelDau Jan 22, 2024
af88209
Dashboard client and server code clean-up
RachelDau Jan 22, 2024
d6ca441
default.jsx code cleanup
RachelDau Jan 22, 2024
fce6ab2
imports updated for notification component
RachelDau Jan 23, 2024
2c76339
removed hidden Notifications updated logic
RachelDau Jan 30, 2024
ec3b5af
updated migration files
RachelDau Jan 30, 2024
846e6e4
notification style update and jest tests written
RachelDau Mar 6, 2024
d87cb8b
css style exit button outside scroll
RachelDau Mar 6, 2024
e4369ab
background removed exit button
RachelDau Mar 6, 2024
b96d0c1
tab order accessibility update
RachelDau Mar 13, 2024
674b75b
update no notifications screen
RachelDau Mar 25, 2024
d4830eb
updates scaling notifications to account for long strings
RachelDau Mar 27, 2024
5556eb5
fix for safari issue with notification indicator
RachelDau Mar 27, 2024
7c74f45
capitialized Index
RachelDau Apr 29, 2024
2add759
Modal added for notifications
RachelDau Apr 30, 2024
d758ecf
remove comments
RachelDau Apr 30, 2024
5dc37a8
migrations regenerated and unit tests updated
RachelDau May 6, 2024
6ca28de
unit tests updated for notifications
RachelDau May 7, 2024
c37cc24
notification unit tests for button click updated
RachelDau May 8, 2024
ad39599
express-current-user test updated date mock
RachelDau May 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,68 @@ 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

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

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)
})
})
})
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('last_login')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

db.removeColumn takes two arguments, the first being the table name.

This should read return db.removeColumn('users', 'last_login').

}

exports._meta = {
version: 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'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.
* yarn db:migrateup
*/
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) {
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
Loading