Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
tibetsprague committed Feb 6, 2024
2 parents ffe5837 + 6f01a74 commit 9c6e5b1
Show file tree
Hide file tree
Showing 40 changed files with 1,236 additions and 450 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## [5.7.0] - 2024-02-05

### Added
- Group Agreements: Groups can now have agreements that members must agree to when joining the group. Each agreement has a title and description, and newly joining members must agree to them before they are let in. If agreements change then the member will be asked to agree to the newly changed agreeements.
- You can now require Join Questions for Groups that are set to Accessibility = Closed or Open, no longer just for Restricted groups

### Changed
- When inviting someone to a group that has Join Questions new members are now asked to answer the join questions before being let in to the group even when invited by join link or email address.

## [5.6.2] - 2023-12-29

### Changed
Expand Down
108 changes: 54 additions & 54 deletions api/controllers/ExportController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ module.exports = {
groupData: async function (req, res) {
const p = req.allParams()

const user = await new User({id: req.session.userId} )
.fetch({ columns: ['email']})
const user = await new User({ id: req.session.userId }).fetch({ columns: ['email'] })

if (!p.groupId) {
return res.status(400).send({ error: "Please specify group ID" })
return res.status(400).send({ error: 'Please specify group ID' })
}
if (!p.datasets || !p.datasets.length) {
return res.status(400).send({ error: "Please specify datasets to export" })
return res.status(400).send({ error: 'Please specify datasets to export' })
}

// auth check
Expand All @@ -25,7 +24,7 @@ module.exports = {
}

if (!ok) {
return res.status(403).send({ error: "No access" })
return res.status(403).send({ error: 'No access' })
}

// process specified datasets
Expand All @@ -35,20 +34,20 @@ module.exports = {
}

// got to the end and nothing output/exited, throw error
throw new Error("Unknown datasets specified: " + JSON.stringify(p.datasets))
throw new Error('Unknown datasets specified: ' + JSON.stringify(p.datasets))
}
}

/**
* Group members export by Group ID
*/
async function exportMembers(groupId, req, email) {
async function exportMembers (groupId, req, email) {
const users = await new Group({ id: groupId })
.members()
.fetch()
.members()
.fetch()

const group = await new Group({ id: groupId })
.fetch()
.fetch()

const results = []
const questions = {}
Expand All @@ -68,54 +67,52 @@ async function exportMembers(groupId, req, email) {
// location (full details)
u.locationObject().fetch()
.then(location => {
results[idx]['location'] = renderLocation(location)
results[idx].location = renderLocation(location)
}),

// affiliations
u.affiliations().fetch()
.then(affils => {
results[idx]['affiliations'] = accumulatePivotCell(affils, renderAffiliation)
results[idx].affiliations = accumulatePivotCell(affils, renderAffiliation)
}),

// skills
u.skills().fetch()
.then(skills => {
results[idx]['skills'] = accumulatePivotCell(skills, renderSkill)
results[idx].skills = accumulatePivotCell(skills, renderSkill)
}),

// skills to learn
u.skillsToLearn().fetch()
.then(skills => {
results[idx]['skills_to_learn'] = accumulatePivotCell(skills, renderSkill)
results[idx].skills_to_learn = accumulatePivotCell(skills, renderSkill)
}),

// Join request questions & answers
u.joinRequests()
.where({ group_id: groupId, status: JoinRequest.STATUS.Accepted })
// Join questions & answers
// TODO: pull direectly from groupJoinQuestionAnswers. how to sort by latest of each question within that group?
// https://stackoverflow.com/questions/12245289/select-unique-values-sorted-by-date
u.groupJoinQuestionAnswers()
.where({ group_id: groupId })
.orderBy('created_at', 'DESC')
.fetchOne()
.then(jr => {
if (!jr) {
return null
}
return jr.questionAnswers().fetch()
.then(qas => Promise.all(qas.map(qa =>
Promise.all([
qa.question().fetch(),
Promise.resolve(qa)
])
)))
.fetch({ withRelated: ['question'] })
.then(answers => {
return Promise.all(answers.map(qa =>
Promise.all([
qa.load(['question']),
Promise.resolve(qa)
])
))
})
.then(data => {
if (!data) return
results[idx]['join_request_answers'] = accumulateJoinRequestQA(data, questions)
results[idx].join_question_answers = accumulateJoinQA(data, questions)
}),

// other groups the requesting member has acccess to
groupFilter(req.session.userId)(u.groups()).fetch()
.then(groups => {
results[idx]['groups'] = accumulatePivotCell(groups, renderGroup)
}),
results[idx].groups = accumulatePivotCell(groups, renderGroup)
})

])
}))
Expand All @@ -132,7 +129,7 @@ async function exportMembers(groupId, req, email) {
}

