Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
tibetsprague committed Jul 3, 2024
2 parents a482184 + cccf786 commit 1fc4fb5
Show file tree
Hide file tree
Showing 30 changed files with 885 additions and 165 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

## [5.8.0] - 2024-07-03

### Added
- New post type: Proposals! Use these to propose ideas, projects, decisions, or anything else to your group. Proposals can be voted on, and have a status that can be updated by the proposer.

### Fixed
- Deleting user accounts

## [5.7.1] - 2024-02-20

## Added
Expand Down
34 changes: 25 additions & 9 deletions api/graphql/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
addModerator,
addPeopleToProjectRole,
addPostToCollection,
addProposalVote,
addRoleToMember,
addSkill,
addSkillToLearn,
Expand Down Expand Up @@ -79,14 +80,17 @@ import {
removePost,
removePostFromCollection,
removeRoleFromMember,
removeProposalVote,
removeSkill,
removeSkillToLearn,
removeSuggestedSkillFromGroup,
resendInvitation,
respondToEvent,
sendEmailVerification,
sendPasswordReset,
setProposalOptions,
subscribe,
swapProposalVote,
unblockUser,
unfulfillPost,
unlinkAccount,
Expand All @@ -98,11 +102,12 @@ import {
updateMe,
updateMembership,
updatePost,
updateProposalOptions,
updateProposalOutcome,
updateStripeAccount,
updateWidget,
useInvitation,
verifyEmail,
vote
verifyEmail
} from './mutations'
import InvitationService from '../services/InvitationService'
import makeModels from './makeModels'
Expand Down Expand Up @@ -225,8 +230,8 @@ export function makeAuthenticatedQueries (userId, fetchOne, fetchMany) {
return Group.where(bookshelf.knex.raw('slug = ?', slug))
.count()
.then(count => {
if (count > 0) return {exists: true}
return {exists: false}
if (count > 0) return { exists: true }
return { exists: false }
})
}
throw new GraphQLYogaError('Slug is invalid')
Expand Down Expand Up @@ -257,7 +262,7 @@ export function makeAuthenticatedQueries (userId, fetchOne, fetchMany) {
// FIXME this shouldn't be used directly here -- there should be some
// way of integrating this into makeModels and using the presentation
// logic that's already in the fetcher
return presentQuerySet(models, merge(args, {total}))
return presentQuerySet(models, merge(args, { total }))
})
},
skills: (root, args) => fetchMany('Skill', args),
Expand Down Expand Up @@ -291,7 +296,7 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {

acceptJoinRequest: (root, { joinRequestId }) => acceptJoinRequest(userId, joinRequestId),

addGroupRole: (root, { groupId, color, name, description, emoji }) => addGroupRole({userId, groupId, color, name, description, emoji}),
addGroupRole: (root, { groupId, color, name, description, emoji }) => addGroupRole({ userId, groupId, color, name, description, emoji }),

addModerator: (root, { personId, groupId }) =>
addModerator(userId, personId, groupId),
Expand All @@ -302,6 +307,8 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
addPostToCollection: (root, { collectionId, postId }) =>
addPostToCollection(userId, collectionId, postId),

addProposalVote: (root, { postId, optionId }) => addProposalVote({ userId, postId, optionId }),

addRoleToMember: (root, { personId, groupRoleId, groupId }) => addRoleToMember({ userId, personId, groupRoleId, groupId }),

addSkill: (root, { name }) => addSkill(userId, name),
Expand Down Expand Up @@ -438,6 +445,8 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
removePostFromCollection: (root, { collectionId, postId }) =>
removePostFromCollection(userId, collectionId, postId),

removeProposalVote: (root, { postId, optionId }) => removeProposalVote({ userId, postId, optionId }),

removeRoleFromMember: (root, { groupRoleId, personId, groupId }) => removeRoleFromMember({ groupRoleId, personId, userId, groupId }),

removeSkill: (root, { id, name }) => removeSkill(userId, id || name),
Expand All @@ -456,9 +465,13 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
respondToEvent: (root, { id, response }) =>
respondToEvent(userId, id, response),

setProposalOptions: (root, { postId, options }) => setProposalOptions({ userId, postId, options }),

subscribe: (root, { groupId, topicId, isSubscribing }) =>
subscribe(userId, topicId, groupId, isSubscribing),

swapProposalVote: (root, { postId, removeOptionId, addOptionId }) => swapProposalVote({ userId, postId, removeOptionId, addOptionId }),

unblockUser: (root, { blockedUserId }) => unblockUser(userId, blockedUserId),

unfulfillPost: (root, { postId }) => unfulfillPost(userId, postId),
Expand All @@ -481,16 +494,19 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
updateMembership: (root, args) => updateMembership(userId, args),

updatePost: (root, args) => updatePost(userId, args),

updateProposalOptions: (root, { postId, options }) => updateProposalOptions({ userId, postId, options }),

updateProposalOutcome: (root, { postId, proposalOutcome }) => updateProposalOutcome({ userId, postId, proposalOutcome }),

updateComment: (root, args) => updateComment(userId, args),

updateStripeAccount: (root, { accountId }) => updateStripeAccount(userId, accountId),

updateWidget: (root, { id, changes }) => updateWidget(id, changes),

useInvitation: (root, { invitationToken, accessCode }) =>
useInvitation(userId, invitationToken, accessCode),

vote: (root, { postId, isUpvote }) => vote(userId, postId, isUpvote)
useInvitation(userId, invitationToken, accessCode)
}
}

