Skip to content

Commit cd63f46

Browse files
committed
Merge branch 'dev'
2 parents ceece7d + d582625 commit cd63f46

File tree

11 files changed

+180
-25
lines changed

11 files changed

+180
-25
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## Unreleased
88

9+
## [5.9.1] - 2024-09-19
10+
11+
### Added
12+
- Track editedAt timestamp for posts and comments
13+
14+
### Changed
15+
- New users have the default notification setting of all posts, meaning they will now receive notifications for all posts in groups they are a member of, instead of just important ones.
16+
917
## [5.9.0] - 2024-07-23
1018

1119
### Added

api/graphql/makeModels.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ export default function makeModels (userId, isAdmin, apiClient) {
270270
'commentsTotal',
271271
'created_at',
272272
'donations_link',
273+
'edited_at',
273274
'end_time',
274275
'fulfilled_at',
275276
'is_public',
@@ -814,7 +815,8 @@ export default function makeModels (userId, isAdmin, apiClient) {
814815
Comment: {
815816
model: Comment,
816817
attributes: [
817-
'created_at'
818+
'created_at',
819+
'edited_at'
818820
],
819821
relations: [
820822
'post',

api/graphql/schema.graphql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,8 @@ type Comment {
473473
createdFrom: String
474474
# The Person who wrote the comment
475475
creator: Person
476+
# When the text was last edited
477+
editedAt: Date
476478
# For threaded comments what's the parent of this one
477479
parentComment: Comment
478480
# The Post this comment is on
@@ -1758,6 +1760,8 @@ type Post {
17581760
details: String
17591761
# Link to a donation page, only used for projects only right now
17601762
donationsLink: String
1763+
# When this post was last significantly edited (such as title and details)
1764+
editedAt: Date
17611765
# When this post "ends". Used for events, resources, projects, requests and offers right now
17621766
endTime: Date
17631767
# Number of people following this post (when someone comments on or reacts to a post they follow it)

api/models/User.js

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -556,16 +556,16 @@ module.exports = bookshelf.Model.extend(merge({
556556

557557
unlinkAccount (provider) {
558558
const fieldName = {
559-
'facebook': 'facebook_url',
560-
'linkedin': 'linkedin_url',
561-
'twitter': 'twitter_name'
559+
facebook: 'facebook_url',
560+
linkedin: 'linkedin_url',
561+
twitter: 'twitter_name'
562562
}[provider]
563563

564564
if (!fieldName) throw new Error(`${provider} not a supported provider`)
565565

566566
return Promise.join(
567-
LinkedAccount.query().where({'user_id': this.id, provider_key: provider}).del(),
568-
this.save({[fieldName]: null})
567+
LinkedAccount.query().where({ user_id: this.id, provider_key: provider }).del(),
568+
this.save({ [fieldName]: null })
569569
)
570570
},
571571

@@ -614,35 +614,35 @@ module.exports = bookshelf.Model.extend(merge({
614614
AXOLOTL_ID: '13986',
615615

616616
authenticate: Promise.method(function (email, password) {
617-
var compare = Promise.promisify(bcrypt.compare, bcrypt)
617+
const compare = Promise.promisify(bcrypt.compare, bcrypt)
618618

619619
if (!email) throw new GraphQLYogaError('no email provided')
620620
if (!password) throw new GraphQLYogaError('no password provided')
621621

622622
return User.query('whereRaw', 'lower(email) = lower(?)', email)
623-
.fetch({withRelated: ['linkedAccounts']})
624-
.then(function (user) {
625-
if (!user) throw new GraphQLYogaError('email not found')
623+
.fetch({ withRelated: ['linkedAccounts'] })
624+
.then(function (user) {
625+
if (!user) throw new GraphQLYogaError('email not found')
626626

627-
var account = user.relations.linkedAccounts.find(a => a.get('provider_key') === 'password')
627+
const account = user.relations.linkedAccounts.find(a => a.get('provider_key') === 'password')
628628

629-
if (!account) {
630-
var keys = user.relations.linkedAccounts.pluck('provider_key')
631-
throw new GraphQLYogaError(`password account not found. available: [${keys.join(',')}]`)
632-
}
629+
if (!account) {
630+
const keys = user.relations.linkedAccounts.pluck('provider_key')
631+
throw new GraphQLYogaError(`password account not found. available: [${keys.join(',')}]`)
632+
}
633633

634-
return compare(password, account.get('provider_user_id')).then(function (match) {
635-
if (!match) throw new GraphQLYogaError('password does not match')
634+
return compare(password, account.get('provider_user_id')).then(function (match) {
635+
if (!match) throw new GraphQLYogaError('password does not match')
636636

637-
return user
637+
return user
638+
})
638639
})
639-
})
640640
}),
641641

642642
clearSessionsFor: async function({ userId, sessionId }) {
643643
const redisClient = await RedisClient.create()
644644
for await (const key of redisClient.scanIterator({ MATCH: `sess:${userId}:*` })) {
645-
if (key !== "sess:" + sessionId) {
645+
if (key !== 'sess:' + sessionId) {
646646
await redisClient.del(key)
647647
}
648648
}
@@ -659,7 +659,8 @@ module.exports = bookshelf.Model.extend(merge({
659659
digest_frequency: 'daily',
660660
signup_in_progress: true,
661661
dm_notifications: 'both',
662-
comment_notifications: 'both'
662+
comment_notifications: 'both',
663+
post_notifications: 'all'
663664
},
664665
active: true
665666
}, omit(attributes, 'account', 'group', 'role'))

api/models/comment/updateComment.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const { GraphQLYogaError } = require('@graphql-yoga/node')
2-
import { difference, uniq } from 'lodash'
2+
import { difference, uniq, isEqual } from 'lodash'
33
import { updateMedia } from './util'
44

55
export default async function updateComment (commenterId, id, params) {
@@ -17,6 +17,10 @@ export default async function updateComment (commenterId, id, params) {
1717
const existingFollowers = await post.followers().fetch().then(f => f.pluck('id'))
1818
const newFollowers = difference(uniq(mentioned.concat(commenterId)), existingFollowers)
1919

20+
if (!isEqual(comment.text(), params.text)) {
21+
attrs.edited_at = new Date()
22+
}
23+
2024
return bookshelf.transaction(trx =>
2125
comment.save(attrs, {transacting: trx})
2226
.then(() => updateMedia(comment, attachments, trx))
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import factories from '../../../test/setup/factories'
2+
import updateComment from './updateComment'
3+
4+
describe('updateComment', () => {
5+
let user, post, comment
6+
7+
before(() => {
8+
user = factories.user()
9+
return user.save()
10+
.then(() => {
11+
post = factories.post({type: Post.Type.THREAD, user_id: user.id})
12+
return post.save()
13+
})
14+
.then(() => {
15+
comment = factories.comment({user_id: user.id, post_id: post.id})
16+
return comment.save()
17+
})
18+
})
19+
20+
it('fails without ID', () => {
21+
return updateComment(user.id, null, {text: 'foo'})
22+
.then(() => {
23+
expect.fail('should reject')
24+
})
25+
.catch(err => {
26+
expect(err.message).to.equal('updateComment called with no ID')
27+
})
28+
})
29+
30+
it('prevents updating non-existent comments', () => {
31+
const id = `${comment.id}0`
32+
return updateComment(user.id, id, {text: 'foo'})
33+
.then(() => {
34+
expect.fail('should reject')
35+
})
36+
.catch(err => {
37+
expect(err.message).to.equal('cannot find comment with ID')
38+
})
39+
})
40+
41+
it('does not set edited_at field if text does not change', async () => {
42+
const recent = false
43+
updateComment(user.id, comment.id, {recent})
44+
.then(async () => {
45+
comment = await Comment.find(comment.id)
46+
expect(comment.edited_at).to.equal(undefined)
47+
})
48+
})
49+
50+
it('sets edited_at field when text changes', async () => {
51+
const text = `${comment.text}, so say I!`
52+
updateComment(user.id, comment.id, {text})
53+
.then(async () => {
54+
comment = await Comment.find(comment.id)
55+
expect(comment.edited_at).not.to.equal(undefined)
56+
})
57+
})
58+
})

api/models/post/updatePost.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { GraphQLYogaError } = require('@graphql-yoga/node')
22
import setupPostAttrs from './setupPostAttrs'
33
import updateChildren from './updateChildren'
4+
import { isEqual } from 'lodash'
45
import {
56
updateGroups,
67
updateAllMedia,
@@ -27,6 +28,10 @@ export default function updatePost (userId, id, params) {
2728
throw new GraphQLYogaError("This post can't be modified")
2829
}
2930

31+
if (!isEqual(post.details(), params.description) || !isEqual(post.title(), params.name)) {
32+
attrs.edited_at = new Date()
33+
}
34+
3035
return post.save(attrs, { patch: true, transacting })
3136
.tap(updatedPost => afterUpdatingPost(updatedPost, { params, userId, transacting }))
3237
})))

api/models/post/updatePost.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@ describe('updatePost', () => {
1414
})
1515
})
1616

17+
it('fails without ID', () => {
18+
try {
19+
return updatePost(user.id, null, {name: 'foo'})
20+
.then(() => {
21+
expect.fail('should reject')
22+
})
23+
.catch(err => {
24+
expect(err.message).to.equal('updatePost called with no ID')
25+
})
26+
} catch(err) {
27+
expect(err.message).to.equal('updatePost called with no ID')
28+
}
29+
})
30+
31+
it('prevents updating non-existent posts', () => {
32+
const id = `${post.id}0`
33+
return updatePost(user.id, id, {name: 'foo'})
34+
.then(() => {
35+
expect.fail('should reject')
36+
})
37+
.catch(err => {
38+
expect(err.message).to.equal('Post not found')
39+
})
40+
})
41+
1742
it('prevents updating of certain post types', () => {
1843
return updatePost(user.id, post.id, {name: 'foo'})
1944
.then(() => {
@@ -23,6 +48,33 @@ describe('updatePost', () => {
2348
expect(err.message).to.equal("This post can't be modified")
2449
})
2550
})
51+
52+
it('does not set edited_at field if name or description does not change', async () => {
53+
const location_id = '12345'
54+
updatePost(user.id, post.id, {location_id})
55+
.then(async () => {
56+
post = await Post.find(post.id)
57+
expect(post.get('edited_at')).to.equal(undefined)
58+
})
59+
})
60+
61+
it('sets edited_at field when name changes', async () => {
62+
const name = `${post.name}, what ho, Bertie.`
63+
updatePost(user.id, post.id, {name})
64+
.then(async () => {
65+
post = await Post.find(post.id)
66+
expect(post.get('edited_at').getTime()).to.be.closeTo(new Date().getTime(), 2000)
67+
})
68+
})
69+
70+
it('sets edited_at field when description changes', async () => {
71+
const description = `${post.description}, I say, Jeeves!`
72+
updatePost(user.id, post.id, {description})
73+
.then(async () => {
74+
post = await Post.find(post.id)
75+
expect(post.get('edited_at').getTime()).to.be.closeTo(new Date().getTime(), 2000)
76+
})
77+
})
2678
})
2779

2880
describe('afterUpdatingPost', () => {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
exports.up = async function(knex) {
3+
await knex.schema.table('posts', t => {
4+
t.timestamp('edited_at')
5+
})
6+
7+
await knex.schema.table('comments', t => {
8+
t.timestamp('edited_at')
9+
})
10+
}
11+
12+
exports.down = async function(knex) {
13+
await knex.schema.table('posts', t => {
14+
t.dropColumn('edited_at')
15+
})
16+
17+
await knex.schema.table('comments', t => {
18+
t.dropColumn('edited_at')
19+
})
20+
}

migrations/schema.sql

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,8 @@ CREATE TABLE public.comments (
239239
deactivated_at timestamp with time zone,
240240
recent boolean,
241241
created_from character varying(255),
242-
comment_id bigint
242+
comment_id bigint,
243+
edited_at timestamp with time zone
243244
);
244245

245246

@@ -1958,7 +1959,7 @@ CREATE TABLE public.posts (
19581959
voting_method text,
19591960
anonymous_voting text,
19601961
proposal_vote_limit integer,
1961-
proposal_strict boolean DEFAULT false
1962+
edited_at timestamp with time zone
19621963
);
19631964

19641965

0 commit comments

Comments
 (0)