Skip to content

Commit cf69291

Browse files
committed
Progress on limiting stats to e.g. look at the last year
1 parent 760ad77 commit cf69291

File tree

9 files changed

+340
-11
lines changed

9 files changed

+340
-11
lines changed

__mocks__/db.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ module.exports = {
8787
setEndTime: jest.fn(async (instanceRef, timezone) => {}),
8888
setStartTime: jest.fn(async (instanceRef, timezone) => {}),
8989
setTimezone: jest.fn(async (instanceRef, timezone) => {}),
90+
setUserSetting: jest.fn(async (instanceRef, userRef, name, value) => {}),
9091
setWeekdays: jest.fn(async (instanceRef, weekdayMask) => {}),
9192
scheduled: jest.fn(async (instanceRef) => {}),
9293
storeScheduled: jest.fn(async (instanceRef, timestamp, messageId, channel) => {}),
@@ -96,5 +97,6 @@ module.exports = {
9697
}),
9798
async recentClickTimes () { return [] },
9899
async slowestClickTimes (instanceRef) { return [] },
100+
async userSettings (instanceRef, userRef) { return {} },
99101
async winningStreaks (instanceRef) { return [{ user: 'test1', streak: 3 }] }
100102
}

__tests__/db.test.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ describe('database', () => {
134134
authedUser: {
135135
id: 'U9'
136136
},
137+
userSettings: {
138+
U1: { statsInterval: 0 },
139+
U2: { statsInterval: 7 }
140+
},
137141
channel: 'C1',
138142
scheduled: {}
139143
}
@@ -214,6 +218,30 @@ describe('database', () => {
214218
{ user: 'U99991111', count: 1 }
215219
])
216220
})
221+
222+
test('is sorted and correct no interval stats set', async () => {
223+
const clicks = await db.clicksPerUser('T2', 'U0')
224+
expect(clicks).toEqual([
225+
{ user: 'U12341234', count: 2 },
226+
{ user: 'U99991111', count: 1 }
227+
])
228+
})
229+
230+
test('is sorted and correct last forever stats', async () => {
231+
const clicks = await db.clicksPerUser('T2', 'U1')
232+
expect(clicks).toEqual([
233+
{ user: 'U12341234', count: 2 },
234+
{ user: 'U99991111', count: 1 }
235+
])
236+
})
237+
238+
test('is sorted and correct last 7 days stats', async () => {
239+
const clicks = await db.clicksPerUser('T2', 'U2', DateTime.fromISO('2020-03-22T00:00:00.000Z').toUTC())
240+
expect(clicks).toEqual([
241+
{ user: 'U12341234', count: 1 },
242+
{ user: 'U99991111', count: 1 }
243+
])
244+
})
217245
})
218246

219247
describe('fastestClickTimes', () => {
@@ -1580,6 +1608,164 @@ describe('database', () => {
15801608
})
15811609
})
15821610