Expand Down
45 changes: 33 additions & 12 deletions api/graphql/makeModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export default function makeModels (userId, isAdmin, apiClient) {
attributes: [
'accept_contributions',
'announcement',
'anonymous_voting',
'commentsTotal',
'created_at',
'donations_link',
Expand All @@ -197,6 +198,10 @@ export default function makeModels (userId, isAdmin, apiClient) {
'link_preview_featured',
'location',
'project_management_link',
'proposal_status',
'voting_method',
'proposal_outcome',
'quorum',
'reactions_summary',
'start_time',
'timezone',
Expand All @@ -207,8 +212,8 @@ export default function makeModels (userId, isAdmin, apiClient) {
commenters: (p, { first }) => p.getCommenters(first, userId),
commentersTotal: p => p.getCommentersTotal(userId),
details: p => p.details(userId),
isAnonymousVote: p => p.get('anonymous_voting') === 'true',
myReactions: p => userId ? p.reactionsForUser(userId).fetch() : [],
myVote: p => userId ? p.userVote(userId).then(v => !!v) : false, // Remove once Mobile has been updated
myEventResponse: p =>
userId && p.isEvent()
? p.userEventInvitation(userId).then(eventInvitation => eventInvitation ? eventInvitation.get('response') : '')
Expand All @@ -222,6 +227,8 @@ export default function makeModels (userId, isAdmin, apiClient) {
'locationObject',
{ members: { querySet: true } },
{ eventInvitations: { querySet: true } },
{ proposalOptions: { querySet: true } },
{ proposalVotes: { querySet: true } },
'linkPreview',
'postMemberships',
{
Expand Down Expand Up @@ -258,6 +265,8 @@ export default function makeModels (userId, isAdmin, apiClient) {
mentionsOf,
offset,
order,
proposalOutcome,
proposalStatus,
sortBy,
search,
topic,
Expand All @@ -283,6 +292,8 @@ export default function makeModels (userId, isAdmin, apiClient) {
onlyMyGroups: context === 'all',
onlyPublic: context === 'public',
order,
proposalOutcome,
proposalStatus,
sort: sortBy,
term: search,
topic,
Expand Down Expand Up @@ -812,17 +823,6 @@ export default function makeModels (userId, isAdmin, apiClient) {
],
filter: nonAdminFilter(reactionFilter('reactions', userId))
},
Vote: { // TO BE REMOVED ONCE MOBILE IS UPDATED
model: Reaction,
getters: {
createdAt: v => v.get('date_reacted')
},
relations: [
'post',
{ user: { alias: 'voter' } }
],
filter: nonAdminFilter(reactionFilter('reactions', userId))
},

GroupTopic: {
model: GroupTag,
Expand Down Expand Up @@ -979,6 +979,27 @@ export default function makeModels (userId, isAdmin, apiClient) {
]
},

ProposalOption: {
model: ProposalOption,
attributes: [
'emoji',
'color',
'text'
]
},

ProposalVote: {
model: ProposalVote,
attributes: [
'created_at',
'id',
'option_id'
],
relations: [
'user'
]
},

GroupExtension: {
model: GroupExtension,
attributes: [
Expand Down
7 changes: 6 additions & 1 deletion api/graphql/mutations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ export {
createPost,
fulfillPost,
unfulfillPost,
setProposalOptions,
addProposalVote,
removeProposalVote,
swapProposalVote,
updatePost,
vote,
updateProposalOptions,
updateProposalOutcome,
deletePost,
pinPost
} from './post'
Expand Down
111 changes: 98 additions & 13 deletions api/graphql/mutations/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ export function createPost (userId, data) {
.then(validatedData => underlyingCreatePost(userId, validatedData))
}

export function deletePost (userId, postId) {
return Post.find(postId)
.then(post => {
if (!post) {
throw new GraphQLYogaError("Post does not exist")
}
if (post.get('user_id') !== userId) {
throw new GraphQLYogaError("You don't have permission to modify this post")
}
return Post.deactivate(postId)
})
.then(() => ({success: true}))
}

export function updatePost (userId, { id, data }) {
return convertGraphqlPostData(data)
.tap(convertedData => validatePostData(userId, convertedData))
Expand Down Expand Up @@ -38,22 +52,92 @@ export function unfulfillPost (userId, postId) {
.then(() => ({success: true}))
}

export function vote (userId, postId, isUpvote) { // TODO: remove after mobile brought back into sync
export async function addProposalVote ({ userId, postId, optionId }) {
if (!userId || !postId || !optionId) throw new GraphQLYogaError(`Missing required parameters: ${JSON.stringify({ userId, postId, optionId })}`)

const authorized = await Post.isVisibleToUser(postId, userId)
if (!authorized) throw new GraphQLYogaError("You don't have permission to vote on this post")

return Post.find(postId)
.then(post => post.vote(userId, isUpvote))
.then(async post => {
if (post.get('proposal_status') !== Post.Proposal_Status.VOTING && post.get('proposal_status') !== Post.Proposal_Status.CASUAL) throw new GraphQLYogaError('Cannot vote on a proposal that is in discussion or completed')
await post.addFollowers([userId])
return post.addProposalVote({ userId, optionId })
})
.catch((err) => { throw new GraphQLYogaError(`adding of vote failed: ${err}`) })
.then(() => ({ success: true }))
}
export function deletePost (userId, postId) {

export async function removeProposalVote ({ userId, postId, optionId }) {
if (!userId || !postId || !optionId) throw new GraphQLYogaError(`Missing required parameters: ${JSON.stringify({ userId, postId, optionId })}`)

const authorized = await Post.isVisibleToUser(postId, userId)
if (!authorized) throw new GraphQLYogaError("You don't have permission to vote on this post")
return Post.find(postId)
.then(post => {
if (!post) {
throw new GraphQLYogaError("Post does not exist")
}
if (post.get('user_id') !== userId) {
throw new GraphQLYogaError("You don't have permission to modify this post")
}
return Post.deactivate(postId)
})
.then(() => ({success: true}))
.then(post => {
if (post.get('proposal_status') !== Post.Proposal_Status.VOTING && post.get('proposal_status') !== Post.Proposal_Status.CASUAL) throw new GraphQLYogaError('Cannot vote on a proposal that is in discussion or completed')
return post.removeProposalVote({ userId, optionId })
})
.catch((err) => { throw new GraphQLYogaError(`removal of vote failed: ${err}`) })
.then(() => ({ success: true }))
}

export async function setProposalOptions ({ userId, postId, options }) {
console.log('entering setProposalOptions')
if (!userId || !postId || !options) throw new GraphQLYogaError(`Missing required parameters: ${JSON.stringify({ userId, postId, options })}`)
const authorized = await Post.isVisibleToUser(postId, userId)
if (!authorized) throw new GraphQLYogaError("You don't have permission to modify this post")
return Post.find(postId)
.then(post => {
if (post.get('proposal_status') !== Post.Proposal_Status.DISCUSSION) throw new GraphQLYogaError("Proposal options cannot be changed unless the proposal is in 'discussion'")
console.log('setting options')
return post.setProposalOptions({ options })
})
.catch((err) => { throw new GraphQLYogaError(`setting of options failed: ${err}`) })
.then(() => ({ success: true }))
}

export async function updateProposalOptions ({ userId, postId, options }) {
if (!userId || !postId || !options) throw new GraphQLYogaError(`Missing required parameters: ${JSON.stringify({ userId, postId, options })}`)
const authorized = await Post.isVisibleToUser(postId, userId)
if (!authorized) throw new GraphQLYogaError("You don't have permission to modify this post")
return Post.find(postId)
.then(post => {
if (post.get('proposal_status') === Post.Proposal_Status.COMPLETED && post.get('proposal_status') !== Post.Proposal_Status.CASUAL) throw new GraphQLYogaError("Proposal options cannot be changed once a proposal is complete'")
return post.updateProposalOptions({ options, userId, opts: { transacting: null, require: false } })
})
.catch((err) => { throw new GraphQLYogaError(`setting of options failed: ${err}`) })
.then(() => ({ success: true }))
}

export async function swapProposalVote ({ userId, postId, removeOptionId, addOptionId }) {
if (!userId || !postId || !removeOptionId || !addOptionId) throw new GraphQLYogaError(`Missing required parameters: ${JSON.stringify({ userId, postId, removeOptionId, addOptionId })}`)
const authorized = await Post.isVisibleToUser(postId, userId)
if (!authorized) throw new GraphQLYogaError("You don't have permission to vote on this post")
if (removeOptionId === addOptionId) throw new GraphQLYogaError('You cannot swap a vote for the same option')

const post = await Post.find(postId)
if (!post) throw new GraphQLYogaError(`Couldn't find post for ${postId}`)
if (post.get('proposal_status') !== Post.Proposal_Status.VOTING && post.get('proposal_status') !== Post.Proposal_Status.CASUAL) throw new GraphQLYogaError('Cannot vote on a proposal that is in discussion or completed')

try {
await post.removeProposalVote({ userId, optionId: removeOptionId })
await post.addProposalVote({ userId, optionId: addOptionId })
return { success: true }
} catch (err) {
throw new GraphQLYogaError(`swap of vote failed: ${err}`)
}
}

export function updateProposalOutcome ({ userId, postId, proposalOutcome }) {
return Post.find(postId)
.then(post => {
if (post.get('user_id') !== userId) {
throw new GraphQLYogaError("You don't have permission to modify this post")
}
return post.updateProposalOutcome(proposalOutcome)
})
.then(() => ({ success: true }))
}

export async function pinPost (userId, postId, groupId) {
Expand All @@ -74,6 +158,7 @@ export async function pinPost (userId, postId, groupId) {
// the legacy code expects -- this sort of thing can be removed/refactored once
// hylo-redux is no longer in use
function convertGraphqlPostData (data) {
console.log(data, 'convertGraphqlPostData')
return Promise.resolve(Object.assign({
name: data.title,
description: data.details,
Expand Down
Loading

0 comments on commit 1fc4fb5

Please sign in to comment.