Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
tibetsprague committed Aug 26, 2022
2 parents b9f0942 + 71e09ef commit 0d28249
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 62 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## [4.2.1] - 2022-08-25

# Added
- If a group is created with a parent group that it can't join then send a request to join

# Fixed
- Bug that was preventing email notifications going out for requests to join a child to a parent group.

## [4.2.0] - 2022-08-19

### Added
Expand Down
27 changes: 15 additions & 12 deletions api/graphql/mutations/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import convertGraphqlData from './convertGraphqlData'
import underlyingDeleteGroupTopic from '../../models/group/deleteGroupTopic'

// Util function
async function getModeratedGroup (userId, groupId) {
const group = await Group.find(groupId)
async function getModeratedGroup (userId, groupId, opts = {}) {
const group = await Group.find(groupId, opts)
if (!group) {
throw new Error('Group not found')
}

const isModerator = await GroupMembership.hasModeratorRole(userId, group)
const isModerator = await GroupMembership.hasModeratorRole(userId, group, opts)
if (!isModerator) {
throw new Error("You don't have permission to moderate this group")
}
Expand Down Expand Up @@ -119,8 +119,8 @@ export async function updateGroup (userId, groupId, changes) {
return group.update(convertGraphqlData(changes))
}

export async function inviteGroupToGroup(userId, fromId, toId, type, questionAnswers = []) {
const toGroup = await Group.find(toId)
export async function inviteGroupToGroup(userId, fromId, toId, type, questionAnswers = [], opts = {}) {
const toGroup = await Group.find(toId, opts)
if (!toGroup) {
throw new Error('Group not found')
}
Expand All @@ -129,34 +129,37 @@ export async function inviteGroupToGroup(userId, fromId, toId, type, questionAns
throw new Error('Invalid group relationship type')
}

const fromGroup = await getModeratedGroup(userId, fromId)
const fromGroup = await getModeratedGroup(userId, fromId, opts)

if (await GroupRelationship.forPair(fromGroup, toGroup).fetch()) {
if (await GroupRelationship.forPair(fromGroup, toGroup).fetch(opts)) {
throw new Error('Groups are already related')
}

// If current user is a moderator of both the from group and the to group they can automatically join the groups together
if (await GroupMembership.hasModeratorRole(userId, toGroup)) {
if (type === GroupRelationshipInvite.TYPE.ParentToChild) {
return { success: true, groupRelationship: await fromGroup.addChild(toGroup) }
return { success: true, groupRelationship: await fromGroup.addChild(toGroup, opts) }
} if (type === GroupRelationshipInvite.TYPE.ChildToParent) {
return { success: true, groupRelationship: await fromGroup.addParent(toGroup) }
return { success: true, groupRelationship: await fromGroup.addParent(toGroup, opts) }
}
} else {
const existingInvite = GroupRelationshipInvite.forPair(fromGroup, toGroup).fetch()
const existingInvite = GroupRelationshipInvite.forPair(fromGroup, toGroup).fetch(opts)

if (existingInvite && existingInvite.status === GroupRelationshipInvite.STATUS.Pending) {
return { success: false, groupRelationshipInvite: existingInvite }
}

// If there's an existing processed invite then let's leave it and create a new one
// TODO: what if the last one was rejected, do we let them create a new one?
const invite = await GroupRelationshipInvite.create({
userId,
fromGroupId: fromId,
toGroupId: toId,
type
})
}, opts)

for (let qa of questionAnswers) {
await GroupToGroupJoinRequestQuestionAnswer.forge({ join_request_id: invite.id, question_id: qa.questionId, answer: qa.answer }).save()
await GroupToGroupJoinRequestQuestionAnswer.forge({ join_request_id: invite.id, question_id: qa.questionId, answer: qa.answer }).save({}, opts)
}
return { success: true, groupRelationshipInvite: invite }
}
Expand Down
6 changes: 5 additions & 1 deletion api/models/Activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ module.exports = bookshelf.Model.extend({
if (this.get('group_id')) {
relations.push('group')
}
if (this.get('other_group_id')) {
relations.push('otherGroup')
}
await this.load(relations, {transacting: trx})
const notificationData = await Activity.generateNotificationMedia(this)

Expand Down Expand Up @@ -249,7 +252,8 @@ module.exports = bookshelf.Model.extend({
} else if (activity.get('comment_id')) {
return get(activity, 'relations.comment.relations.post.relations.groups', []).map(c => c.id)
} else if (activity.get('group_id')) {
return [activity.relations.group.id]
// For group to group join requests/invites other group is the one related to the reader of the notification
return activity.get('other_group_id') ? [activity.relations.group.id, activity.relations.otherGroup.id] : [activity.relations.group.id]
}
return []
},
Expand Down
42 changes: 27 additions & 15 deletions api/models/Group.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LocationHelpers } from 'hylo-shared'
import HasSettings from './mixins/HasSettings'
import findOrCreateThread from './post/findOrCreateThread'
import { groupFilter } from '../graphql/filters'
import { inviteGroupToGroup } from '../graphql/mutations/group.js'

import DataType, {
getDataTypeForInstance, getDataTypeForModel, getModelForDataType
Expand Down Expand Up @@ -546,20 +547,6 @@ module.exports = bookshelf.Model.extend(merge({

const memberships = await bookshelf.transaction(async trx => {
await group.save(null, {transacting: trx})
if (data.parent_ids) {
for (const parentId of data.parent_ids) {
// Only allow for adding parent groups that the creator is a moderator of or that are Open
const parentGroupMembership = await GroupMembership.forIds(userId, parentId, {
query: q => { q.select('group_memberships.*', 'groups.accessibility as accessibility', 'groups.visibility as visibility')}
}).fetch({ transacting: trx })

if (parentGroupMembership &&
(parentGroupMembership.get('role') === GroupMembership.Role.MODERATOR
|| parentGroupMembership.get('accessibility') === Group.Accessibility.OPEN)) {
await group.parentGroups().attach(parentId, { transacting: trx })
}
}
}

if (data.group_extensions) {
for (const extData of data.group_extensions) {
Expand All @@ -577,8 +564,33 @@ module.exports = bookshelf.Model.extend(merge({

await group.createInitialWidgets(trx)

return group.addMembers([userId],
const members = await group.addMembers([userId],
{role: GroupMembership.Role.MODERATOR}, { transacting: trx })

// Have to add/request add to parent group after moderator has been added to the group
if (data.parent_ids) {
for (const parentId of data.parent_ids) {
const parent = await Group.findActive(parentId, { transacting: trx })

if (parent) {
// Only allow for adding parent groups that the creator is a moderator of or that are Open
const parentGroupMembership = await GroupMembership.forIds(userId, parentId, {
query: q => { q.select('group_memberships.*', 'groups.accessibility as accessibility', 'groups.visibility as visibility')}
}).fetch({ transacting: trx })

if (parentGroupMembership &&
(parentGroupMembership.get('role') === GroupMembership.Role.MODERATOR
|| parentGroupMembership.get('accessibility') === Group.Accessibility.OPEN)) {
await group.parentGroups().attach(parentId, { transacting: trx })
} else {
// If can't add directly to parent group then send a request to join
await inviteGroupToGroup(userId, group.id, parentId, GroupRelationshipInvite.TYPE.ChildToParent, [], { transacting: trx })
}
}
}
}

return members
})

if (data.location && !data.location_id) {
Expand Down
4 changes: 2 additions & 2 deletions api/models/GroupMembership.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ module.exports = bookshelf.Model.extend(Object.assign({
return !!gm && gm.get('active')
},

async hasModeratorRole (userOrId, groupOrId) {
const gm = await this.forPair(userOrId, groupOrId).fetch()
async hasModeratorRole (userOrId, groupOrId, opts) {
const gm = await this.forPair(userOrId, groupOrId).fetch(opts)
return gm && gm.hasRole(this.Role.MODERATOR)
},

Expand Down
58 changes: 27 additions & 31 deletions api/models/GroupRelationshipInvite.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,24 +93,7 @@ module.exports = bookshelf.Model.extend(Object.assign({
}

return true
},

afterCreate: async function () {
await this.load(['toGroup', 'createdBy', 'fromGroup'])
const { toGroup, createdBy, fromGroup } = this.relations

const moderators = await toGroup.moderators().fetch()

const notifications = moderators.map(moderator => ({
actor_id: createdBy.id,
reader_id: moderator.id,
group_id: fromGroup.id,
other_group_id: toGroup.id,
reason: this.get('type') === GroupRelationshipInvite.TYPE.ParentToChild ? Activity.Reason.GroupChildGroupInvite : Activity.Reason.GroupParentGroupJoinRequest
}))

return Activity.saveForReasons(notifications)
},
}

}, EnsureLoad), {

Expand All @@ -126,21 +109,34 @@ module.exports = bookshelf.Model.extend(Object.assign({
ChildToParent: 1
},

create: function (opts) {
return new GroupRelationshipInvite({
create: async function (attrs, opts) {
const invite = await new GroupRelationshipInvite({
created_at: new Date(),
created_by_id: opts.userId || opts.createdById,
from_group_id: opts.fromGroupId,
message: opts.message,
created_by_id: attrs.userId || attrs.createdById,
from_group_id: attrs.fromGroupId,
message: attrs.message,
status: this.STATUS.Pending,
subject: opts.subject,
to_group_id: opts.toGroupId,
type: opts.type
}).save()
.then(async invite => {
invite.afterCreate()
return invite
})
subject: attrs.subject,
to_group_id: attrs.toGroupId,
type: attrs.type
}).save({}, opts)

await invite.load(['toGroup', 'createdBy', 'fromGroup'], opts)
const { toGroup, createdBy, fromGroup } = invite.relations

const moderators = await toGroup.moderators().fetch(opts)

const notifications = moderators.map(moderator => ({
actor_id: createdBy.id,
reader_id: moderator.id,
group_id: fromGroup.id,
other_group_id: toGroup.id,
reason: invite.get('type') === GroupRelationshipInvite.TYPE.ParentToChild ? Activity.Reason.GroupChildGroupInvite : Activity.Reason.GroupParentGroupJoinRequest
}))

await Activity.saveForReasons(notifications, opts.transacting)

return invite
},

createAcceptNotifications: function({ inviteId, actorId }) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"author": "Tibet Sprague <tibet@hylo.com>",
"license": "GNU AFFERO GENERAL PUBLIC LICENSE v3",
"private": true,
"version": "4.1.5",
"version": "4.2.1",
"repository": {
"type": "git",
"url": "git://github.com/Hylozoic/hylo-node.git"
Expand Down

0 comments on commit 0d28249

Please sign in to comment.