// toplevel output function for specific endpoints to complete with
function output(data, columns, email, groupName, questions) {
function output (data, columns, email, groupName, questions) {
// Add each question as a column in the results
const questionsArray = Object.values(questions)
questionsArray.forEach((question) => {
Expand All @@ -141,83 +138,86 @@ function output(data, columns, email, groupName, questions) {

// Add rows for each user to match their answers with the added question colums
const transformedData = data.map((user) => {
const answers = user.join_request_answers
const answers = user.join_question_answers
questionsArray.forEach((question) => {
if (!answers) {
user[`${question.text}`] = '-'
} else {
const foundAnswer = answers.find((answer) => `${question.id}` === `${answer.question_id}`)
user[`${question.text}`] = foundAnswer
? user[`${question.text}`] = foundAnswer.answer
:user[`${question.text}`] = '-'
: user[`${question.text}`] = '-'
}
})
return user
})

stringify(transformedData, {
header: true,
columns
}, (err, output) => {
const formattedDate = new Date().toISOString().slice(0,10)
if (err) {
console.error(err)
return
}
const formattedDate = new Date().toISOString().slice(0, 10)
const buff = Buffer.from(output)
const base64output = buff.toString('base64')

Queue.classMethod('Email', 'sendExportMembersList', {
email: email,
email: email,
files: [
{
id: `members-export-${groupName}-${formattedDate}.csv`,
data: base64output
}
}
]
})
})

}

// reduce helper to format lists of records into single CSV cells
function accumulatePivotCell(records, renderValue) {
function accumulatePivotCell (records, renderValue) {
return records.reduce((joined, a) => joined ? (joined + `,${renderValue(a)}`) : renderValue(a), null)
}

const accumulateJoinRequestQA = (records, questions) => {
const accumulateJoinQA = (records, questions) => {
// an array of question/answer pairs
if (records[0] && records[0][0]){
if (records[0] && records[0][0]) {
records.forEach((record) => {
const question = record[0].toJSON()
questions[question.id] = question
const question = record[0].toJSON().question
questions[question.id] = question
})
}
return records.reduce((accum, record) => accum.concat(renderJoinRequestAnswersToJSON(record)), [])
return records.reduce((accum, record) => accum.concat(renderJoinQuestionAnswersToJSON(record)), [])
}

// formatting for individual sub-cell record types

function renderLocation(l) {
function renderLocation (l) {
if (l === null || l.get('center') === null) {
return ''
}

const geometry = l.get('center') // :TODO: make this work for polygonal locations, if needed
const geometry = l.get('center') // :TODO: make this work for polygonal locations, if needed
const lat = geometry.lat
const lng = geometry.lng
return `${l.get('full_text')}${lat && lng ? ` (${lat.toFixed(3)},${lng.toFixed(3)})` : ''}`
}

function renderAffiliation(a) {
function renderAffiliation (a) {
return `${a.get('role')} ${a.get('preposition')} ${a.get('org_name')} ${a.get('url') ? `(${a.get('url')})` : ''}`
}

function renderSkill(s) {
function renderSkill (s) {
return s.get('name')
}

function renderJoinRequestAnswersToJSON(QApair) {
if (QApair.length === 0) {return []}
function renderJoinQuestionAnswersToJSON (QApair) {
if (QApair.length === 0) { return [] }
return [QApair[1].toJSON()]
}

function renderGroup(g) {
function renderGroup (g) {
return `${g.get('name')} (${Frontend.Route.group(g)})`
}
45 changes: 23 additions & 22 deletions api/graphql/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useLazyLoadedSchema } from '@envelop/core'
const { createServer, GraphQLYogaError } = require('@graphql-yoga/node')
import { readFileSync } from 'fs'
import { join } from 'path'
import setupBridge from '../../lib/graphql-bookshelf-bridge'
Expand Down Expand Up @@ -111,6 +110,7 @@ import { makeExecutableSchema } from 'graphql-tools'
import { inspect } from 'util'
import { red } from 'chalk'
import { merge, reduce } from 'lodash'
const { createServer, GraphQLYogaError } = require('@graphql-yoga/node')

const schemaText = readFileSync(join(__dirname, 'schema.graphql')).toString()

Expand Down Expand Up @@ -223,11 +223,11 @@ export function makeAuthenticatedQueries (userId, fetchOne, fetchMany) {
groupExists: (root, { slug }) => {
if (Group.isSlugValid(slug)) {
return Group.where(bookshelf.knex.raw('slug = ?', slug))
.count()
.then(count => {
if (count > 0) return {exists: true}
return {exists: false}
})
.count()
.then(count => {
if (count > 0) return {exists: true}
return {exists: false}
})
}
throw new GraphQLYogaError('Slug is invalid')
},
Expand All @@ -242,7 +242,7 @@ export function makeAuthenticatedQueries (userId, fetchOne, fetchMany) {
messageThread: (root, { id }) => fetchOne('MessageThread', id),
notifications: (root, { first, offset, resetCount, order = 'desc' }) => {
return fetchMany('Notification', { first, offset, order })
.tap(() => resetCount && User.resetNewNotificationCount(userId))
.tap(() => resetCount && User.resetNewNotificationCount(userId))
},
people: (root, args) => fetchMany('Person', args),
// you can query by id or email, with id taking preference
Expand All @@ -253,12 +253,12 @@ export function makeAuthenticatedQueries (userId, fetchOne, fetchMany) {
search: (root, args) => {
if (!args.first) args.first = 20
return Search.fullTextSearch(userId, args)
.then(({ models, total }) => {
// 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}))
})
.then(({ models, total }) => {
// 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}))
})
},
skills: (root, args) => fetchMany('Skill', args),
topic: (root, { id, name }) => // you can specify id or name, but not both
Expand All @@ -279,7 +279,7 @@ export function makePublicMutations (expressContext, fetchOne) {
}

export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
const { req, res } = expressContext
const { req } = expressContext
const sessionId = req.session.id

return {
Expand Down Expand Up @@ -324,10 +324,10 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {

createGroup: (root, { data }) => createGroup(userId, data),

createInvitation: (root, {groupId, data}) =>
createInvitation: (root, { groupId, data }) =>
createInvitation(userId, groupId, data), // consider sending locale from the frontend here

createJoinRequest: (root, {groupId, questionAnswers}) => createJoinRequest(userId, groupId, questionAnswers),
createJoinRequest: (root, { groupId, questionAnswers }) => createJoinRequest(userId, groupId, questionAnswers),

createMessage: (root, { data }) => createMessage(userId, data),

Expand All @@ -341,7 +341,7 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {

createZapierTrigger: (root, { groupIds, targetUrl, type, params }) => createZapierTrigger(userId, groupIds, targetUrl, type, params),

joinGroup: (root, { groupId }) => joinGroup(groupId, userId),
joinGroup: (root, { groupId, questionAnswers }) => joinGroup(groupId, userId, questionAnswers),

joinProject: (root, { id }) => joinProject(id, userId),

Expand Down Expand Up @@ -373,7 +373,7 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {

deleteZapierTrigger: (root, { id }) => deleteZapierTrigger(userId, id),

expireInvitation: (root, {invitationId}) =>
expireInvitation: (root, { invitationId }) =>
expireInvitation(userId, invitationId),

findOrCreateThread: (root, { data }) => findOrCreateThread(userId, data),
Expand All @@ -390,7 +390,7 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
inviteGroupToJoinParent: (root, { parentId, childId }) =>
inviteGroupToGroup(userId, parentId, childId, GroupRelationshipInvite.TYPE.ParentToChild),

invitePeopleToEvent: (root, {eventId, inviteeIds}) =>
invitePeopleToEvent: (root, { eventId, inviteeIds }) =>
invitePeopleToEvent(userId, eventId, inviteeIds),

leaveGroup: (root, { id }) => leaveGroup(userId, id),
Expand Down Expand Up @@ -450,10 +450,10 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
requestToAddGroupToParent: (root, { parentId, childId, questionAnswers }) =>
inviteGroupToGroup(userId, childId, parentId, GroupRelationshipInvite.TYPE.ChildToParent, questionAnswers),

resendInvitation: (root, {invitationId}) =>
resendInvitation: (root, { invitationId }) =>
resendInvitation(userId, invitationId),

respondToEvent: (root, {id, response}) =>
respondToEvent: (root, { id, response }) =>
respondToEvent(userId, id, response),

subscribe: (root, { groupId, topicId, isSubscribing }) =>
Expand All @@ -466,7 +466,8 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
unlinkAccount: (root, { provider }) =>
unlinkAccount(userId, provider),

updateGroupRole: (root, { groupRoleId, color, name, description, emoji, active, groupId }) => updateGroupRole({userId, groupRoleId, color, name, description, emoji, active, groupId}),
updateGroupRole: (root, { groupRoleId, color, name, description, emoji, active, groupId }) =>
updateGroupRole({ userId, groupRoleId, color, name, description, emoji, active, groupId }),

updateGroupSettings: (root, { id, changes }) =>
updateGroup(userId, id, changes),
Expand Down
Loading

0 comments on commit 9c6e5b1

Please sign in to comment.