1611+
describe('userSettings', () => {
1612+
beforeEach(async () => {
1613+
const collection = await db._instanceCollection()
1614+
await collection.deleteMany({})
1615+
1616+
// For each of these tests, initialize the db with some contents.
1617+
const instanceCommon = {
1618+
accessToken: 'xoxop-134234234',
1619+
manualAnnounce: false,
1620+
weekdays: 0,
1621+
intervalStart: 32400,
1622+
intervalEnd: 57600,
1623+
timezone: 'Europe/Copenhagen',
1624+
scope: 'chat:write',
1625+
botUserId: 'U8',
1626+
appId: 'A1',
1627+
authedUser: {
1628+
id: 'U9'
1629+
},
1630+
channel: 'C1',
1631+
scheduled: {}
1632+
}
1633+
1634+
const instance1 = {
1635+
...instanceCommon,
1636+
team: {
1637+
id: 'T1',
1638+
name: 'Team1'
1639+
},
1640+
userSettings: {
1641+
U1234: {
1642+
statsInterval: 365
1643+
}
1644+
}
1645+
}
1646+
1647+
const instance2 = {
1648+
...instanceCommon,
1649+
team: {
1650+
id: 'T2',
1651+
name: 'Team2'
1652+
}
1653+
}
1654+
1655+
await collection.insertMany([instance1, instance2])
1656+
})
1657+
1658+
test('is empty when no user settings exists', async () => {
1659+
const clicks = await db.userSettings('T2', 'U1234')
1660+
expect(clicks).toEqual({})
1661+
})
1662+
1663+
test('is empty when user does not exists', async () => {
1664+
const clicks = await db.userSettings('T1', 'U0000')
1665+
expect(clicks).toEqual({})
1666+
})
1667+
1668+
test('is not empty when user exists', async () => {
1669+
const clicks = await db.userSettings('T1', 'U1234')
1670+
expect(clicks).toEqual({ statsInterval: 365 })
1671+
})
1672+
})
1673+
1674+
describe('setUserSetting', () => {
1675+
beforeEach(async () => {
1676+
const collection = await db._instanceCollection()
1677+
await collection.deleteMany({})
1678+
1679+
// For each of these tests, initialize the db with some contents.
1680+
const sharedProperties = {
1681+
accessToken: 'xoxop-134234234',
1682+
manualAnnounce: false,
1683+
weekdays: 0,
1684+
intervalStart: 32400,
1685+
intervalEnd: 57600,
1686+
timezone: 'Europe/Copenhagen',
1687+
scope: 'chat:write',
1688+
botUserId: 'U8',
1689+
appId: 'A1',
1690+
authedUser: {
1691+
id: 'U9'
1692+
},
1693+
buttons: []
1694+
}
1695+
1696+
await collection.insertMany([{
1697+
...sharedProperties,
1698+
team: {
1699+
id: 'T1',
1700+
name: 'Team1'
1701+
},
1702+
userSettings: {
1703+
U1: {},
1704+
U2: {
1705+
statsInterval: 1
1706+
}
1707+
},
1708+
channel: null,
1709+
scheduled: {}
1710+
}, {
1711+
...sharedProperties,
1712+
team: {
1713+
id: 'T2',
1714+
name: 'Team1'
1715+
},
1716+
// missing userSettings completely
1717+
channel: null,
1718+
scheduled: {}
1719+
}])
1720+
})
1721+
1722+
test('stores setting when userSettings does not exists', async () => {
1723+
await db.setUserSetting('T2', 'U1337', 'statsInterval', 365)
1724+
const collection = await db._instanceCollection()
1725+
const instance = await collection.findOne({
1726+
'team.id': 'T2'
1727+
})
1728+
1729+
expect(instance.userSettings).toEqual({
1730+
U1337: {
1731+
statsInterval: 365
1732+
}
1733+
})
1734+
})
1735+
1736+
test('stores setting when user does not exists', async () => {
1737+
await db.setUserSetting('T1', 'U3', 'statsInterval', 365)
1738+
const collection = await db._instanceCollection()
1739+
const instance = await collection.findOne({
1740+
'team.id': 'T1'
1741+
})
1742+
1743+
expect(instance.userSettings).toHaveProperty('U3', { statsInterval: 365 }) // New
1744+
expect(instance.userSettings).toHaveProperty('U2', { statsInterval: 1 }) // old
1745+
})
1746+
1747+
test('stores setting when user exists but does not have setting', async () => {
1748+
await db.setUserSetting('T1', 'U1', 'statsInterval', 365)
1749+
const collection = await db._instanceCollection()
1750+
const instance = await collection.findOne({
1751+
'team.id': 'T1'
1752+
})
1753+
1754+
expect(instance.userSettings).toHaveProperty('U1', { statsInterval: 365 }) // New
1755+
expect(instance.userSettings).toHaveProperty('U2', { statsInterval: 1 }) // old
1756+
})
1757+
1758+
test('stores setting when user exists and has setting', async () => {
1759+
await db.setUserSetting('T1', 'U2', 'statsInterval', 365)
1760+
const collection = await db._instanceCollection()
1761+
const instance = await collection.findOne({
1762+
'team.id': 'T1'
1763+
})
1764+
1765+
expect(instance.userSettings).toHaveProperty('U2', { statsInterval: 365 }) // New
1766+
})
1767+
})
1768+
15831769
describe('winningStreaks', () => {
15841770
beforeEach(async () => {
15851771
const collection = await db._instanceCollection()

__tests__/settings.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,30 @@ describe('weekday setting', () => {
206206
expect(db.setWeekdays.mock.calls[0][1]).toBe(0b0000000)
207207
expect(res.send).toHaveBeenCalledAfter(db.setWeekdays)
208208
})
209+
210+
test('can set stats interval correctly', async () => {
211+
const instanceRef = 'T1234'
212+
const userRef = 'U1'
213+
const action = { // the real object has the complete plain text and stuff for the whole view too.
214+
type: 'static_select',
215+
action_id: 'user_stats_interval',
216+
selected_option: {
217+
text: {
218+
type: 'plain_text',
219+
text: 'Forever',
220+
emoji: true
221+
},
222+
value: '0'
223+
}
224+
}
225+
const res = { send: jest.fn() }
226+
227+
await settings.setUserSetting(res, instanceRef, action, userRef, 'statsInterval')
228+
expect(db.setUserSetting).toHaveBeenCalledTimes(1)
229+
expect(db.setUserSetting.mock.calls[0][0]).toBe(instanceRef)
230+
expect(db.setUserSetting.mock.calls[0][1]).toBe(userRef)
231+
expect(db.setUserSetting.mock.calls[0][2]).toBe('statsInterval')
232+
expect(db.setUserSetting.mock.calls[0][3]).toBe(0)
233+
expect(res.send).toHaveBeenCalledAfter(db.setUserSetting)
234+
})
209235
})

__tests__/stats.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,14 @@ describe('stats blocks', () => {
2626
const stats = await statsBlocks('T1')
2727
expect(stats[1].fields[1].text).toMatch(/3 <@test1>/)
2828
})
29+
30+
test('does not includes stats interval if user id is not passed', async () => {
31+
const stats = await statsBlocks('T1')
32+
expect(stats).toHaveLength(3)
33+
})
34+
35+
test('includes default Forever stats interval if nothing else is set', async () => {
36+
const stats = await statsBlocks('T1', 'U1234')
37+
expect(stats).toHaveLength(4)
38+
})
2939
})

db.js

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ function connectMongo () {
3434
})
3535
}
3636

37+
/**
38+
* Filters out clicks older than currentTime - userSettings.[user].statsInterval.
39+
* If currentTime is not set, use current time.
40+
*/
41+
function filteredClicks (instance, userRef = null, currentTime = undefined) {
42+
currentTime ??= DateTime.local().toUTC()
43+
const days = instance.userSettings?.[userRef]?.statsInterval ?? 0
44+
45+
if (!userRef || days === 0) {
46+
return instance.buttons
47+
}
48+
49+
return instance.buttons
50+
.filter(e => currentTime.diff(DateTime.fromISO(e.uuid).toUTC(), 'days').as('days') <= days)
51+
}
52+
3753
/**
3854
* Instance schema:
3955
*
@@ -48,6 +64,11 @@ function connectMongo () {
4864
* intervalStart = 32400, // 09:00
4965
* intervalEnd = 57600, // 16:00
5066
* timezone = 'Europe/Copenhagen',
67+
* userSettings = {
68+
* 'U12341234': {
69+
* statsInterval: 365, // Show stats for last 365 days, if 0 show all stats. Default 0.
70+
* },
71+
* },
5172
* scope = '',
5273
* botUserId = '',
5374
* appId = '',
@@ -105,16 +126,18 @@ module.exports = {
105126

106127
/**
107128
* Returns a list sorted by the number of times a user has won.
129+
* If userRef is given, it fetches user settings to do filtering based on select stats interval setting.
130+
* If _currentTime is given, use that time instead of current date. Only meant for tests.
108131
*/
109-
async clicksPerUser (instanceRef) {
132+
async clicksPerUser (instanceRef, userRef = null, _currentTime = undefined) {
110133
// We now that the database already have clicks in sorted order, so we can just grab the first
111134
// click for each button to get the winner.
112135
const collection = await instanceCollection()
113136
const instance = await collection.findOne({
114137
'team.id': instanceRef
115138
})
116139

117-
const winningClicks = instance.buttons
140+
const winningClicks = filteredClicks(instance, userRef, _currentTime)
118141
.map(e => e.clicks ? e.clicks[0] : undefined)
119142
.filter(e => e !== undefined)
120143
.map(e => e.user)
@@ -402,6 +425,23 @@ module.exports = {
402425
}
403426
},
404427

428+
/**
429+
* Sets a specific user setting, overwriting the old setting. Leaves other settings untouched.
430+
*/
431+
async setUserSetting (instanceRef, userRef, name, value) {
432+
const collection = await instanceCollection()
433+
const result = await collection.updateOne({ 'team.id': instanceRef }, {
434+
$set: {
435+
[`userSettings.${userRef}.${name}`]: value
436+
}
437+
})
438+
439+
if (result.matchedCount !== 1) {
440+
console.error(`result: ${result} as JSON: ${JSON.stringify(result)}`)
441+
throw new Error(`Failed to set user setting, nothing were matched in query! instanceRef: ${instanceRef}`)
442+
}
443+
},
444+
405445
async setWeekdays (instanceRef, weekdays) {
406446
const collection = await instanceCollection()
407447
const result = await collection.updateOne({ 'team.id': instanceRef }, {
@@ -485,6 +525,18 @@ module.exports = {
485525
}
486526
},
487527

528+
/**
529+
* Returns a object with user settings.
530+
*/
531+
async userSettings (instanceRef, userRef) {
532+
const collection = await instanceCollection()
533+
const instance = await collection.findOne({
534+
'team.id': instanceRef
535+
})
536+
537+
return instance.userSettings?.[userRef] ?? {}
538+
},
539+
488540
/**
489541
* Returns a list sorted by the longest winning streaks (descending order).
490542
*/

home.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ module.exports = {
248248
async publishHome ({ instanceRef, user }) {
249249
// Get current settings and stats.
250250
const instance = await db.instance(instanceRef)
251-
const stats = await statsBlocks(instanceRef)
251+
const stats = await statsBlocks(instanceRef, user)
252252

253253
// Check if user is admin.
254254
const isAdmin = instance.authedUser.id === user

routes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module.exports = (app, asyncEventHandler) => {
4343
try {
4444
const payload = JSON.parse(req.body.payload)
4545
const instanceRef = payload.team.id
46+
const userRef = payload.user.id
4647

4748
if (payload.type === 'block_actions') {
4849
for (const action of payload.actions) {
@@ -62,6 +63,9 @@ module.exports = (app, asyncEventHandler) => {
6263
case 'admin_endtime':
6364
await settings.setEndTime(res, instanceRef, action, asyncEventHandler)
6465
break
66+
case 'user_stats_interval':
67+
await settings.setUserSetting(res, instanceRef, action, userRef, 'statsInterval')
68+
break
6569
case 'wild_button':
6670
await clickCommand(res, payload, asyncEventHandler)
6771
break

0 commit comments

Comments
 (0)