From e7386067044ae3bee2248e8fe4e7f091f11035f0 Mon Sep 17 00:00:00 2001 From: SJ Pratt Date: Wed, 26 Jul 2017 01:17:54 -0700 Subject: [PATCH] clarify distinction & relationship btwn user + member; simplify idm user data integration --- src/common/actions/cycle.js | 48 ++++--- src/common/actions/member.js | 100 ++++++++++++++ src/common/actions/project.js | 34 ----- .../actions/queries/deactivateMember.js | 14 ++ src/common/actions/queries/findMembers.js | 22 ++- src/common/actions/queries/findUsers.js | 27 ---- .../actions/queries/getCycleVotingResults.js | 2 +- ...{getUserSummary.js => getMemberSummary.js} | 10 +- .../actions/queries/getProjectSummary.js | 10 +- src/common/actions/queries/index.js | 11 +- src/common/actions/queries/lockSurvey.js | 50 ------- .../actions/queries/lockSurveyForMember.js | 14 ++ src/common/actions/queries/unlockSurvey.js | 50 ------- .../actions/queries/unlockSurveyForMember.js | 14 ++ .../{updateUser.js => updateMember.js} | 6 +- src/common/actions/schemas/index.js | 4 - src/common/actions/survey.js | 34 +++++ src/common/actions/types/index.js | 22 ++- src/common/actions/user.js | 126 ------------------ .../components/CycleVotingResults/index.jsx | 2 +- .../{UserDetail => MemberDetail}/index.jsx | 116 ++++++++-------- .../{UserDetail => MemberDetail}/index.scss | 2 +- .../{UserDetail => MemberDetail}/theme.scss | 0 .../{UserForm => MemberForm}/index.jsx | 14 +- .../{UserForm => MemberForm}/index.scss | 0 src/common/components/MemberList/index.jsx | 34 +++++ .../index.jsx | 12 +- .../index.scss | 2 +- src/common/components/ProjectDetail/index.jsx | 76 +++++------ .../index.jsx | 69 ++++------ .../index.scss | 2 +- src/common/components/UserList/index.jsx | 34 ----- .../VotingPoolResults/CandidateGoal.jsx | 1 - .../VotingPoolResults/__tests__/index.test.js | 8 +- .../components/VotingPoolResults/index.jsx | 14 +- .../__tests__/CycleVotingResults.test.js | 14 +- .../{UserList.test.js => MemberList.test.js} | 17 ++- .../components/__tests__/ProjectList.test.js | 6 +- src/common/containers/App/index.jsx | 6 +- .../containers/CycleVotingResults/index.jsx | 59 +++----- src/common/containers/MemberDetail/index.jsx | 117 ++++++++++++++++ .../{UserForm => MemberForm}/index.jsx | 40 +++--- .../{UserList => MemberList}/index.css | 0 .../{UserList => MemberList}/index.jsx | 70 +++++----- src/common/containers/ProjectDetail/index.jsx | 80 ++++++----- src/common/containers/ProjectList/index.jsx | 28 ++-- src/common/containers/UserDetail/index.jsx | 111 --------------- src/common/reducers/index.js | 6 +- src/common/reducers/memberSummaries.js | 38 ++++++ src/common/reducers/projectSummaries.js | 12 -- src/common/reducers/surveys.js | 12 ++ src/common/reducers/userSummaries.js | 62 --------- src/common/reducers/users.js | 35 ----- src/common/routes/index.jsx | 18 +-- src/common/util/__tests__/index.test.js | 11 ++ src/common/util/index.js | 11 ++ src/common/util/userCan.js | 16 +-- src/common/validations/index.js | 2 +- src/common/validations/{user.js => member.js} | 2 +- src/common/validations/project.js | 4 +- src/script/cloneArtifacts.js | 4 +- src/script/previewProjects.js | 27 ++-- src/script/printProjects.js | 4 +- ...eUser.test.js => deactivateMember.test.js} | 35 +---- ...s => findMemberProjectEvaluations.test.js} | 10 +- .../actions/__tests__/findMemberUsers.test.js | 82 ++++++++++++ .../actions/__tests__/findUsers.test.js | 84 ------------ ...{getUser.test.js => getMemberUser.test.js} | 8 +- .../__tests__/lockSurveyForMember.test.js | 45 +++++++ .../__tests__/retroSurveyLockUnlock.test.js | 80 ----------- .../__tests__/unlockSurveyForMember.test.js | 46 +++++++ .../assertMembersCurrentCycleInState.js | 4 +- src/server/actions/compileSurveyData.js | 40 +++--- src/server/actions/deactivateIDMUser.js | 13 ++ src/server/actions/deactivateMember.js | 15 +++ src/server/actions/deactivateUser.js | 30 ----- .../actions/findActiveMembersForPhase.js | 4 +- .../actions/findActiveMembersInChapter.js | 6 +- .../findActiveVotingMembersInChapter.js | 4 +- ...ons.js => findMemberProjectEvaluations.js} | 16 +-- .../{findUsers.js => findMemberUsers.js} | 8 +- src/server/actions/formProjects.js | 10 +- src/server/actions/getMemberInfo.js | 9 -- .../actions/{getUser.js => getMemberUser.js} | 8 +- .../actions/getSurveyCompletedByMember.js | 10 ++ src/server/actions/getUsersByHandles.js | 9 -- src/server/actions/importProject.js | 4 +- src/server/actions/initializeProject.js | 11 +- src/server/actions/lockSurveyForMember.js | 12 ++ .../{mergeUsers.js => mergeMembers.js} | 20 ++- src/server/actions/retroSurveyLockUnlock.js | 30 ----- .../sendCycleReflectionAnnouncements.js | 8 +- .../actions/sendProjectWelcomeMessages.js | 16 +-- .../actions/sendRetroCompletedNotification.js | 34 ++--- src/server/actions/unlockSurveyForMember.js | 10 ++ .../{updateUser.js => updateMember.js} | 6 +- .../commands/__tests__/project.test.js | 12 +- src/server/cliCommand/commands/cycle.js | 20 +-- src/server/cliCommand/index.js | 4 +- ...{deactivateUser.js => deactivateMember.js} | 14 +- .../mutations/lockRetroSurveyForUser.js | 31 ----- .../graphql/mutations/lockSurveyForMember.js | 27 ++++ .../mutations/reassignMembersToChapter.js | 4 +- .../mutations/unlockRetroSurveyForUser.js | 31 ----- .../mutations/unlockSurveyForMember.js | 27 ++++ src/server/graphql/mutations/updateMember.js | 22 +++ src/server/graphql/mutations/updateUser.js | 22 --- .../queries/__tests__/findMembers.test.js | 59 ++++---- .../queries/__tests__/findUsers.test.js | 50 ------- .../__tests__/getCycleVotingResults.test.js | 4 +- .../{getUser.test.js => getMember.test.js} | 10 +- .../queries/__tests__/getMemberById.test.js | 45 ------- ...mmary.test.js => getMemberSummary.test.js} | 24 ++-- .../__tests__/getProjectSummary.test.js | 8 +- src/server/graphql/queries/findMembers.js | 25 ++-- src/server/graphql/queries/findUsers.js | 21 --- .../queries/{getUser.js => getMember.js} | 8 +- src/server/graphql/queries/getMemberById.js | 23 ---- ...{getUserSummary.js => getMemberSummary.js} | 10 +- .../queries/getProjectSummaryForMember.js | 4 +- src/server/graphql/resolvers/index.js | 85 ++++++------ .../schemas/{InputUser.js => InputMember.js} | 6 +- src/server/graphql/schemas/Member.js | 35 +++++ ...aluation.js => MemberProjectEvaluation.js} | 8 +- .../graphql/schemas/MemberProjectSummary.js | 15 +++ src/server/graphql/schemas/MemberSummary.js | 17 +++ src/server/graphql/schemas/PhaseSummary.js | 4 +- src/server/graphql/schemas/Project.js | 4 +- .../graphql/schemas/ProjectMemberSummary.js | 17 +++ src/server/graphql/schemas/ProjectSummary.js | 7 +- .../graphql/schemas/ProjectUserSummary.js | 17 --- src/server/graphql/schemas/Survey.js | 2 +- src/server/graphql/schemas/User.js | 18 --- src/server/graphql/schemas/UserProfile.js | 35 ----- .../graphql/schemas/UserProjectSummary.js | 15 --- src/server/graphql/schemas/UserSummary.js | 17 --- src/server/graphql/schemas/Vote.js | 14 +- .../graphql/schemas/VotingPoolResults.js | 4 +- src/server/reports/memberRetroFeedback.js | 32 ++--- src/server/reports/projectTeams.js | 9 +- src/server/reports/util.js | 21 --- .../findProjectByNameForMember.test.js | 6 +- ....test.js => findProjectsForMember.test.js} | 14 +- ...ctsForUser.js => findProjectsForMember.js} | 4 +- .../queries/findVotingResultsForCycle.js | 2 +- ...tTeammatesTheyGaveGoodFeedbackAppraiser.js | 4 +- ...matesTheyGaveGoodFeedbackAppraiser.test.js | 14 +- .../projectFormationService/lib/pool.js | 4 +- src/server/util/index.js | 1 + .../workers/__tests__/projectCreated.test.js | 4 +- .../workers/__tests__/surveySubmitted.test.js | 4 +- .../workers/__tests__/userCreated.test.js | 5 +- src/server/workers/memberPhaseChanged.js | 8 +- src/server/workers/surveySubmitted.js | 6 +- src/test/helpers/fixtures.js | 27 ++-- 155 files changed, 1572 insertions(+), 1941 deletions(-) create mode 100644 src/common/actions/member.js create mode 100644 src/common/actions/queries/deactivateMember.js delete mode 100644 src/common/actions/queries/findUsers.js rename src/common/actions/queries/{getUserSummary.js => getMemberSummary.js} (83%) delete mode 100644 src/common/actions/queries/lockSurvey.js create mode 100644 src/common/actions/queries/lockSurveyForMember.js delete mode 100644 src/common/actions/queries/unlockSurvey.js create mode 100644 src/common/actions/queries/unlockSurveyForMember.js rename src/common/actions/queries/{updateUser.js => updateMember.js} (50%) delete mode 100644 src/common/actions/user.js rename src/common/components/{UserDetail => MemberDetail}/index.jsx (56%) rename src/common/components/{UserDetail => MemberDetail}/index.scss (96%) rename src/common/components/{UserDetail => MemberDetail}/theme.scss (100%) rename src/common/components/{UserForm => MemberForm}/index.jsx (89%) rename src/common/components/{UserForm => MemberForm}/index.scss (100%) create mode 100644 src/common/components/MemberList/index.jsx rename src/common/components/{UserProjectSummary => MemberProjectSummary}/index.jsx (86%) rename src/common/components/{UserProjectSummary => MemberProjectSummary}/index.scss (96%) rename src/common/components/{ProjectUserSummary => ProjectMemberSummary}/index.jsx (62%) rename src/common/components/{ProjectUserSummary => ProjectMemberSummary}/index.scss (97%) delete mode 100644 src/common/components/UserList/index.jsx rename src/common/components/__tests__/{UserList.test.js => MemberList.test.js} (71%) create mode 100644 src/common/containers/MemberDetail/index.jsx rename src/common/containers/{UserForm => MemberForm}/index.jsx (69%) rename src/common/containers/{UserList => MemberList}/index.css (100%) rename src/common/containers/{UserList => MemberList}/index.jsx (52%) delete mode 100644 src/common/containers/UserDetail/index.jsx create mode 100644 src/common/reducers/memberSummaries.js delete mode 100644 src/common/reducers/userSummaries.js delete mode 100644 src/common/reducers/users.js rename src/common/validations/{user.js => member.js} (65%) rename src/server/actions/__tests__/{deactivateUser.test.js => deactivateMember.test.js} (53%) rename src/server/actions/__tests__/{findUserProjectEvaluations.test.js => findMemberProjectEvaluations.test.js} (87%) create mode 100644 src/server/actions/__tests__/findMemberUsers.test.js delete mode 100644 src/server/actions/__tests__/findUsers.test.js rename src/server/actions/__tests__/{getUser.test.js => getMemberUser.test.js} (82%) create mode 100644 src/server/actions/__tests__/lockSurveyForMember.test.js delete mode 100644 src/server/actions/__tests__/retroSurveyLockUnlock.test.js create mode 100644 src/server/actions/__tests__/unlockSurveyForMember.test.js create mode 100644 src/server/actions/deactivateIDMUser.js create mode 100644 src/server/actions/deactivateMember.js delete mode 100644 src/server/actions/deactivateUser.js rename src/server/actions/{findUserProjectEvaluations.js => findMemberProjectEvaluations.js} (77%) rename src/server/actions/{findUsers.js => findMemberUsers.js} (67%) delete mode 100644 src/server/actions/getMemberInfo.js rename src/server/actions/{getUser.js => getMemberUser.js} (63%) create mode 100644 src/server/actions/getSurveyCompletedByMember.js delete mode 100644 src/server/actions/getUsersByHandles.js create mode 100644 src/server/actions/lockSurveyForMember.js rename src/server/actions/{mergeUsers.js => mergeMembers.js} (59%) delete mode 100644 src/server/actions/retroSurveyLockUnlock.js create mode 100644 src/server/actions/unlockSurveyForMember.js rename src/server/actions/{updateUser.js => updateMember.js} (53%) rename src/server/graphql/mutations/{deactivateUser.js => deactivateMember.js} (58%) delete mode 100644 src/server/graphql/mutations/lockRetroSurveyForUser.js create mode 100644 src/server/graphql/mutations/lockSurveyForMember.js delete mode 100644 src/server/graphql/mutations/unlockRetroSurveyForUser.js create mode 100644 src/server/graphql/mutations/unlockSurveyForMember.js create mode 100644 src/server/graphql/mutations/updateMember.js delete mode 100644 src/server/graphql/mutations/updateUser.js delete mode 100644 src/server/graphql/queries/__tests__/findUsers.test.js rename src/server/graphql/queries/__tests__/{getUser.test.js => getMember.test.js} (84%) delete mode 100644 src/server/graphql/queries/__tests__/getMemberById.test.js rename src/server/graphql/queries/__tests__/{getUserSummary.test.js => getMemberSummary.test.js} (70%) delete mode 100644 src/server/graphql/queries/findUsers.js rename src/server/graphql/queries/{getUser.js => getMember.js} (54%) delete mode 100644 src/server/graphql/queries/getMemberById.js rename src/server/graphql/queries/{getUserSummary.js => getMemberSummary.js} (60%) rename src/server/graphql/schemas/{InputUser.js => InputMember.js} (79%) create mode 100644 src/server/graphql/schemas/Member.js rename src/server/graphql/schemas/{UserProjectEvaluation.js => MemberProjectEvaluation.js} (67%) create mode 100644 src/server/graphql/schemas/MemberProjectSummary.js create mode 100644 src/server/graphql/schemas/MemberSummary.js create mode 100644 src/server/graphql/schemas/ProjectMemberSummary.js delete mode 100644 src/server/graphql/schemas/ProjectUserSummary.js delete mode 100644 src/server/graphql/schemas/User.js delete mode 100644 src/server/graphql/schemas/UserProfile.js delete mode 100644 src/server/graphql/schemas/UserProjectSummary.js delete mode 100644 src/server/graphql/schemas/UserSummary.js rename src/server/services/dataService/queries/__tests__/{findProjectsForUser.test.js => findProjectsForMember.test.js} (58%) rename src/server/services/dataService/queries/{findProjectsForUser.js => findProjectsForMember.js} (51%) diff --git a/src/common/actions/cycle.js b/src/common/actions/cycle.js index 01ca8425..7fab71cc 100644 --- a/src/common/actions/cycle.js +++ b/src/common/actions/cycle.js @@ -1,7 +1,8 @@ import {normalize} from 'normalizr' +import socketCluster from 'socketcluster-client' import {flatten, getGraphQLFetcher} from 'src/common/util' -import {findUsers} from './user' +import {findMembers} from './member' import types from './types' import schemas from './schemas' import queries from './queries' @@ -25,33 +26,48 @@ export function getCycleVotingResults(options = {}) { return dispatch(action) .then(() => { - return options.withUsers ? _findUsersForCycleVotingResults(dispatch, getState) : null + return options.withMembers ? _findMembersForCycleVotingResults(dispatch, getState) : null }) } } -export function receivedCycleVotingResults(cycleVotingResults) { +export function subscribeToCycleVotingResults(cycleId) { return (dispatch, getState) => { - dispatch(_receivedCycleVotingResultsWithoutLoadingUsers(cycleVotingResults)) - return _findUsersForCycleVotingResults(dispatch, getState) + if (cycleId) { + console.log(`subscribing to voting results for cycle ${cycleId} ...`) + this.socket = socketCluster.connect() + this.socket.on('connect', () => console.log('... socket connected')) + this.socket.on('disconnect', () => console.log('socket disconnected, will try to reconnect socket ...')) + this.socket.on('connectAbort', () => null) + this.socket.on('error', error => console.warn(error.message)) + const cycleVotingResultsChannel = this.socket.subscribe(`cycleVotingResults-${cycleId}`) + cycleVotingResultsChannel.watch(cycleVotingResults => { + const response = normalize(cycleVotingResults, schemas.cycleVotingResults) + dispatch({type: types.RECEIVED_CYCLE_VOTING_RESULTS, response}) + dispatch(_findMembersForCycleVotingResults(dispatch, getState)) + }) + } } } -function _findUsersForCycleVotingResults(dispatch, getState) { - // we'll only load users from IDM that haven't already been loaded, because +export function unsubscribeFromCycleVotingResults(cycleId) { + if (this.socket && cycleId) { + console.log(`unsubscribing from voting results for cycle ${cycleId} ...`) + this.socket.unwatch(`cycleVotingResults-${cycleId}`) + this.socket.unsubscribe(`cycleVotingResults-${cycleId}`) + } +} + +function _findMembersForCycleVotingResults(dispatch, getState) { + // we'll only load members that haven't already been loaded, because // it's unlikely that their names, handles, and avatars have changed since // the last load, and those are the attributes we use in the voting results const { cycleVotingResults: {cycleVotingResults: {CURRENT: cycleVotingResults}}, - users: {users}, + members: {members}, } = getState() - const memberIds = flatten(cycleVotingResults.pools.map(_ => _.users.map(_ => _.id))) - const userIdsToLoad = memberIds.filter(memberId => !users[memberId]) - return userIdsToLoad.length === 0 ? null : dispatch(findUsers(userIdsToLoad)) -} - -function _receivedCycleVotingResultsWithoutLoadingUsers(cycleVotingResults) { - const response = normalize(cycleVotingResults, schemas.cycleVotingResults) - return {type: types.RECEIVED_CYCLE_VOTING_RESULTS, response} + const memberIds = flatten(cycleVotingResults.pools.map(pool => pool.members.map(m => m.id))) + const memberIdsToLoad = memberIds.filter(memberId => !members[memberId]) + return memberIdsToLoad.length === 0 ? null : dispatch(findMembers(memberIdsToLoad)) } diff --git a/src/common/actions/member.js b/src/common/actions/member.js new file mode 100644 index 00000000..2768c717 --- /dev/null +++ b/src/common/actions/member.js @@ -0,0 +1,100 @@ +import {normalize} from 'normalizr' + +import {getGraphQLFetcher} from 'src/common/util' +import types from './types' +import schemas from './schemas' +import queries from './queries' + +export function deactivateMember(memberId) { + return { + types: [ + types.DEACTIVATE_MEMBER_REQUEST, + types.DEACTIVATE_MEMBER_SUCCESS, + types.DEACTIVATE_MEMBER_FAILURE, + ], + shouldCallAPI: () => true, + callAPI: (dispatch, getState) => { + const query = queries.deactivateMember(memberId) + return getGraphQLFetcher(dispatch, getState().auth)(query) + .then(graphQLResponse => graphQLResponse.data.deactivateMember) + }, + redirect: _redirectMember, + payload: {}, + } +} + +export function findMembers() { + return { + types: [ + types.FIND_MEMBERS_REQUEST, + types.FIND_MEMBERS_SUCCESS, + types.FIND_MEMBERS_FAILURE, + ], + shouldCallAPI: () => true, + callAPI: (dispatch, getState) => { + const query = queries.findMembers() + return getGraphQLFetcher(dispatch, getState().auth)(query) + .then(graphQLResponse => graphQLResponse.data.findMembers) + .then(members => normalize(members, schemas.members)) + }, + payload: {}, + } +} + +export function getMemberSummary(identifier) { + return { + types: [ + types.GET_MEMBER_SUMMARY_REQUEST, + types.GET_MEMBER_SUMMARY_SUCCESS, + types.GET_MEMBER_SUMMARY_FAILURE, + ], + shouldCallAPI: () => true, + callAPI: (dispatch, getState) => { + const query = queries.getMemberSummary(identifier) + return getGraphQLFetcher(dispatch, getState().auth)(query) + .then(graphQLResponse => graphQLResponse.data.getMemberSummary) + }, + payload: {}, + } +} + +export function reassignMembersToChapter(memberIds, chapterId) { + return { + types: [ + types.REASSIGN_MEMBERS_TO_CHAPTER_REQUEST, + types.REASSIGN_MEMBERS_TO_CHAPTER_SUCCESS, + types.REASSIGN_MEMBERS_TO_CHAPTER_FAILURE, + ], + shouldCallAPI: () => true, + callAPI: (dispatch, getState) => { + const mutation = queries.reassignMembersToChapter(memberIds, chapterId) + return getGraphQLFetcher(dispatch, getState().auth)(mutation) + .then(graphQLResponse => graphQLResponse.data.reassignMembersToChapter) + .then(members => normalize(members, schemas.members)) + }, + redirect: '/members', + payload: {memberIds, chapterId}, + } +} + +export function updateMember(values) { + return { + types: [ + types.UPDATE_MEMBER_REQUEST, + types.UPDATE_MEMBER_SUCCESS, + types.UPDATE_MEMBER_FAILURE, + ], + shouldCallAPI: () => true, + callAPI: (dispatch, getState) => { + const mutation = queries.updateMember(values) + return getGraphQLFetcher(dispatch, getState().auth)(mutation) + .then(graphQLResponse => graphQLResponse.data.updateMember) + }, + redirect: _redirectMember, + payload: {}, + } +} + +function _redirectMember(member) { + return member && member.handle ? `/members/${member.handle}` : '/members' +} diff --git a/src/common/actions/project.js b/src/common/actions/project.js index bb016c8b..5fb03a74 100644 --- a/src/common/actions/project.js +++ b/src/common/actions/project.js @@ -103,40 +103,6 @@ export function importProject(values) { } } -export function unlockSurvey(memberId, projectId) { - return { - types: [ - types.UNLOCK_SURVEY_REQUEST, - types.UNLOCK_SURVEY_SUCCESS, - types.UNLOCK_SURVEY_FAILURE, - ], - shouldCallAPI: () => true, - callAPI: (dispatch, getState) => { - const query = queries.unlockSurvey(memberId, projectId) - return getGraphQLFetcher(dispatch, getState().auth)(query) - .then(graphQLResponse => graphQLResponse.data.unlockRetroSurveyForUser) - }, - payload: {memberId, projectId}, - } -} - -export function lockSurvey(memberId, projectId) { - return { - types: [ - types.LOCK_SURVEY_REQUEST, - types.LOCK_SURVEY_SUCCESS, - types.LOCK_SURVEY_FAILURE, - ], - shouldCallAPI: () => true, - callAPI: (dispatch, getState) => { - const query = queries.lockSurvey(memberId, projectId) - return getGraphQLFetcher(dispatch, getState().auth)(query) - .then(graphQLResponse => graphQLResponse.data.lockRetroSurveyForUser) - }, - payload: {memberId, projectId}, - } -} - export function deleteProject(identifier) { return { types: [ diff --git a/src/common/actions/queries/deactivateMember.js b/src/common/actions/queries/deactivateMember.js new file mode 100644 index 00000000..67ca5c9c --- /dev/null +++ b/src/common/actions/queries/deactivateMember.js @@ -0,0 +1,14 @@ +export default function deactivateMember(memberId) { + return { + variables: {memberId}, + query: ` + mutation ($memberId: ID!) { + deactivateMember(identifier: $memberId) { + id + active + handle + } + } + `, + } +} diff --git a/src/common/actions/queries/findMembers.js b/src/common/actions/queries/findMembers.js index ebd73303..3d0ce14e 100644 --- a/src/common/actions/queries/findMembers.js +++ b/src/common/actions/queries/findMembers.js @@ -1,10 +1,11 @@ -export default function findMembers() { +export default function findMembers(identifiers) { return { - variables: {}, + variables: {identifiers}, query: ` - query { - findMembers { + query ($identifiers: [String]) { + findMembers(identifiers: $identifiers) { id + chapterId chapter { id name @@ -12,6 +13,19 @@ export default function findMembers() { timezone inviteCodes } + phone + email + name + handle + avatarUrl + profileUrl + timezone + active + phaseId + phase { + id + number + } createdAt updatedAt } diff --git a/src/common/actions/queries/findUsers.js b/src/common/actions/queries/findUsers.js deleted file mode 100644 index fa71d9f1..00000000 --- a/src/common/actions/queries/findUsers.js +++ /dev/null @@ -1,27 +0,0 @@ -export default function findUsers(identifiers) { - return { - variables: {identifiers}, - query: ` - query ($identifiers: [String]) { - findUsers(identifiers: $identifiers) { - id - chapterId - phone - email - name - handle - avatarUrl - profileUrl - timezone - active - phase { - id - number - } - createdAt - updatedAt - } - } - `, - } -} diff --git a/src/common/actions/queries/getCycleVotingResults.js b/src/common/actions/queries/getCycleVotingResults.js index 91825511..01a8faba 100644 --- a/src/common/actions/queries/getCycleVotingResults.js +++ b/src/common/actions/queries/getCycleVotingResults.js @@ -21,7 +21,7 @@ export default function getCycleVotingResults() { id name voterMemberIds - users { + members { id } phase { diff --git a/src/common/actions/queries/getUserSummary.js b/src/common/actions/queries/getMemberSummary.js similarity index 83% rename from src/common/actions/queries/getUserSummary.js rename to src/common/actions/queries/getMemberSummary.js index 2930a4df..55ae2882 100644 --- a/src/common/actions/queries/getUserSummary.js +++ b/src/common/actions/queries/getMemberSummary.js @@ -1,11 +1,11 @@ import {FEEDBACK_TYPE_DESCRIPTORS} from 'src/common/models/feedbackType' -export default function getUserSummary(identifier) { +export default function Member(identifier) { return { variables: {identifier}, query: `query ($identifier: String!) { - getUserSummary(identifier: $identifier) { - user { + Member(identifier: $identifier) { + member { id phone email @@ -26,7 +26,7 @@ export default function getUserSummary(identifier) { name } } - userProjectSummaries { + memberProjectSummaries { project { id name @@ -44,7 +44,7 @@ export default function getUserSummary(identifier) { phase } } - userProjectEvaluations { + memberProjectEvaluations { ${FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK} } } diff --git a/src/common/actions/queries/getProjectSummary.js b/src/common/actions/queries/getProjectSummary.js index 55454792..8ef5bf67 100644 --- a/src/common/actions/queries/getProjectSummary.js +++ b/src/common/actions/queries/getProjectSummary.js @@ -34,18 +34,18 @@ export default function getProjectSummary(identifier) { number } } - projectUserSummaries { - user { + projectMemberSummaries { + member { id name handle avatarUrl } - userProjectEvaluations { + memberProjectEvaluations { ${FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK} } - userRetrospectiveComplete - userRetrospectiveUnlocked + memberRetrospectiveComplete + memberRetrospectiveUnlocked } } }`, diff --git a/src/common/actions/queries/index.js b/src/common/actions/queries/index.js index 709bc61d..613512a2 100644 --- a/src/common/actions/queries/index.js +++ b/src/common/actions/queries/index.js @@ -1,5 +1,6 @@ export default { createInviteCode: require('./createInviteCode'), + deleteProject: require('./deleteProject'), findChapters: require('./findChapters'), findPhases: require('./findPhases'), findPhaseSummaries: require('./findPhaseSummaries'), @@ -7,20 +8,18 @@ export default { findProjects: require('./findProjects'), findProjectsForCycle: require('./findProjectsForCycle'), findRetrospectiveSurveys: require('./findRetrospectiveSurveys'), - findUsers: require('./findUsers'), getChapter: require('./getChapter'), getCycleVotingResults: require('./getCycleVotingResults'), getProject: require('./getProject'), getProjectSummary: require('./getProjectSummary'), getRetrospectiveSurvey: require('./getRetrospectiveSurvey'), - getUserSummary: require('./getUserSummary'), + getMemberSummary: require('./getMemberSummary'), importProject: require('./importProject'), + lockSurveyForMember: require('./lockSurveyForMember'), reassignMembersToChapter: require('./reassignMembersToChapter'), saveChapter: require('./saveChapter'), saveRetrospectiveSurveyResponses: require('./saveRetrospectiveSurveyResponses'), submitSurvey: require('./submitSurvey'), - unlockSurvey: require('./unlockSurvey'), - lockSurvey: require('./lockSurvey'), - deleteProject: require('./deleteProject'), - updateUser: require('./updateUser'), + unlockSurveyForMember: require('./unlockSurveyForMember'), + updateMember: require('./updateMember'), } diff --git a/src/common/actions/queries/lockSurvey.js b/src/common/actions/queries/lockSurvey.js deleted file mode 100644 index ea56a21e..00000000 --- a/src/common/actions/queries/lockSurvey.js +++ /dev/null @@ -1,50 +0,0 @@ -import {FEEDBACK_TYPE_DESCRIPTORS} from 'src/common/models/feedbackType' - -export default function lockSurvey(memberId, projectId) { - return { - variables: {memberId, projectId}, - query: ` - mutation($memberId: ID!, $projectId: ID!) { - lockRetroSurveyForUser(memberId: $memberId, projectId: $projectId) { - project { - id - name - artifactURL - retrospectiveSurveyId - createdAt - updatedAt - goal { - number - title - phase - } - chapter { - id - name - } - cycle { - id - cycleNumber - state - startTimestamp - endTimestamp - } - } - projectUserSummaries { - user { - id - name - handle - avatarUrl - } - userProjectEvaluations { - ${FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK} - } - userRetrospectiveComplete - userRetrospectiveUnlocked - } - } - } - `, - } -} diff --git a/src/common/actions/queries/lockSurveyForMember.js b/src/common/actions/queries/lockSurveyForMember.js new file mode 100644 index 00000000..ff11d90a --- /dev/null +++ b/src/common/actions/queries/lockSurveyForMember.js @@ -0,0 +1,14 @@ +export default function lockSurveyForMember(surveyId, memberId) { + return { + variables: {surveyId, memberId}, + query: ` + mutation($surveyId: ID!, $memberId: ID!) { + lockSurveyForMember(surveyId: $surveyId, memberId: $memberId) { + id + completedBy + unlockedFor + } + } + `, + } +} diff --git a/src/common/actions/queries/unlockSurvey.js b/src/common/actions/queries/unlockSurvey.js deleted file mode 100644 index 985549d1..00000000 --- a/src/common/actions/queries/unlockSurvey.js +++ /dev/null @@ -1,50 +0,0 @@ -import {FEEDBACK_TYPE_DESCRIPTORS} from 'src/common/models/feedbackType' - -export default function unlockSurvey(memberId, projectId) { - return { - variables: {memberId, projectId}, - query: ` - mutation($memberId: ID!, $projectId: ID!) { - unlockRetroSurveyForUser(memberId: $memberId, projectId: $projectId) { - project { - id - name - artifactURL - retrospectiveSurveyId - createdAt - updatedAt - goal { - number - title - phase - } - chapter { - id - name - } - cycle { - id - cycleNumber - state - startTimestamp - endTimestamp - } - } - projectUserSummaries { - user { - id - name - handle - avatarUrl - } - userProjectEvaluations { - ${FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK} - } - userRetrospectiveComplete - userRetrospectiveUnlocked - } - } - } - `, - } -} diff --git a/src/common/actions/queries/unlockSurveyForMember.js b/src/common/actions/queries/unlockSurveyForMember.js new file mode 100644 index 00000000..aa89e0f3 --- /dev/null +++ b/src/common/actions/queries/unlockSurveyForMember.js @@ -0,0 +1,14 @@ +export default function unlockSurveyForMember(surveyId, memberId) { + return { + variables: {surveyId, memberId}, + query: ` + mutation($surveyId: ID!, $memberId: ID!) { + unlockSurveyForMember(surveyId: $surveyId, memberId: $memberId) { + id + completedBy + unlockedFor + } + } + `, + } +} diff --git a/src/common/actions/queries/updateUser.js b/src/common/actions/queries/updateMember.js similarity index 50% rename from src/common/actions/queries/updateUser.js rename to src/common/actions/queries/updateMember.js index dbc20554..b46376d3 100644 --- a/src/common/actions/queries/updateUser.js +++ b/src/common/actions/queries/updateMember.js @@ -1,9 +1,9 @@ -export default function updateUser(values) { +export default function updateMember(values) { return { variables: {values}, query: ` - mutation ($values: InputUser!) { - updateUser(values: $values) { + mutation ($values: InputMember!) { + updateMember(values: $values) { id handle updatedAt diff --git a/src/common/actions/schemas/index.js b/src/common/actions/schemas/index.js index 2f2af459..cfdd474f 100644 --- a/src/common/actions/schemas/index.js +++ b/src/common/actions/schemas/index.js @@ -6,13 +6,11 @@ const cycleVotingResults = new Schema('cycleVotingResults') const phase = new Schema('phases') const member = new Schema('members') const project = new Schema('projects') -const user = new Schema('users') const chapters = arrayOf(chapter) const phases = arrayOf(phase) const members = arrayOf(member) const projects = arrayOf(project) -const users = arrayOf(user) cycle.define({chapter}) cycleVotingResults.define({cycle}) @@ -29,6 +27,4 @@ export default { members, project, projects, - user, - users, } diff --git a/src/common/actions/survey.js b/src/common/actions/survey.js index 87bfb885..51c84a92 100644 --- a/src/common/actions/survey.js +++ b/src/common/actions/survey.js @@ -77,6 +77,40 @@ export function submitSurvey(surveyId) { } } +export function unlockSurveyForMember(surveyId, memberId) { + return { + types: [ + types.UNLOCK_SURVEY_REQUEST, + types.UNLOCK_SURVEY_SUCCESS, + types.UNLOCK_SURVEY_FAILURE, + ], + shouldCallAPI: () => true, + callAPI: (dispatch, getState) => { + const query = queries.unlockSurveyForMember(surveyId, memberId) + return getGraphQLFetcher(dispatch, getState().auth)(query) + .then(graphQLResponse => graphQLResponse.data.unlockSurveyForMember) + }, + payload: {surveyId, memberId}, + } +} + +export function lockSurveyForMember(surveyId, memberId) { + return { + types: [ + types.LOCK_SURVEY_REQUEST, + types.LOCK_SURVEY_SUCCESS, + types.LOCK_SURVEY_FAILURE, + ], + shouldCallAPI: () => true, + callAPI: (dispatch, getState) => { + const query = queries.lockSurveyForMember(surveyId, memberId) + return getGraphQLFetcher(dispatch, getState().auth)(query) + .then(graphQLResponse => graphQLResponse.data.lockSurveyForMember) + }, + payload: {surveyId, memberId}, + } +} + export function setSurveyGroup(groupIndex) { return {type: types.SET_SURVEY_GROUP, groupIndex} } diff --git a/src/common/actions/types/index.js b/src/common/actions/types/index.js index e01542a8..036953f3 100644 --- a/src/common/actions/types/index.js +++ b/src/common/actions/types/index.js @@ -29,10 +29,6 @@ export default keyMirror({ FIND_PROJECTS_SUCCESS: null, FIND_PROJECTS_FAILURE: null, - FIND_USERS_REQUEST: null, - FIND_USERS_SUCCESS: null, - FIND_USERS_FAILURE: null, - GET_CHAPTER_REQUEST: null, GET_CHAPTER_SUCCESS: null, GET_CHAPTER_FAILURE: null, @@ -57,13 +53,13 @@ export default keyMirror({ GET_SURVEY_SUCCESS: null, GET_SURVEY_FAILURE: null, - GET_USER_SUMMARY_REQUEST: null, - GET_USER_SUMMARY_SUCCESS: null, - GET_USER_SUMMARY_FAILURE: null, + GET_MEMBER_SUMMARY_REQUEST: null, + GET_MEMBER_SUMMARY_SUCCESS: null, + GET_MEMBER_SUMMARY_FAILURE: null, - DEACTIVATE_USER_REQUEST: null, - DEACTIVATE_USER_SUCCESS: null, - DEACTIVATE_USER_FAILURE: null, + DEACTIVATE_MEMBER_REQUEST: null, + DEACTIVATE_MEMBER_SUCCESS: null, + DEACTIVATE_MEMBER_FAILURE: null, IMPORT_PROJECT_REQUEST: null, IMPORT_PROJECT_SUCCESS: null, @@ -73,9 +69,9 @@ export default keyMirror({ REASSIGN_MEMBERS_TO_CHAPTER_SUCCESS: null, REASSIGN_MEMBERS_TO_CHAPTER_FAILURE: null, - UPDATE_USER_REQUEST: null, - UPDATE_USER_SUCCESS: null, - UPDATE_USER_FAILURE: null, + UPDATE_MEMBER_REQUEST: null, + UPDATE_MEMBER_SUCCESS: null, + UPDATE_MEMBER_FAILURE: null, RECEIVED_CYCLE_VOTING_RESULTS: null, diff --git a/src/common/actions/user.js b/src/common/actions/user.js deleted file mode 100644 index 3863c9b5..00000000 --- a/src/common/actions/user.js +++ /dev/null @@ -1,126 +0,0 @@ -import {normalize} from 'normalizr' - -import {getGraphQLFetcher} from 'src/common/util' -import types from './types' -import schemas from './schemas' -import queries from './queries' - -export function findUsers(identifiers) { - return { - types: [ - types.FIND_USERS_REQUEST, - types.FIND_USERS_SUCCESS, - types.FIND_USERS_FAILURE, - ], - shouldCallAPI: () => true, - callAPI: (dispatch, getState) => { - const query = queries.findUsers(identifiers) - return getGraphQLFetcher(dispatch, getState().auth)(query) - .then(graphQLResponse => graphQLResponse.data.findUsers) - .then(users => normalize(users, schemas.users)) - }, - payload: {}, - } -} - -export function deactivateUser(id) { - return { - types: [ - types.DEACTIVATE_USER_REQUEST, - types.DEACTIVATE_USER_SUCCESS, - types.DEACTIVATE_USER_FAILURE, - ], - shouldCallAPI: () => true, - callAPI: (dispatch, getState) => { - const query = { - query: 'mutation ($memberId: ID!) { deactivateUser(identifier: $memberId) { id active handle } }', - variables: {memberId: id}, - } - return getGraphQLFetcher(dispatch, getState().auth)(query) - .then(graphQLResponse => graphQLResponse.data.deactivateUser) - }, - payload: {}, - } -} - -export function findMembers(options = {}) { - return (dispatch, getState) => { - const action = { - types: [ - types.FIND_MEMBERS_REQUEST, - types.FIND_MEMBERS_SUCCESS, - types.FIND_MEMBERS_FAILURE, - ], - shouldCallAPI: () => true, - callAPI: (dispatch, getState) => { - const query = queries.findMembers() - return getGraphQLFetcher(dispatch, getState().auth)(query) - .then(graphQLResponse => graphQLResponse.data.findMembers) - .then(members => normalize(members, schemas.members)) - }, - payload: {}, - } - - return dispatch(action) - .then(() => { - if (options.withUsers) { - const memberIds = Object.keys(getState().members.members) - return dispatch(findUsers(memberIds)) - } - }) - } -} - -export function getUserSummary(identifier) { - return { - types: [ - types.GET_USER_SUMMARY_REQUEST, - types.GET_USER_SUMMARY_SUCCESS, - types.GET_USER_SUMMARY_FAILURE, - ], - shouldCallAPI: () => true, - callAPI: (dispatch, getState) => { - const query = queries.getUserSummary(identifier) - return getGraphQLFetcher(dispatch, getState().auth)(query) - .then(graphQLResponse => graphQLResponse.data.getUserSummary) - }, - payload: {}, - } -} - -export function reassignMembersToChapter(memberIds, chapterId) { - return { - types: [ - types.REASSIGN_MEMBERS_TO_CHAPTER_REQUEST, - types.REASSIGN_MEMBERS_TO_CHAPTER_SUCCESS, - types.REASSIGN_MEMBERS_TO_CHAPTER_FAILURE, - ], - shouldCallAPI: () => true, - callAPI: (dispatch, getState) => { - const mutation = queries.reassignMembersToChapter(memberIds, chapterId) - return getGraphQLFetcher(dispatch, getState().auth)(mutation) - .then(graphQLResponse => graphQLResponse.data.reassignMembersToChapter) - .then(members => normalize(members, schemas.members)) - }, - redirect: '/members', - payload: {memberIds, chapterId}, - } -} - -export function updateUser(values) { - return { - types: [ - types.UPDATE_USER_REQUEST, - types.UPDATE_USER_SUCCESS, - types.UPDATE_USER_FAILURE, - ], - shouldCallAPI: () => true, - callAPI: (dispatch, getState) => { - const mutation = queries.updateUser(values) - return getGraphQLFetcher(dispatch, getState().auth)(mutation) - .then(graphQLResponse => graphQLResponse.data.updateUser) - }, - redirect: user => (user && user.handle ? `/users/${user.handle}` : '/users'), - payload: {}, - } -} diff --git a/src/common/components/CycleVotingResults/index.jsx b/src/common/components/CycleVotingResults/index.jsx index da9b07ae..cc686063 100644 --- a/src/common/components/CycleVotingResults/index.jsx +++ b/src/common/components/CycleVotingResults/index.jsx @@ -8,7 +8,7 @@ import VotingPoolResults, {poolPropType} from 'src/common/components/VotingPoolR import styles from './index.css' const currentUserIsInPool = (currentUser, pool) => { - return pool.users.some(user => user.id === currentUser.id) + return pool.members.some(member => member.id === currentUser.id) } export default class CycleVotingResults extends Component { diff --git a/src/common/components/UserDetail/index.jsx b/src/common/components/MemberDetail/index.jsx similarity index 56% rename from src/common/components/UserDetail/index.jsx rename to src/common/components/MemberDetail/index.jsx index da41739d..582be521 100644 --- a/src/common/components/UserDetail/index.jsx +++ b/src/common/components/MemberDetail/index.jsx @@ -7,88 +7,84 @@ import Helmet from 'react-helmet' import ConfirmationDialog from 'src/common/components/ConfirmationDialog' import WrappedButton from 'src/common/components/WrappedButton' import ContentSidebar from 'src/common/components/ContentSidebar' -import UserProjectSummary from 'src/common/components/UserProjectSummary' +import MemberProjectSummary from 'src/common/components/MemberProjectSummary' import {Flex} from 'src/common/components/Layout' import {formatPartialPhoneNumber} from 'src/common/util/format' -import {userCan} from 'src/common/util' import styles from './index.scss' import theme from './theme.scss' -class UserDetail extends Component { +class MemberDetail extends Component { constructor(props) { super(props) this.renderSidebar = this.renderSidebar.bind(this) this.renderTabs = this.renderTabs.bind(this) this.renderProjects = this.renderProjects.bind(this) this.handleChangeTab = this.handleChangeTab.bind(this) - this.showDeactivateUserDialog = this.showDeactivateUserDialog.bind(this) - this.hideDeactivateUserDialog = this.hideDeactivateUserDialog.bind(this) - this.handleDeactivateUser = this.handleDeactivateUser.bind(this) - this.state = {tabIndex: 0, showingDeactivateUserDialog: false} + this.showDeactivateDialog = this.showDeactivateDialog.bind(this) + this.handleClickCancelDeactivate = this.handleClickCancelDeactivate.bind(this) + this.handleClickConfirmDeactivate = this.handleClickConfirmDeactivate.bind(this) + this.state = {tabIndex: 0, showDeactivateDialog: false} } - showDeactivateUserDialog() { - this.setState({showingDeactivateUserDialog: true}) + handleClickDeactivate() { + this.setState({showDeactivateDialog: true}) } - hideDeactivateUserDialog() { - this.setState({showingDeactivateUserDialog: false}) + handleClickCancelDeactivate() { + this.setState({showDeactivateDialog: false}) } handleChangeTab(tabIndex) { this.setState({tabIndex}) } - handleDeactivateUser() { - const {onDeactivateUser} = this.props - onDeactivateUser(this.props.user.id) - this.setState({showingDeactivateUserDialog: false}) + handleClickConfirmDeactivate() { + const {onClickDeactivate} = this.props + onClickDeactivate(this.props.member.id) + this.setState({showDeactivateDialog: false}) } renderSidebar() { - const {user, currentUser, defaultAvatarURL, onClickEdit} = this.props + const {showEdit, showDeactivate, member, defaultAvatarURL, onClickEdit} = this.props - const emailLink = user.email ? ( - - {user.email} + const emailLink = member.email ? ( + + {member.email} ) : null - const phoneLink = user.phone ? ( - - {formatPartialPhoneNumber(user.phone)} + const phoneLink = member.phone ? ( + + {formatPartialPhoneNumber(member.phone)} ) : null - const canBeDeactivated = user.active && userCan(currentUser, 'deactivateUser') - const canBeEdited = userCan(currentUser, 'updateUser') - - const deactivateUserDialog = canBeDeactivated ? ( + const deactivateDialog = showDeactivate ? ( - Are you sure you want to deactivate {user.name} ({user.handle})? + Are you sure you want to deactivate {member.name} ({member.handle})? ) : null - const deactivateUserButton = canBeDeactivated ? ( + const deactivateButton = showDeactivate ? ( ) :
- const editUserButton = canBeEdited ? ( + const editButton = showEdit ? (
@@ -124,27 +120,27 @@ class UserDetail extends Component {
{emailLink || '--'}
{phoneLink || '--'}
 
-
{user.chapter ? user.chapter.name : '--'}
-
{user.phase ? user.phase.number : '--'}
-
{moment(user.createdAt).format('MMM DD, YYYY') || '--'}
-
{moment(user.updatedAt).format('MMM DD, YYYY') || '--'}
+
{member.chapter ? member.chapter.name : '--'}
+
{member.phase ? member.phase.number : '--'}
+
{moment(member.createdAt).format('MMM DD, YYYY') || '--'}
+
{moment(member.updatedAt).format('MMM DD, YYYY') || '--'}
- {deactivateUserButton} - {editUserButton} + {deactivateButton} + {editButton}
- {deactivateUserDialog} + {deactivateDialog} ) } renderProjects() { - const {userProjectSummaries} = this.props - const projectSummaries = userProjectSummaries.map((summary, i) => - + const {memberProjectSummaries} = this.props + const projectSummaries = memberProjectSummaries.map((summary, i) => + ) return (
@@ -174,14 +170,14 @@ class UserDetail extends Component { } render() { - if (!this.props.user) { + if (!this.props.member) { return null } return ( - + - {this.props.user.handle} + {this.props.member.handle} {this.renderSidebar()} @@ -194,8 +190,9 @@ class UserDetail extends Component { } } -UserDetail.propTypes = { - user: PropTypes.shape({ +MemberDetail.propTypes = { + defaultAvatarURL: PropTypes.string, + member: PropTypes.shape({ id: PropTypes.string, handle: PropTypes.string, name: PropTypes.string, @@ -204,15 +201,12 @@ UserDetail.propTypes = { name: PropTypes.string, }), }), - currentUser: PropTypes.shape({ - id: PropTypes.string, - roles: PropTypes.array, - }), - userProjectSummaries: PropTypes.array, + memberProjectSummaries: PropTypes.array, navigate: PropTypes.func.isRequired, - onDeactivateUser: PropTypes.func.isRequired, + showEdit: PropTypes.bool, + showDeactivate: PropTypes.bool, onClickEdit: PropTypes.func.isRequired, - defaultAvatarURL: PropTypes.string, + onClickDeactivate: PropTypes.func.isRequired, } -export default UserDetail +export default MemberDetail diff --git a/src/common/components/UserDetail/index.scss b/src/common/components/MemberDetail/index.scss similarity index 96% rename from src/common/components/UserDetail/index.scss rename to src/common/components/MemberDetail/index.scss index 2a7568ab..07642669 100644 --- a/src/common/components/UserDetail/index.scss +++ b/src/common/components/MemberDetail/index.scss @@ -1,6 +1,6 @@ @import "../../theme.scss"; -.userDetail { +.memberDetail { margin-top: 20px; } diff --git a/src/common/components/UserDetail/theme.scss b/src/common/components/MemberDetail/theme.scss similarity index 100% rename from src/common/components/UserDetail/theme.scss rename to src/common/components/MemberDetail/theme.scss diff --git a/src/common/components/UserForm/index.jsx b/src/common/components/MemberForm/index.jsx similarity index 89% rename from src/common/components/UserForm/index.jsx rename to src/common/components/MemberForm/index.jsx index d2a66cca..505b958f 100644 --- a/src/common/components/UserForm/index.jsx +++ b/src/common/components/MemberForm/index.jsx @@ -11,7 +11,7 @@ import {FORM_TYPES, renderDropdown} from 'src/common/util/form' import styles from './index.scss' -class UserForm extends Component { +class MemberForm extends Component { handleBlurField(event) { // blur event handling is causing redux state to not be // properly updated with selected options. @@ -27,7 +27,7 @@ class UserForm extends Component { invalid, formType, onSave, - user, + member, phaseOptions } = this.props @@ -36,7 +36,7 @@ class UserForm extends Component { } const submitDisabled = Boolean(pristine || submitting || invalid) - const title = `Edit User: ${user.handle}` + const title = `Edit Member: ${member.handle}` return ( @@ -44,7 +44,7 @@ class UserForm extends Component { {title} -
+ 0 ? ( + + ) : ( +
No members yet.
+ ) + + return ( +
+ + Members + + + {content} +
+ ) + } +} + +MemberList.propTypes = { + tableModel: PropTypes.object, + tableSource: PropTypes.array, +} diff --git a/src/common/components/UserProjectSummary/index.jsx b/src/common/components/MemberProjectSummary/index.jsx similarity index 86% rename from src/common/components/UserProjectSummary/index.jsx rename to src/common/components/MemberProjectSummary/index.jsx index 92f0734c..8675404b 100644 --- a/src/common/components/UserProjectSummary/index.jsx +++ b/src/common/components/MemberProjectSummary/index.jsx @@ -8,7 +8,7 @@ import {renderGoalAsString} from 'src/common/models/goal' import styles from './index.scss' -export default class UserProjectSummary extends Component { +export default class MemberProjectSummary extends Component { constructor(props) { super(props) this.renderSummary = this.renderSummary.bind(this) @@ -37,8 +37,8 @@ export default class UserProjectSummary extends Component { } renderFeedback() { - const {userProjectEvaluations} = this.props - const evaluationItems = (userProjectEvaluations || []).filter(evaluation => ( + const {memberProjectEvaluations} = this.props + const evaluationItems = (memberProjectEvaluations || []).filter(evaluation => ( evaluation[FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK] )).map((evaluation, i) => (
@@ -54,7 +54,7 @@ export default class UserProjectSummary extends Component { render() { return ( - + {this.renderSummary()} {this.renderFeedback()} @@ -62,7 +62,7 @@ export default class UserProjectSummary extends Component { } } -UserProjectSummary.propTypes = { +MemberProjectSummary.propTypes = { project: PropTypes.shape({ name: PropTypes.string, cycle: PropTypes.shape({ @@ -77,7 +77,7 @@ UserProjectSummary.propTypes = { title: PropTypes.string, }), }), - userProjectEvaluations: PropTypes.arrayOf(PropTypes.shape({ + memberProjectEvaluations: PropTypes.arrayOf(PropTypes.shape({ [FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK]: PropTypes.string, })), } diff --git a/src/common/components/UserProjectSummary/index.scss b/src/common/components/MemberProjectSummary/index.scss similarity index 96% rename from src/common/components/UserProjectSummary/index.scss rename to src/common/components/MemberProjectSummary/index.scss index 280a896e..098d000a 100644 --- a/src/common/components/UserProjectSummary/index.scss +++ b/src/common/components/MemberProjectSummary/index.scss @@ -1,6 +1,6 @@ @import "node_modules/react-toolbox/lib/_colors"; -.userProjectSummary { +.memberProjectSummary { font-size: 1.3rem; line-height: 2rem; padding: 20px 0; diff --git a/src/common/components/ProjectDetail/index.jsx b/src/common/components/ProjectDetail/index.jsx index 800ad3a9..a232cf00 100644 --- a/src/common/components/ProjectDetail/index.jsx +++ b/src/common/components/ProjectDetail/index.jsx @@ -8,7 +8,7 @@ import {Tab, Tabs} from 'react-toolbox' import Helmet from 'react-helmet' import ContentHeader from 'src/common/components/ContentHeader' -import ProjectUserSummary from 'src/common/components/ProjectUserSummary' +import ProjectMemberSummary from 'src/common/components/ProjectMemberSummary' import {Flex} from 'src/common/components/Layout' import {safeUrl, urlParts, objectValuesAreAllNull} from 'src/common/util' import {renderGoalAsString} from 'src/common/models/goal' @@ -24,14 +24,12 @@ class ProjectDetail extends Component { this.renderHeader = this.renderHeader.bind(this) this.renderDetails = this.renderDetails.bind(this) this.renderTabs = this.renderTabs.bind(this) - this.renderUserSummaries = this.renderUserSummaries.bind(this) + this.renderMemberSummaries = this.renderMemberSummaries.bind(this) } renderHeader() { - const {project: {name, goal, retrospectiveSurveyId}, allowEdit, onClickEdit} = this.props - - const editDisabled = Boolean(retrospectiveSurveyId) - const editButton = allowEdit ? ( + const {project: {name, goal, showEdit}, editDisabled, onClickEdit} = this.props + const editButton = showEdit ? ( { - const {user} = projectUserSummary + const memberList = projectMemberSummaries.map((projectMemberSummary, index) => { + const {member} = projectMemberSummary const prefix = index > 0 ? ', ' : '' return ( - - {`${prefix}${user.handle}`} + + {`${prefix}${member.handle}`} ) }) @@ -114,48 +112,49 @@ class ProjectDetail extends Component { ) } - renderUserSummaries() { - const {projectUserSummaries, project, unlockMemberSurvey, lockMemberSurvey, isLockingOrUnlocking} = this.props - - const memberSummaries = (projectUserSummaries || []) - .map((userSummary, i) => { - const onUnlockMemberSurvey = () => unlockMemberSurvey(userSummary.user.id, project.id) - const onLockMemberSurvey = () => lockMemberSurvey(userSummary.user.id, project.id) + renderMemberSummaries() { + const {projectMemberSummaries, onClickUnlockRetro, onClickLockRetro, lockDisabled} = this.props + const memberSummaries = (projectMemberSummaries || []) + .map((memberSummary, i) => { + const handleClickUnlockRetro = () => onClickUnlockRetro(memberSummary.member) + const handleClickLockRetro = () => onClickLockRetro(memberSummary.member) return ( - ) }) return (
- {memberSummaries.length > 0 ? - memberSummaries : -
No project members.
- } + {memberSummaries.length > 0 ? memberSummaries :
No project members.
}
) } renderTabs() { - const {projectUserSummaries} = this.props - const hasProjectUserSummaries = (projectUserSummaries || []).length > 0 - const hasViewableProjectUserSummaries = hasProjectUserSummaries && projectUserSummaries.every(({userProjectEvaluations}) => { - return !objectValuesAreAllNull({userProjectEvaluations}) + const {projectMemberSummaries} = this.props + const hasMemberSummaries = (projectMemberSummaries || []).length > 0 + const hasViewableMemberSummaries = hasMemberSummaries && projectMemberSummaries.every(({memberProjectEvaluations}) => { + return !objectValuesAreAllNull({memberProjectEvaluations}) }) - return hasViewableProjectUserSummaries ? ( + return hasViewableMemberSummaries ? (
-
{this.renderUserSummaries()}
+ +
+ {this.renderMemberSummaries()} +
+
) :
@@ -200,14 +199,13 @@ ProjectDetail.propTypes = { phase: PropTypes.shape({ number: PropTypes.number, }), - retrospectiveSurveyId: PropTypes.string, }), - projectUserSummaries: PropTypes.array, - isLockingOrUnlocking: PropTypes.bool, - allowEdit: PropTypes.bool, + projectMemberSummaries: PropTypes.array, + editDisabled: PropTypes.bool, + lockDisabled: PropTypes.bool, onClickEdit: PropTypes.func, - unlockMemberSurvey: PropTypes.func, - lockMemberSurvey: PropTypes.func, + onClickUnlockRetro: PropTypes.func, + onClickLockRetro: PropTypes.func, } export default ProjectDetail diff --git a/src/common/components/ProjectUserSummary/index.jsx b/src/common/components/ProjectMemberSummary/index.jsx similarity index 62% rename from src/common/components/ProjectUserSummary/index.jsx rename to src/common/components/ProjectMemberSummary/index.jsx index 43b9cddd..740f5600 100644 --- a/src/common/components/ProjectUserSummary/index.jsx +++ b/src/common/components/ProjectMemberSummary/index.jsx @@ -8,10 +8,9 @@ import {ProgressBar} from 'react-toolbox/lib/progress_bar' import styles from './index.scss' -export default class ProjectUserSummary extends Component { +export default class ProjectMemberSummary extends Component { constructor(props) { super(props) - this.renderSummary = this.renderSummary.bind(this) this.renderFeedback = this.renderFeedback.bind(this) this.handleUnlockSurveyClick = this.handleUnlockSurveyClick.bind(this) @@ -19,68 +18,57 @@ export default class ProjectUserSummary extends Component { } handleUnlockSurveyClick(e) { - const { - onUnlockMemberSurvey, - } = this.props - e.preventDefault() - onUnlockMemberSurvey() + this.props.onClickUnlockRetro() } handleLockSurveyClick(e) { - const { - onLockMemberSurvey, - } = this.props - e.preventDefault() - onLockMemberSurvey() + this.props.onClickLockRetro() } renderLockButton(onClick, icon, actionName) { - const {isLockingOrUnlocking} = this.props - + const {lockDisabled} = this.props const button = - const widget = isLockingOrUnlocking ? ( + const widget = lockDisabled ? ( {'Please wait ...'} ) : ( - {button}{`${actionName} Survey`} + {button}{`${actionName} Survey`} ) return
{widget}
} renderSurveyLockUnlock() { const { - userRetrospectiveComplete, - userRetrospectiveUnlocked, + memberRetrospectiveComplete, + memberRetrospectiveUnlocked, } = this.props - if (userRetrospectiveComplete) { - return userRetrospectiveUnlocked ? + if (memberRetrospectiveComplete) { + return memberRetrospectiveUnlocked ? this.renderLockButton(this.handleLockSurveyClick, 'lock_outline', 'Lock') : this.renderLockButton(this.handleUnlockSurveyClick, 'lock_open', 'Unlock') } } renderSummary() { - const {user} = this.props - - const userProfilePath = `/users/${user.handle}` - + const {member} = this.props + const profilePath = `/members/${member.handle}` return (
- - + +
- - {user.handle} + + {member.handle}
-
{user.name}
+
{member.name}
{this.renderSurveyLockUnlock()}
@@ -89,8 +77,8 @@ export default class ProjectUserSummary extends Component { } renderFeedback() { - const {userProjectEvaluations} = this.props - const evaluationItems = (userProjectEvaluations || []).filter(evaluation => ( + const {memberProjectEvaluations} = this.props + const evaluationItems = (memberProjectEvaluations || []).filter(evaluation => ( evaluation[FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK] )).map((evaluation, i) => (
@@ -110,7 +98,7 @@ export default class ProjectUserSummary extends Component { render() { return ( - + {this.renderSummary()} {this.renderFeedback()} @@ -118,18 +106,19 @@ export default class ProjectUserSummary extends Component { } } -ProjectUserSummary.propTypes = { - user: PropTypes.shape({ +ProjectMemberSummary.propTypes = { + member: PropTypes.shape({ name: PropTypes.string, handle: PropTypes.string, avatarUrl: PropTypes.string, }), - userProjectEvaluations: PropTypes.arrayOf(PropTypes.shape({ + memberProjectEvaluations: PropTypes.arrayOf(PropTypes.shape({ [FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK]: PropTypes.string, })), - isLockingOrUnlocking: PropTypes.bool, - onUnlockMemberSurvey: PropTypes.func.isRequired, - onLockMemberSurvey: PropTypes.func.isRequired, - userRetrospectiveComplete: PropTypes.bool, - userRetrospectiveUnlocked: PropTypes.bool, + lockDisabled: PropTypes.bool, + lockIsBusy: PropTypes.bool, + onClickUnlockRetro: PropTypes.func.isRequired, + onClickLockRetro: PropTypes.func.isRequired, + memberRetrospectiveComplete: PropTypes.bool, + memberRetrospectiveUnlocked: PropTypes.bool, } diff --git a/src/common/components/ProjectUserSummary/index.scss b/src/common/components/ProjectMemberSummary/index.scss similarity index 97% rename from src/common/components/ProjectUserSummary/index.scss rename to src/common/components/ProjectMemberSummary/index.scss index 651acf4c..12392eb9 100644 --- a/src/common/components/ProjectUserSummary/index.scss +++ b/src/common/components/ProjectMemberSummary/index.scss @@ -1,6 +1,6 @@ @import "node_modules/react-toolbox/lib/_colors"; -.projectUserSummary { +.projectMemberSummary { font-size: 1.3rem; line-height: 2rem; padding: 20px 0; diff --git a/src/common/components/UserList/index.jsx b/src/common/components/UserList/index.jsx deleted file mode 100644 index 8f2c95ff..00000000 --- a/src/common/components/UserList/index.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import Helmet from 'react-helmet' - -import ContentHeader from 'src/common/components/ContentHeader' -import ContentTable from 'src/common/components/ContentTable' - -export default class UserList extends Component { - render() { - const {userData, userModel} = this.props - const content = userData.length > 0 ? ( - - ) : ( -
No user yet.
- ) - - return ( -
- - Users - - - {content} -
- ) - } -} - -UserList.propTypes = { - userModel: PropTypes.object, - userData: PropTypes.array, -} diff --git a/src/common/components/VotingPoolResults/CandidateGoal.jsx b/src/common/components/VotingPoolResults/CandidateGoal.jsx index df1834b3..fa936224 100644 --- a/src/common/components/VotingPoolResults/CandidateGoal.jsx +++ b/src/common/components/VotingPoolResults/CandidateGoal.jsx @@ -122,6 +122,5 @@ CandidateGoal.propTypes = { email: PropTypes.string.isRequired, name: PropTypes.string.isRequired, }), - candidateGoal: candidateGoalPropType.isRequired, } diff --git a/src/common/components/VotingPoolResults/__tests__/index.test.js b/src/common/components/VotingPoolResults/__tests__/index.test.js index 9cf6d102..76c5c7ea 100644 --- a/src/common/components/VotingPoolResults/__tests__/index.test.js +++ b/src/common/components/VotingPoolResults/__tests__/index.test.js @@ -29,13 +29,13 @@ describe(testContext(__filename), function () { currentUser, cycle, pool: { + candidateGoals, + voterMemberIds, + members: users, name: 'Turquoise', phase: { number: 1 }, - candidateGoals, - users, - voterMemberIds, votingIsStillOpen: true, }, isOnlyPool: true, @@ -85,7 +85,7 @@ describe(testContext(__filename), function () { it('does not render voter ratio unless it is available', function () { const props = this.getProps() - props.pool.users = [] + props.pool.members = [] props.pool.voterMemberIds = [] const root = shallow(React.createElement(VotingPoolResults, props)) diff --git a/src/common/components/VotingPoolResults/index.jsx b/src/common/components/VotingPoolResults/index.jsx index 02f4d156..c79bcf9a 100644 --- a/src/common/components/VotingPoolResults/index.jsx +++ b/src/common/components/VotingPoolResults/index.jsx @@ -26,18 +26,18 @@ export default class VotingPoolResults extends Component { } renderProgress() { - const {pool: {users, voterMemberIds}} = this.props + const {pool: {members, voterMemberIds}} = this.props let progressBar = '' let progressMsg = '' - if (users && users.length > 0 && voterMemberIds) { - const percentageComplete = Math.floor(voterMemberIds.length / users.length * 100) + if (members && members.length > 0 && voterMemberIds) { + const percentageComplete = Math.floor(voterMemberIds.length / members.length * 100) progressBar = ( ) progressMsg = ( - {voterMemberIds.length}/{users.length} + {voterMemberIds.length}/{members.length} members have voted. ) @@ -60,11 +60,11 @@ export default class VotingPoolResults extends Component { } renderUserGrid() { - const {pool: {users, voterMemberIds}} = this.props + const {pool: {members, voterMemberIds}} = this.props return ( - + ) } @@ -156,7 +156,7 @@ export const poolPropType = PropTypes.shape({ number: PropTypes.number.isRequired, }), candidateGoals: PropTypes.arrayOf(candidateGoalPropType), - users: PropTypes.arrayOf(PropTypes.shape({ + members: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired, handle: PropTypes.string.isRequired, name: PropTypes.string.isRequired, diff --git a/src/common/components/__tests__/CycleVotingResults.test.js b/src/common/components/__tests__/CycleVotingResults.test.js index d52f8adf..9267f1ac 100644 --- a/src/common/components/__tests__/CycleVotingResults.test.js +++ b/src/common/components/__tests__/CycleVotingResults.test.js @@ -25,7 +25,7 @@ describe(testContext(__filename), function () { number: 1 }, candidateGoals: [], - users: [], + members: [], voterMemberIds: [], votingIsStillOpen: true, }, { @@ -34,7 +34,7 @@ describe(testContext(__filename), function () { number: 2 }, candidateGoals: [], - users: [], + members: [], voterMemberIds: [], votingIsStillOpen: false, }], @@ -102,9 +102,9 @@ describe(testContext(__filename), function () { describe('collapsed / expanded', function () { beforeEach(async function () { - const myPoolUsers = await factory.buildMany('user', 3) - myPoolUsers.push(this.currentUser) - const myPoolVoterMemberIds = [this.currentUser.id, myPoolUsers[0].id] + const myPoolMembers = await factory.buildMany('user', 3) + myPoolMembers.push(this.currentUser) + const myPoolVoterMemberIds = [this.currentUser.id, myPoolMembers[0].id] const myPoolMemberGoalRank = await factory.build('memberGoalRank', {memberId: this.currentUser.id}) this.myPoolCandidateGoals = new Array(3).fill({ memberGoalRanks: [myPoolMemberGoalRank], @@ -120,7 +120,7 @@ describe(testContext(__filename), function () { number: 2 }, candidateGoals: this.myPoolCandidateGoals, - users: myPoolUsers, + members: myPoolMembers, voterMemberIds: myPoolVoterMemberIds, votingIsStillOpen: true, } @@ -142,7 +142,7 @@ describe(testContext(__filename), function () { number: 1 }, candidateGoals: this.otherCandidateGoals, - users: otherUsers, + members: otherUsers, voterMemberIds: otherVoterMemberIds, votingIsStillOpen: true, } diff --git a/src/common/components/__tests__/UserList.test.js b/src/common/components/__tests__/MemberList.test.js similarity index 71% rename from src/common/components/__tests__/UserList.test.js rename to src/common/components/__tests__/MemberList.test.js index 2275c75f..c3104b41 100644 --- a/src/common/components/__tests__/UserList.test.js +++ b/src/common/components/__tests__/MemberList.test.js @@ -5,11 +5,11 @@ import React from 'react' import {mount} from 'enzyme' -import UserList from 'src/common/components/UserList' +import MemberList from 'src/common/components/MemberList' describe(testContext(__filename), function () { before(function () { - const userModel = { + const tableModel = { handle: {type: String}, name: {type: String}, chapterName: {title: 'Chapter', type: String}, @@ -17,7 +17,7 @@ describe(testContext(__filename), function () { email: {type: String}, active: {type: String}, } - const userData = [ + const tableSource = [ { name: 'Ivanna Lerntokode', handle: 'ivannalerntokode', @@ -36,17 +36,16 @@ describe(testContext(__filename), function () { } ] this.getProps = customProps => { - return Object.assign({userData, userModel}, customProps || {}) + return Object.assign({tableSource, tableModel}, customProps || {}) } }) describe('rendering', function () { - it('renders all the users', function () { + it('renders all the members', function () { const props = this.getProps() - const root = mount(React.createElement(UserList, props)) - const userRows = root.find('TableRow') - - expect(userRows.length).to.equal(props.userData.length) + const root = mount(React.createElement(MemberList, props)) + const memberRows = root.find('TableRow') + expect(memberRows.length).to.equal(props.tableSource.length) }) }) }) diff --git a/src/common/components/__tests__/ProjectList.test.js b/src/common/components/__tests__/ProjectList.test.js index c792adf8..8a55dba3 100644 --- a/src/common/components/__tests__/ProjectList.test.js +++ b/src/common/components/__tests__/ProjectList.test.js @@ -29,7 +29,7 @@ describe(testContext(__filename), function () { return shallow(React.createElement(ProjectList, props)) } - function buildProjectProps({projects, cycle, phase, users}) { + function buildProjectProps({projects, cycle, phase, members}) { const projectData = [] projects.forEach(function (project) { const {cycleNumber, state} = cycle @@ -40,7 +40,7 @@ describe(testContext(__filename), function () { name: project.name, phaseNumber: phase.number, goalTitle: project.goal.title, - memberHandles: users.map(u => u.handle).join(', '), + memberHandles: members.map(u => u.handle).join(', '), }) }) return projectData @@ -63,7 +63,7 @@ describe(testContext(__filename), function () { cycle: this.cycle, phase: this.phase, projects: this.projects, - users: this.users, + members: this.users, }), } return customProps ? Object.assign({}, baseProps, customProps) : baseProps diff --git a/src/common/containers/App/index.jsx b/src/common/containers/App/index.jsx index e254e36e..b72d9093 100644 --- a/src/common/containers/App/index.jsx +++ b/src/common/containers/App/index.jsx @@ -29,9 +29,9 @@ const navItems = [ path: '/projects', }, { - label: 'Users', - permission: 'listUsers', - path: '/users', + label: 'Members', + permission: 'listMembers', + path: '/members', }, { label: 'Chapters', diff --git a/src/common/containers/CycleVotingResults/index.jsx b/src/common/containers/CycleVotingResults/index.jsx index 4537d152..a9b9c03e 100644 --- a/src/common/containers/CycleVotingResults/index.jsx +++ b/src/common/containers/CycleVotingResults/index.jsx @@ -1,11 +1,14 @@ import React, {Component, PropTypes} from 'react' import {push} from 'react-router-redux' import {connect} from 'react-redux' -import socketCluster from 'socketcluster-client' import {showLoad, hideLoad} from 'src/common/actions/app' import CycleVotingResults, {cycleVotingResultsPropType} from 'src/common/components/CycleVotingResults' -import {getCycleVotingResults, receivedCycleVotingResults} from 'src/common/actions/cycle' +import { + getCycleVotingResults, + subscribeToCycleVotingResults, + unsubscribeFromCycleVotingResults, +} from 'src/common/actions/cycle' class CycleVotingResultsContainer extends Component { constructor() { @@ -16,11 +19,11 @@ class CycleVotingResultsContainer extends Component { componentDidMount() { this.props.showLoad() this.props.fetchData() - this.subscribeToCycleVotingResults(this.currentCycleId()) + this.subscribeToCycleVotingResults(this.getCurrentCycleId()) } componentWillUnmount() { - this.unsubscribeFromCycleVotingResults(this.currentCycleId()) + this.unsubscribeFromCycleVotingResults(this.getCurrentCycleId()) } componentWillReceiveProps(nextProps) { @@ -28,9 +31,8 @@ class CycleVotingResultsContainer extends Component { this.props.hideLoad() } - const newCycleId = nextProps.cycle && nextProps.cycle.id - const oldCycleId = this.currentCycleId() - + const oldCycleId = this.props.cycle ? this.props.cycle.id : null + const newCycleId = nextProps.cycle ? nextProps.cycle.id : null if (!newCycleId) { this.unsubscribeFromCycleVotingResults(oldCycleId) } else if (oldCycleId !== newCycleId) { @@ -38,31 +40,8 @@ class CycleVotingResultsContainer extends Component { } } - currentCycleId() { - return this.props.cycle && this.props.cycle.id - } - - subscribeToCycleVotingResults(cycleId) { - if (cycleId) { - console.log(`subscribing to voting results for cycle ${cycleId} ...`) - this.socket = socketCluster.connect() - this.socket.on('connect', () => console.log('... socket connected')) - this.socket.on('disconnect', () => console.log('socket disconnected, will try to reconnect socket ...')) - this.socket.on('connectAbort', () => null) - this.socket.on('error', error => console.warn(error.message)) - const cycleVotingResultsChannel = this.socket.subscribe(`cycleVotingResults-${cycleId}`) - cycleVotingResultsChannel.watch(cycleVotingResults => { - this.props.receivedCycleVotingResults(cycleVotingResults) - }) - } - } - - unsubscribeFromCycleVotingResults(cycleId) { - if (this.socket && cycleId) { - console.log(`unsubscribing from voting results for cycle ${cycleId} ...`) - this.socket.unwatch(`cycleVotingResults-${cycleId}`) - this.socket.unsubscribe(`cycleVotingResults-${cycleId}`) - } + getCurrentCycleId() { + return this.props.cycle ? this.props.cycle.id : null } handleClose() { @@ -94,13 +73,12 @@ CycleVotingResultsContainer.propTypes = Object.assign({}, cycleVotingResultsProp CycleVotingResultsContainer.fetchData = fetchData function fetchData(dispatch) { - dispatch(getCycleVotingResults({withUsers: true})) + dispatch(getCycleVotingResults({withMembers: true})) } -function addUserDataToPools(pools, allUsers) { +function addMembersToPools(pools, members) { pools.forEach(pool => { - const userDatas = pool.users.map(({id}) => allUsers[id]).filter(user => Boolean(user)) - pool.users = userDatas + pool.members = pool.members.map(({id}) => members[id]).filter(m => m) }) } @@ -110,10 +88,10 @@ function mapStateToProps(state) { auth: {currentUser}, cycles, chapters, - users, + members, cycleVotingResults: cvResults, } = state - const isBusy = cycles.isBusy || chapters.isBusy || cvResults.isBusy || users.isBusy + const isBusy = cycles.isBusy || chapters.isBusy || cvResults.isBusy || members.isBusy // this part of the state is a singleton, which is why this looks weird const cycleVotingResults = cvResults.cycleVotingResults.CURRENT let cycle @@ -123,7 +101,7 @@ function mapStateToProps(state) { cycle = cycles.cycles[cycleVotingResults.cycle] chapter = cycle ? chapters.chapters[cycle.chapter] : null pools = cycleVotingResults.pools.map(pool => ({...pool})) // deep copy so we don't mutate state - addUserDataToPools(pools, users.users) + addMembersToPools(pools, members.members) } return { @@ -143,7 +121,8 @@ function mapDispatchToProps(dispatch) { navigate: path => dispatch(push(path)), showLoad: () => dispatch(showLoad()), hideLoad: () => dispatch(hideLoad()), - receivedCycleVotingResults: results => dispatch(receivedCycleVotingResults(results)), + subscribeToCycleVotingResults: cycleId => dispatch(subscribeToCycleVotingResults(cycleId)), + unsubscribeFromCycleVotingResults: cycleId => dispatch(unsubscribeFromCycleVotingResults(cycleId)), } } diff --git a/src/common/containers/MemberDetail/index.jsx b/src/common/containers/MemberDetail/index.jsx new file mode 100644 index 00000000..f789a578 --- /dev/null +++ b/src/common/containers/MemberDetail/index.jsx @@ -0,0 +1,117 @@ +import React, {Component, PropTypes} from 'react' +import {push} from 'react-router-redux' +import {connect} from 'react-redux' + +import {showLoad, hideLoad} from 'src/common/actions/app' +import {getMemberSummary, deactivateMember} from 'src/common/actions/member' +import MemberDetail from 'src/common/components/MemberDetail' +import {userCan} from 'src/common/util' + +class MemberDetailContainer extends Component { + constructor(props) { + super(props) + this.handleSelectProjectRow = this.handleSelectProjectRow.bind(this) + this.handleClickEdit = this.handleClickEdit.bind(this) + } + + componentDidMount() { + const {showLoad, fetchData} = this.props + showLoad() + fetchData() + } + + componentWillReceiveProps(nextProps) { + if (!nextProps.isBusy && nextProps.loading) { + this.props.hideLoad() + } + } + + handleSelectProjectRow(rowIndex) { + const {memberProjectSummaries} = this.props || [] + const project = memberProjectSummaries[rowIndex] || {} + const projectDetailUrl = `/projects/${project.name}` + this.props.navigate(projectDetailUrl) + } + + handleClickEdit() { + if (!this.props.member) { + return + } + this.props.navigate(`/members/${this.props.member.handle}/edit`) + } + + render() { + return this.props.member ? ( + + ) : null + } +} + +MemberDetailContainer.propTypes = { + member: PropTypes.object, + memberProjectSummaries: PropTypes.array, + isBusy: PropTypes.bool.isRequired, + loading: PropTypes.bool.isRequired, + fetchData: PropTypes.func.isRequired, + navigate: PropTypes.func.isRequired, + showLoad: PropTypes.func.isRequired, + hideLoad: PropTypes.func.isRequired, + showEdit: PropTypes.func, + showDeactivate: PropTypes.func, + onClickDeactivate: PropTypes.func.isRequired, + defaultAvatarURL: PropTypes.string, +} + +MemberDetailContainer.fetchData = fetchData + +function fetchData(dispatch, props) { + dispatch(getMemberSummary(props.params.identifier)) +} + +function mapStateToProps(state, ownProps) { + const {identifier} = ownProps.params + const {memberSummaries, auth: {currentUser}} = state + const {memberSummaries: memberSummariesByMemberId} = memberSummaries + + const memberSummary = Object.values(memberSummariesByMemberId).find(memberSummary => { + return memberSummary.member && ( + memberSummary.member.handle.toLowerCase() === identifier.toLowerCase() || + memberSummary.member.id === identifier + ) + }) || {} + + const showEdit = userCan(currentUser, 'updateMember') + const showDeactivate = userCan(currentUser, 'deactivateMember') && memberSummary && memberSummary.member.active + + return { + showEdit, + showDeactivate, + member: memberSummary.memberSummary, + memberProjectSummaries: memberSummary.memberProjectSummaries, + isBusy: memberSummaries.isBusy, + loading: state.app.showLoading, + defaultAvatarURL: process.env.LOGO_FULL_URL, + } +} + +function mapDispatchToProps(dispatch, props) { + return { + fetchData: () => fetchData(dispatch, props), + navigate: path => dispatch(push(path)), + showLoad: () => dispatch(showLoad()), + hideLoad: () => dispatch(hideLoad()), + onClickDeactivate: id => dispatch(deactivateMember(id)), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(MemberDetailContainer) diff --git a/src/common/containers/UserForm/index.jsx b/src/common/containers/MemberForm/index.jsx similarity index 69% rename from src/common/containers/UserForm/index.jsx rename to src/common/containers/MemberForm/index.jsx index 7c442170..4b644617 100644 --- a/src/common/containers/UserForm/index.jsx +++ b/src/common/containers/MemberForm/index.jsx @@ -3,16 +3,16 @@ import {connect} from 'react-redux' import {reduxForm} from 'redux-form' import {showLoad, hideLoad} from 'src/common/actions/app' -import {findUsers, updateUser} from 'src/common/actions/user' +import {getMember, updateMember} from 'src/common/actions/member' import {findPhases} from 'src/common/actions/phase' -import {userSchema, asyncValidate} from 'src/common/validations' -import UserForm from 'src/common/components/UserForm' +import {memberSchema, asyncValidate} from 'src/common/validations' +import MemberForm from 'src/common/components/MemberForm' import {findAny} from 'src/common/util' import {FORM_TYPES} from 'src/common/util/form' -const FORM_NAME = 'user' +const FORM_NAME = 'member' -class UserFormContainer extends Component { +class MemberFormContainer extends Component { componentDidMount() { this.props.showLoad() this.props.fetchData() @@ -28,11 +28,11 @@ class UserFormContainer extends Component { if (!this.props.project && this.props.isBusy) { return null } - return + return } } -UserFormContainer.propTypes = { +MemberFormContainer.propTypes = { project: PropTypes.object, isBusy: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired, @@ -41,25 +41,25 @@ UserFormContainer.propTypes = { hideLoad: PropTypes.func.isRequired, } -UserFormContainer.fetchData = fetchData +MemberFormContainer.fetchData = fetchData function fetchData(dispatch, props) { if (props.params.identifier) { - dispatch(findUsers([props.params.identifier])) + dispatch(getMember(props.params.identifier)) dispatch(findPhases()) } } function handleSubmit(dispatch) { return values => { - return dispatch(updateUser(values)) + return dispatch(updateMember(values)) } } function mapStateToProps(state, props) { const {identifier} = props.params - const {app, users, phases} = state - const user = findAny(users.users, identifier, ['id', 'handle']) + const {app, members, phases} = state + const member = findAny(members.members, identifier, ['id', 'handle']) const sortedPhases = Object.values(phases.phases).sort((p1, p2) => p1.number - p2.number) const sortedPhaseOptions = [ @@ -67,21 +67,21 @@ function mapStateToProps(state, props) { ] let formType = FORM_TYPES.UPDATE - if (identifier && !user && !users.isBusy) { + if (identifier && !member && !members.isBusy) { formType = FORM_TYPES.NOT_FOUND } - const initialValues = user ? { - id: user.id, - phaseNumber: user.phase ? user.phase.number : null, + const initialValues = member ? { + id: member.id, + phaseNumber: member.phase ? member.phase.number : null, } : null return { - isBusy: users.isBusy, + isBusy: member.isBusy, loading: app.showLoading, phaseOptions: sortedPhaseOptions, formType, - user, + member, initialValues, } } @@ -103,10 +103,10 @@ const formOptions = { form: FORM_NAME, enableReinitialize: true, asyncBlurFields: ['phaseNumber'], - asyncValidate: asyncValidate(userSchema, {abortEarly: false}), + asyncValidate: asyncValidate(memberSchema, {abortEarly: false}), } export default connect( mapStateToProps, mapDispatchToProps, -)(reduxForm(formOptions)(UserFormContainer)) +)(reduxForm(formOptions)(MemberFormContainer)) diff --git a/src/common/containers/UserList/index.css b/src/common/containers/MemberList/index.css similarity index 100% rename from src/common/containers/UserList/index.css rename to src/common/containers/MemberList/index.css diff --git a/src/common/containers/UserList/index.jsx b/src/common/containers/MemberList/index.jsx similarity index 52% rename from src/common/containers/UserList/index.jsx rename to src/common/containers/MemberList/index.jsx index f5f60d08..1c8e998f 100644 --- a/src/common/containers/UserList/index.jsx +++ b/src/common/containers/MemberList/index.jsx @@ -5,14 +5,14 @@ import {connect} from 'react-redux' import {showLoad, hideLoad} from 'src/common/actions/app' import {findChapters} from 'src/common/actions/chapter' -import {findUsers} from 'src/common/actions/user' -import UserList from 'src/common/components/UserList' +import {findMembers} from 'src/common/actions/member' +import MemberList from 'src/common/components/MemberList' import {toSortedArray, userCan} from 'src/common/util' import Flex from 'src/common/components/Layout/Flex' import styles from './index.css' -const UserModel = { +const tableModel = { avatarUrl: {title: 'Photo', type: String}, handle: {type: String}, name: {type: String}, @@ -22,7 +22,7 @@ const UserModel = { active: {type: String}, } -class UserListContainer extends Component { +class MemberListContainer extends Component { componentDidMount() { this.props.showLoad() this.props.fetchData() @@ -35,76 +35,74 @@ class UserListContainer extends Component { } render() { - const {users, isBusy, currentUser} = this.props - - const userData = users.map(user => { - const userURL = userCan(currentUser, 'viewUser') ? - `/users/${user.handle}` : null - const mailtoURL = `mailto:${user.email}` - const altTitle = `${user.name} (${user.handle})` - return Object.assign({}, user, { + const {members, isBusy, showMemberLinks} = this.props + const tableSource = members.map(member => { + const profileUrl = showMemberLinks ? `/members/${members.handle}` : null + const mailtoURL = `mailto:${member.email}` + const altTitle = `${member.name} (${member.handle})` + return Object.assign({}, member, { avatarUrl: ( - + {altTitle} ), - handle: {user.handle}, - name: {user.name}, - chapterName: (user.chapter || {}).name, - phaseNumber: ((user || {}).phase || {}).number, - email: {user.email}, - active: user.active ? 'Yes' : 'No', + handle: {members.handle}, + name: {members.name}, + chapterName: (member.chapter || {}).name, + phaseNumber: ((member || {}).phase || {}).number, + email: {member.email}, + active: member.active ? 'Yes' : 'No', }) }) return isBusy ? null : ( - + ) } } -UserListContainer.propTypes = { - users: PropTypes.array.isRequired, +MemberListContainer.propTypes = { + members: PropTypes.array.isRequired, isBusy: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired, - currentUser: PropTypes.object.isRequired, + showMemberLinks: PropTypes.bool.isRequired, fetchData: PropTypes.func.isRequired, navigate: PropTypes.func.isRequired, showLoad: PropTypes.func.isRequired, hideLoad: PropTypes.func.isRequired, } -UserListContainer.fetchData = fetchData +MemberListContainer.fetchData = fetchData function fetchData(dispatch) { dispatch(findChapters()) - dispatch(findUsers()) + dispatch(findMembers()) } function mapStateToProps(state) { - const {app, users, chapters} = state + const {app, auth, members, chapters} = state const {chapters: chaptersById} = chapters - const {users: usersById} = users + const {members: membersById} = members - const usersWithChapters = Object.values(usersById).map(user => { - const chapter = chaptersById[user.chapterId] || {} - return {...user, chapter} + const membersWithChapters = Object.values(membersById).map(member => { + const chapter = chaptersById[member.chapterId] || {} + return {...member, chapter} }) - const userList = toSortedArray(usersWithChapters, 'handle') + const memberList = toSortedArray(membersWithChapters, 'handle') return { - users: userList, - isBusy: users.isBusy || chapters.isBusy, + members: memberList, + isBusy: members.isBusy || chapters.isBusy, loading: app.showLoading, - currentUser: state.auth.currentUser, + showMemberLinks: userCan(auth.currentUser, 'viewMember'), } } @@ -117,4 +115,4 @@ function mapDispatchToProps(dispatch) { } } -export default connect(mapStateToProps, mapDispatchToProps)(UserListContainer) +export default connect(mapStateToProps, mapDispatchToProps)(MemberListContainer) diff --git a/src/common/containers/ProjectDetail/index.jsx b/src/common/containers/ProjectDetail/index.jsx index 87ac9650..43dd8cbf 100644 --- a/src/common/containers/ProjectDetail/index.jsx +++ b/src/common/containers/ProjectDetail/index.jsx @@ -3,14 +3,17 @@ import {connect} from 'react-redux' import {push} from 'react-router-redux' import {showLoad, hideLoad} from 'src/common/actions/app' -import {unlockSurvey, lockSurvey, getProjectSummary} from 'src/common/actions/project' -import ProjectDetail from 'src/common/components/ProjectDetail' +import {getProjectSummary} from 'src/common/actions/project' +import {unlockSurveyForMember, lockSurveyForMember} from 'src/common/actions/survey' import {userCan} from 'src/common/util' +import ProjectDetail from 'src/common/components/ProjectDetail' class ProjectDetailContainer extends Component { constructor(props) { super(props) this.handleClickEdit = this.handleClickEdit.bind(this) + this.handleClickUnlockRetro = this.handleClickUnlockRetro.bind(this) + this.handleClickLockRetro = this.handleClickLockRetro.bind(this) } componentDidMount() { @@ -19,7 +22,7 @@ class ProjectDetailContainer extends Component { } componentWillReceiveProps(nextProps) { - if (!nextProps.isBusy && nextProps.loading) { + if (this.props.isBusy && !nextProps.isBusy) { this.props.hideLoad() } } @@ -31,48 +34,56 @@ class ProjectDetailContainer extends Component { this.props.navigate(`/projects/${this.props.project.name}/edit`) } + handleClickUnlockRetro(member) { + const {project, unlockSurveyForMember} = this.props + unlockSurveyForMember(project.retrospectiveSurveyId, member.id) + } + + handleClickLockRetro(member) { + const {project, lockSurveyForMember} = this.props + lockSurveyForMember(project.retrospectiveSurveyId, member.id) + } + render() { const { - currentUser, isBusy, - isLockingOrUnlocking, project, - projectUserSummaries, - unlockMemberSurvey, - lockMemberSurvey, + projectMemberSummaries, + showEdit, + editDisabled, + lockDisabled, } = this.props return isBusy ? null : ( ) } } ProjectDetailContainer.propTypes = { - project: PropTypes.object, - projectUserSummaries: PropTypes.array, isBusy: PropTypes.bool.isRequired, - isLockingOrUnlocking: PropTypes.bool.isRequired, - loading: PropTypes.bool.isRequired, - currentUser: PropTypes.object, + project: PropTypes.object, + projectMemberSummaries: PropTypes.array, + showEdit: PropTypes.bool.isRequired, + editDisabled: PropTypes.bool.isRequired, + lockDisabled: PropTypes.bool.isRequired, fetchData: PropTypes.func.isRequired, navigate: PropTypes.func.isRequired, showLoad: PropTypes.func.isRequired, hideLoad: PropTypes.func.isRequired, - unlockMemberSurvey: PropTypes.func.isRequired, - lockMemberSurvey: PropTypes.func.isRequired, + unlockSurveyForMember: PropTypes.func.isRequired, + lockSurveyForMember: PropTypes.func.isRequired, } -ProjectDetailContainer.unlockMemberSurvey = unlockSurvey -ProjectDetailContainer.lockMemberSurvey = lockSurvey ProjectDetailContainer.fetchData = fetchData function fetchData(dispatch, props) { @@ -81,23 +92,26 @@ function fetchData(dispatch, props) { function mapStateToProps(state, ownProps) { const {identifier} = ownProps.params - const {app, auth, projectSummaries} = state - const {isLockingOrUnlocking, projectSummaries: projectSummariesByProjectId} = projectSummaries + const {app, auth, projectSummaries, surveys} = state + const {projectSummaries: projectSummariesByProjectId} = projectSummaries const projectSummary = Object.values(projectSummariesByProjectId).find(projectSummary => { return projectSummary.project && ( - projectSummary.project.name === identifier || + projectSummary.project.name.toLowerCase() === identifier.toLowerCase() || projectSummary.project.id === identifier ) }) || {} + const {project = {}} = projectSummary + return { - project: projectSummary.project, - projectUserSummaries: projectSummary.projectUserSummaries, - isBusy: projectSummaries.isBusy, - isLockingOrUnlocking, - loading: app.showLoading, - currentUser: auth.currentUser, + project, + projectMemberSummaries: projectSummary.projectMemberSummaries, + showEdit: userCan(auth.currentUser, 'importProject'), + editDisabled: !project || !project.retrospectiveSurveyId, + lockDisabled: !userCan(auth.currentUser, 'lockAndUnlockSurveys'), + lockIsBusy: surveys.isBusy, + isBusy: projectSummaries.isBusy || app.showLoading, } } @@ -107,8 +121,8 @@ function mapDispatchToProps(dispatch, props) { navigate: path => dispatch(push(path)), showLoad: () => dispatch(showLoad()), hideLoad: () => dispatch(hideLoad()), - unlockMemberSurvey: (memberId, projectId) => dispatch(unlockSurvey(memberId, projectId)), - lockMemberSurvey: (memberId, projectId) => dispatch(lockSurvey(memberId, projectId)), + unlockSurveyForMember: (surveyId, memberId) => dispatch(unlockSurveyForMember(surveyId, memberId)), + lockSurveyForMember: (surveyId, memberId) => dispatch(lockSurveyForMember(surveyId, memberId)), } } diff --git a/src/common/containers/ProjectList/index.jsx b/src/common/containers/ProjectList/index.jsx index 40c1d98f..2d9bf16f 100644 --- a/src/common/containers/ProjectList/index.jsx +++ b/src/common/containers/ProjectList/index.jsx @@ -7,7 +7,7 @@ import FontIcon from 'react-toolbox/lib/font_icon' import ProjectList from 'src/common/components/ProjectList' import {showLoad, hideLoad} from 'src/common/actions/app' import {findProjectsForCycle} from 'src/common/actions/project' -import {findUsers} from 'src/common/actions/user' +import {findMembers} from 'src/common/actions/member' import {findPhases} from 'src/common/actions/phase' import {userCan} from 'src/common/util' import {formatDate} from 'src/common/util/format' @@ -47,7 +47,7 @@ class ProjectListContainer extends Component { } render() { - const {isBusy, currentUser, projects} = this.props + const {isBusy, showMemberLinks, showImport, projects} = this.props const projectData = projects.map(project => { const cycle = project.cycle || {} @@ -55,13 +55,13 @@ class ProjectListContainer extends Component { const projectGoal = project.goal || {} const projectURL = `/projects/${project.name}` const memberHandles = (project.members || []).map(member => { - const memberURL = `/users/${member.handle}` + const memberURL = `/members/${member.handle}` const linkKey = `${project.name}-${member.handle}` return {member.handle} }).reduce((a, b) => [a, ', ', b]) return { memberHandles: {memberHandles}, - name: userCan(currentUser, 'viewProject') ? ( + name: showMemberLinks ? ( {project.name} ) : project.name, state: cycle.state, @@ -85,7 +85,7 @@ class ProjectListContainer extends Component { @@ -98,7 +98,8 @@ ProjectListContainer.propTypes = { oldestCycleNumber: PropTypes.number, isBusy: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired, - currentUser: PropTypes.object.isRequired, + showMemberLinks: PropTypes.bool.isRequired, + showImport: PropTypes.bool.isRequired, fetchData: PropTypes.func.isRequired, handleLoadMore: PropTypes.func.isRequired, navigate: PropTypes.func.isRequired, @@ -109,22 +110,22 @@ ProjectListContainer.propTypes = { ProjectListContainer.fetchData = fetchData function fetchData(dispatch) { - dispatch(findUsers()) + dispatch(findMembers()) dispatch(findPhases()) dispatch(findProjectsForCycle()) } function mapStateToProps(state) { - const {app, auth, projects, users, phases} = state + const {app, auth, projects, members, phases} = state const {projects: projectsById} = projects - const {users: usersById} = users + const {members: membersById} = members const {phases: phasesById} = phases const expandedProjects = Object.values(projectsById).map(project => { return { ...project, phase: phasesById[project.phaseId], - members: (project.memberIds || []).map(userId => (usersById[userId] || {})), + members: (project.memberIds || []).map(memberId => (membersById[memberId] || {})), } }) @@ -139,11 +140,12 @@ function mapStateToProps(state) { projectList[projectList.length - 1].cycle.cycleNumber : null return { - isBusy: projects.isBusy || users.isBusy, + isBusy: projects.isBusy || members.isBusy, loading: app.showLoading, - currentUser: auth.currentUser, - oldestCycleNumber, projects: projectList, + showMemberLinks: userCan(auth.currentUser, 'viewProject'), + showImport: userCan(auth.currentUser, 'importProject'), + oldestCycleNumber, } } diff --git a/src/common/containers/UserDetail/index.jsx b/src/common/containers/UserDetail/index.jsx deleted file mode 100644 index a0351b59..00000000 --- a/src/common/containers/UserDetail/index.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import {push} from 'react-router-redux' -import {connect} from 'react-redux' - -import {showLoad, hideLoad} from 'src/common/actions/app' -import {getUserSummary, deactivateUser} from 'src/common/actions/user' -import UserDetail from 'src/common/components/UserDetail' - -class UserDetailContainer extends Component { - constructor(props) { - super(props) - this.handleSelectProjectRow = this.handleSelectProjectRow.bind(this) - this.handleClickEdit = this.handleClickEdit.bind(this) - } - - componentDidMount() { - const {showLoad, fetchData} = this.props - showLoad() - fetchData() - } - - componentWillReceiveProps(nextProps) { - if (!nextProps.isBusy && nextProps.loading) { - this.props.hideLoad() - } - } - - handleSelectProjectRow(rowIndex) { - const {userProjectSummaries} = this.props || [] - const project = userProjectSummaries[rowIndex] || {} - const projectDetailUrl = `/projects/${project.name}` - this.props.navigate(projectDetailUrl) - } - - handleClickEdit() { - if (!this.props.user) { - return - } - this.props.navigate(`/users/${this.props.user.handle}/edit`) - } - - render() { - const {user, navigate, currentUser, onDeactivateUser, userProjectSummaries, defaultAvatarURL} = this.props - return user ? ( - - ) : null - } -} - -UserDetailContainer.propTypes = { - user: PropTypes.object, - currentUser: PropTypes.object, - userProjectSummaries: PropTypes.array, - isBusy: PropTypes.bool.isRequired, - loading: PropTypes.bool.isRequired, - fetchData: PropTypes.func.isRequired, - navigate: PropTypes.func.isRequired, - showLoad: PropTypes.func.isRequired, - hideLoad: PropTypes.func.isRequired, - onDeactivateUser: PropTypes.func.isRequired, - defaultAvatarURL: PropTypes.string, -} - -UserDetailContainer.fetchData = fetchData - -function fetchData(dispatch, props) { - dispatch(getUserSummary(props.params.identifier)) -} - -function mapStateToProps(state, ownProps) { - const {identifier} = ownProps.params - const {userSummaries, auth} = state - const {userSummaries: userSummariesByUserId} = userSummaries - - const userSummary = Object.values(userSummariesByUserId).find(userSummary => { - return userSummary.user && ( - userSummary.user.handle.toLowerCase() === identifier.toLowerCase() || - userSummary.user.id === identifier - ) - }) || {} - - return { - user: userSummary.user, - userProjectSummaries: userSummary.userProjectSummaries, - isBusy: userSummaries.isBusy, - loading: state.app.showLoading, - currentUser: auth.currentUser, - defaultAvatarURL: process.env.LOGO_FULL_URL, - } -} - -function mapDispatchToProps(dispatch, props) { - return { - fetchData: () => fetchData(dispatch, props), - navigate: path => dispatch(push(path)), - showLoad: () => dispatch(showLoad()), - hideLoad: () => dispatch(hideLoad()), - onDeactivateUser: id => dispatch(deactivateUser(id)), - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(UserDetailContainer) diff --git a/src/common/reducers/index.js b/src/common/reducers/index.js index a0d54d64..d59df7ca 100644 --- a/src/common/reducers/index.js +++ b/src/common/reducers/index.js @@ -11,10 +11,9 @@ import cycleVotingResults from './cycleVotingResults' import phases from './phases' import phaseSummaries from './phaseSummaries' import members from './members' +import memberSummaries from './memberSummaries' import projects from './projects' import projectSummaries from './projectSummaries' -import users from './users' -import userSummaries from './userSummaries' import surveys from './surveys' const rootReducer = combineReducers({ @@ -28,10 +27,9 @@ const rootReducer = combineReducers({ phases, phaseSummaries, members, + memberSummaries, projects, projectSummaries, - users, - userSummaries, surveys, }) diff --git a/src/common/reducers/memberSummaries.js b/src/common/reducers/memberSummaries.js new file mode 100644 index 00000000..5fd00a32 --- /dev/null +++ b/src/common/reducers/memberSummaries.js @@ -0,0 +1,38 @@ +import { + GET_MEMBER_SUMMARY_REQUEST, + GET_MEMBER_SUMMARY_SUCCESS, + GET_MEMBER_SUMMARY_FAILURE, +} from 'src/common/actions/types' + +const initialState = { + memberSummaries: {}, + isBusy: false, +} + +export default function memberSummaries(state = initialState, action) { + switch (action.type) { + case GET_MEMBER_SUMMARY_REQUEST: + return Object.assign({}, state, { + isBusy: true, + }) + + case GET_MEMBER_SUMMARY_SUCCESS: + { + const memberSummary = action.response || {} + const {member} = memberSummary || {} + const memberSummaries = Object.assign({}, state.memberSummaries, {[member.id]: memberSummary}) + return Object.assign({}, state, { + isBusy: false, + memberSummaries, + }) + } + + case GET_MEMBER_SUMMARY_FAILURE: + return Object.assign({}, state, { + isBusy: false, + }) + + default: + return state + } +} diff --git a/src/common/reducers/projectSummaries.js b/src/common/reducers/projectSummaries.js index f0703b6f..694e87f1 100644 --- a/src/common/reducers/projectSummaries.js +++ b/src/common/reducers/projectSummaries.js @@ -2,16 +2,11 @@ import { GET_PROJECT_SUMMARY_REQUEST, GET_PROJECT_SUMMARY_SUCCESS, GET_PROJECT_SUMMARY_FAILURE, - LOCK_SURVEY_REQUEST, - LOCK_SURVEY_SUCCESS, - UNLOCK_SURVEY_REQUEST, - UNLOCK_SURVEY_SUCCESS, } from 'src/common/actions/types' const initialState = { projectSummaries: {}, isBusy: false, - isLockingOrUnlocking: false, } export default function projectSummaries(state = initialState, action) { @@ -21,19 +16,12 @@ export default function projectSummaries(state = initialState, action) { isBusy: true, }) - case LOCK_SURVEY_REQUEST: - case UNLOCK_SURVEY_REQUEST: - return Object.assign({}, state, {isLockingOrUnlocking: true}) - case GET_PROJECT_SUMMARY_SUCCESS: - case LOCK_SURVEY_SUCCESS: - case UNLOCK_SURVEY_SUCCESS: { const projectSummary = action.response || {} const {project} = projectSummary || {} const projectSummaries = Object.assign({}, state.projectSummaries, {[project.id]: projectSummary}) return Object.assign({}, state, { - isLockingOrUnlocking: false, isBusy: false, projectSummaries, }) diff --git a/src/common/reducers/surveys.js b/src/common/reducers/surveys.js index 6d4c7a8c..46161b82 100644 --- a/src/common/reducers/surveys.js +++ b/src/common/reducers/surveys.js @@ -11,6 +11,12 @@ import { SUBMIT_SURVEY_REQUEST, SUBMIT_SURVEY_SUCCESS, SUBMIT_SURVEY_FAILURE, + LOCK_SURVEY_REQUEST, + LOCK_SURVEY_SUCCESS, + LOCK_SURVEY_FAILURE, + UNLOCK_SURVEY_REQUEST, + UNLOCK_SURVEY_SUCCESS, + UNLOCK_SURVEY_FAILURE, SET_SURVEY_GROUP, } from 'src/common/actions/types' @@ -29,6 +35,8 @@ export default function surveys(state = initialState, action) { case FIND_SURVEYS_REQUEST: case GET_SURVEY_REQUEST: case SAVE_SURVEY_RESPONSES_REQUEST: + case LOCK_SURVEY_REQUEST: + case UNLOCK_SURVEY_REQUEST: return Object.assign({}, state, { isBusy: true, }) @@ -61,6 +69,10 @@ export default function surveys(state = initialState, action) { case SUBMIT_SURVEY_SUCCESS: case SUBMIT_SURVEY_FAILURE: + case LOCK_SURVEY_SUCCESS: + case LOCK_SURVEY_FAILURE: + case UNLOCK_SURVEY_SUCCESS: + case UNLOCK_SURVEY_FAILURE: return Object.assign({}, state, { isBusy: false, isSubmitting: false, diff --git a/src/common/reducers/userSummaries.js b/src/common/reducers/userSummaries.js deleted file mode 100644 index 608e5fa7..00000000 --- a/src/common/reducers/userSummaries.js +++ /dev/null @@ -1,62 +0,0 @@ -import { - DEACTIVATE_USER_REQUEST, - DEACTIVATE_USER_SUCCESS, - DEACTIVATE_USER_FAILURE, - GET_USER_SUMMARY_REQUEST, - GET_USER_SUMMARY_SUCCESS, - GET_USER_SUMMARY_FAILURE, -} from 'src/common/actions/types' - -const initialState = { - userSummaries: {}, - isBusy: false, -} - -export default function userSummaries(state = initialState, action) { - switch (action.type) { - case GET_USER_SUMMARY_REQUEST: - return Object.assign({}, state, { - isBusy: true, - }) - - case GET_USER_SUMMARY_SUCCESS: - { - const userSummary = action.response || {} - const {user} = userSummary || {} - const userSummaries = Object.assign({}, state.userSummaries, {[user.id]: userSummary}) - return Object.assign({}, state, { - isBusy: false, - userSummaries, - }) - } - - case GET_USER_SUMMARY_FAILURE: - return Object.assign({}, state, { - isBusy: false, - }) - - case DEACTIVATE_USER_REQUEST: - return Object.assign({}, state, { - isBusy: true, - }) - case DEACTIVATE_USER_SUCCESS: - { - const userAttrs = action.response || {} - const userSummary = state.userSummaries[userAttrs.id] || {} - const user = Object.assign({}, userSummary.user, userAttrs) - const newUserSummary = Object.assign({}, userSummary, {user}) - const userSummaries = Object.assign({}, state.userSummaries, {[user.id]: newUserSummary}) - return Object.assign({}, state, { - isBusy: false, - userSummaries, - }) - } - case DEACTIVATE_USER_FAILURE: - return Object.assign({}, state, { - isBusy: false, - }) - - default: - return state - } -} diff --git a/src/common/reducers/users.js b/src/common/reducers/users.js deleted file mode 100644 index c64f693d..00000000 --- a/src/common/reducers/users.js +++ /dev/null @@ -1,35 +0,0 @@ -import { - FIND_USERS_REQUEST, - FIND_USERS_SUCCESS, - FIND_USERS_FAILURE, -} from 'src/common/actions/types' - -import {mergeEntities} from 'src/common/util' - -const initialState = { - users: {}, - isBusy: false, -} - -export default function users(state = initialState, action) { - switch (action.type) { - case FIND_USERS_REQUEST: - return Object.assign({}, state, { - isBusy: true, - }) - case FIND_USERS_SUCCESS: - { - const users = mergeEntities(state.users, action.response.entities.users) - return Object.assign({}, state, { - isBusy: false, - users, - }) - } - case FIND_USERS_FAILURE: - return Object.assign({}, state, { - isBusy: false, - }) - default: - return state - } -} diff --git a/src/common/routes/index.jsx b/src/common/routes/index.jsx index 35b099c0..c6593bac 100644 --- a/src/common/routes/index.jsx +++ b/src/common/routes/index.jsx @@ -1,7 +1,7 @@ /* eslint new-cap: [2, {"capIsNewExceptions": ["UserAuthWrapper"]}] */ /* global window */ import React from 'react' -import {Route, IndexRoute, IndexRedirect} from 'react-router' +import {Route, IndexRoute, IndexRedirect, Redirect} from 'react-router' import {UserAuthWrapper as userAuthWrapper} from 'redux-auth-wrapper' import {push} from 'react-router-redux' @@ -11,10 +11,10 @@ import {loginURL} from 'src/common/util/auth' import App from 'src/common/containers/App' import ChapterForm from 'src/common/containers/ChapterForm' import ChapterList from 'src/common/containers/ChapterList' -import UserList from 'src/common/containers/UserList' -import UserDetail from 'src/common/containers/UserDetail' +import MemberForm from 'src/common/containers/MemberForm' +import MemberList from 'src/common/containers/MemberList' +import MemberDetail from 'src/common/containers/MemberDetail' import ProjectForm from 'src/common/containers/ProjectForm' -import UserForm from 'src/common/containers/UserForm' import ProjectList from 'src/common/containers/ProjectList' import ProjectDetail from 'src/common/containers/ProjectDetail' import RetroSurvey from 'src/common/containers/RetroSurvey' @@ -72,10 +72,12 @@ const routes = store => { - - - - + + + + + + diff --git a/src/common/util/__tests__/index.test.js b/src/common/util/__tests__/index.test.js index 443be46d..630aecb8 100644 --- a/src/common/util/__tests__/index.test.js +++ b/src/common/util/__tests__/index.test.js @@ -8,6 +8,7 @@ import { range, segment, sortByAttr, + without, } from '../index' describe(testContext(__filename), function () { @@ -152,4 +153,14 @@ describe(testContext(__filename), function () { ) }) }) + + describe('without()', function () { + it('excludes correct element', function () { + expect( + without([1, 2, 3], 3) + ).to.deep.eq( + [1, 2] + ) + }) + }) }) diff --git a/src/common/util/index.js b/src/common/util/index.js index 0e4c8172..5d666fcc 100644 --- a/src/common/util/index.js +++ b/src/common/util/index.js @@ -148,6 +148,13 @@ export function mapById(arr, idKey = 'id') { }, new Map()) } +export function hashById(arr, idKey = 'id') { + return arr.reduce((result, item) => { + result[item[idKey]] = item + return result + }, {}) +} + export function groupById(arr, idKey = 'id') { return arr.reduce((result, item) => { const groupKey = item[idKey] @@ -260,3 +267,7 @@ export function flatten(potentialArray) { } return potentialArray.reduce((result, next) => result.concat(flatten(next)), []) } + +export function without(values = [], excludedValue) { + return values.filter(value => value !== excludedValue) +} diff --git a/src/common/util/userCan.js b/src/common/util/userCan.js index 868f38ed..d182a32f 100644 --- a/src/common/util/userCan.js +++ b/src/common/util/userCan.js @@ -21,22 +21,22 @@ const CAPABILITY_ROLES = { deleteProject: [ADMIN], viewCycleVotingResults: GENERAL_USE, - updateUser: [ADMIN], importProject: [ADMIN], updateProject: [ADMIN], listProjects: GENERAL_USE, findProjects: GENERAL_USE, viewProject: GENERAL_USE, viewProjectSummary: GENERAL_USE, - viewProjectUserSummary: [ADMIN], + viewProjectMemberSummary: [ADMIN], setProjectArtifact: GENERAL_USE, - viewUser: GENERAL_USE, - viewUserFeedback: [ADMIN], - viewUserSummary: GENERAL_USE, - listUsers: GENERAL_USE, - findUsers: GENERAL_USE, - deactivateUser: [ADMIN], + viewMember: GENERAL_USE, + viewMemberFeedback: [ADMIN], + viewMemberSummary: GENERAL_USE, + listMembers: GENERAL_USE, + findMembers: GENERAL_USE, + updateMember: [ADMIN], + deactivateMember: [ADMIN], viewPhases: GENERAL_USE, listPhases: GENERAL_USE, diff --git a/src/common/validations/index.js b/src/common/validations/index.js index cb90703e..37c22b8c 100644 --- a/src/common/validations/index.js +++ b/src/common/validations/index.js @@ -3,8 +3,8 @@ import validate from 'validate.js' export * from './chapter' export * from './cycle' export * from './inviteCode' +export * from './member' export * from './project' -export * from './user' export function validationErrorToReduxFormErrors(error) { const errorMap = {} diff --git a/src/common/validations/user.js b/src/common/validations/member.js similarity index 65% rename from src/common/validations/user.js rename to src/common/validations/member.js index 309c7ee0..ef0ed8a9 100644 --- a/src/common/validations/user.js +++ b/src/common/validations/member.js @@ -1,5 +1,5 @@ import yup from 'yup' -export const userSchema = yup.object().shape({ +export const memberSchema = yup.object().shape({ phaseNumber: yup.number().integer().positive().max(5).nullable(), }) diff --git a/src/common/validations/project.js b/src/common/validations/project.js index 32d99837..22a8fc8c 100644 --- a/src/common/validations/project.js +++ b/src/common/validations/project.js @@ -5,8 +5,8 @@ export const projectSchema = yup.object().shape({ cycleIdentifier: yup.number().integer().required().min(1), goalIdentifier: yup.number().integer().positive().required(), memberIdentifiers: yup.string().trim().required().test( - 'are-valid-user-identifiers', - 'Invalid user identifier(s)', + 'are-valid-member-identifiers', + 'Invalid member identifier(s)', _isValidIdentifierList, ), }) diff --git a/src/script/cloneArtifacts.js b/src/script/cloneArtifacts.js index c2855896..16a6bfeb 100644 --- a/src/script/cloneArtifacts.js +++ b/src/script/cloneArtifacts.js @@ -2,7 +2,7 @@ import Promise from 'bluebird' import parseArgs from 'minimist' import clone from 'git-clone' -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' import {Chapter, Cycle, Project} from 'src/server/services/dataService' import {finish} from './util' @@ -33,7 +33,7 @@ async function run() { const projectsWithMembers = await Promise.all(projects.map(async (p, index) => { return { ...projects[index], - members: await getMemberInfo(p.memberIds) + members: await findMemberUsers(p.memberIds) } })) diff --git a/src/script/previewProjects.js b/src/script/previewProjects.js index ef0acbc7..37befb6e 100644 --- a/src/script/previewProjects.js +++ b/src/script/previewProjects.js @@ -1,8 +1,9 @@ import parseArgs from 'minimist' -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' import {buildProjects} from 'src/server/actions/formProjects' -import {Chapter, Cycle, Member} from 'src/server/services/dataService' +import {Chapter, Cycle} from 'src/server/services/dataService' +import {mapById} from 'src/server/util' import {finish} from './util' const LOG_PREFIX = `[${__filename.split('js')[0]}]` @@ -54,24 +55,14 @@ function _parseCLIArgs(argv) { async function _expandProjectData(projects) { const allMembers = new Map() const allProjects = await Promise.all(projects.map(async project => { - const members = await Promise.all(project.memberIds.map(async memberId => { - const [users, member] = await Promise.all([ - getMemberInfo([memberId]), - Member.get(memberId), - ]) - - const mergedUser = { - ...users[0], - ...member, - } - - const memberProject = allMembers.get(member.id) || {...mergedUser, projects: []} + const memberUsersById = mapById(await findMemberUsers(project.memberIds)) + const members = await Promise.all(project.memberIds.map(memberId => { + const memberUser = memberUsersById.get(memberId) + const memberProject = allMembers.get(memberId) || {...memberUser, projects: []} memberProject.projects.push(project) - allMembers.set(member.id, memberProject) - - return mergedUser + allMembers.set(memberId, memberProject) + return memberUser })) - return {...project, members} })) diff --git a/src/script/printProjects.js b/src/script/printProjects.js index 502cff40..74ec48e5 100644 --- a/src/script/printProjects.js +++ b/src/script/printProjects.js @@ -1,7 +1,7 @@ import fs from 'fs' import parseArgs from 'minimist' -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' import {Chapter, Cycle, Project} from 'src/server/services/dataService' import {finish} from './util' @@ -39,7 +39,7 @@ async function run() { const projectsWithMembers = await Promise.all(projects.map(async (p, index) => { return { ...projects[index], - members: await getMemberInfo(p.memberIds) + members: await findMemberUsers(p.memberIds) } })) diff --git a/src/server/actions/__tests__/deactivateUser.test.js b/src/server/actions/__tests__/deactivateMember.test.js similarity index 53% rename from src/server/actions/__tests__/deactivateUser.test.js rename to src/server/actions/__tests__/deactivateMember.test.js index 80f7f577..10515dbc 100644 --- a/src/server/actions/__tests__/deactivateUser.test.js +++ b/src/server/actions/__tests__/deactivateMember.test.js @@ -1,14 +1,12 @@ /* eslint-env mocha */ /* global expect testContext */ /* eslint-disable prefer-arrow-callback, no-unused-expressions, max-nested-callbacks */ +import config from 'src/config' import factory from 'src/test/factories' import {useFixture, resetDB} from 'src/test/helpers' import stubs from 'src/test/stubs' -import getUser from 'src/server/actions/getUser' -import deactivateUser from 'src/server/actions/deactivateUser' -import nock from 'nock' -import config from 'src/config' +import deactivateMember from '../deactivateMember' describe(testContext(__filename), function () { beforeEach(resetDB) @@ -17,45 +15,24 @@ describe(testContext(__filename), function () { this.user = await factory.build('user') this.member = await factory.create('member', {id: this.user.id}) useFixture.nockClean() - this.nockIDMDeactivateUser = () => { - nock(config.server.idm.baseURL) - .persist() - .intercept('/graphql', 'POST') - .reply(200, () => ({ - data: { - deactivateUser: { - id: this.user.id, - active: false, - handle: this.user.handle, - }, - }, - })) - } - stubs.herokuService.enableOne('removeCollaboratorFromApps') + useFixture.nockIDMGetUser(this.user) + useFixture.nockIDMDeactivateUser(this.user) stubs.gitHubService.enableOne('removeUserFromOrganizations') stubs.chatService.enableOne('deactivateUser') }) afterEach(function () { - stubs.herokuService.disableOne('removeCollaboratorFromApps') stubs.gitHubService.disableOne('removeUserFromOrganizations') stubs.chatService.disableOne('deactivateUser') }) - it('calls heroku, github, and slack and deactivates the user in idm', async function () { + it('calls github, and slack and deactivates the user in idm', async function () { const gitHubService = require('src/server/services/gitHubService') - const herokuService = require('src/server/services/herokuService') const chatService = require('src/server/services/chatService') - useFixture.nockIDMGetUser(this.user) - const user = await getUser(this.user.id) - - useFixture.nockIDMGetUser(this.user) - this.nockIDMDeactivateUser() - const result = await deactivateUser(this.user.id) + const result = await deactivateMember(this.user.id) expect(gitHubService.removeUserFromOrganizations).to.have.been.calledWith(this.user.handle, config.server.github.organizations) - expect(herokuService.removeCollaboratorFromApps).to.have.been.calledWith(user, config.losPermissions.heroku.apps) expect(chatService.deactivateUser).to.have.been.calledWith(this.user.id) expect(result.active).to.eql(false) }) diff --git a/src/server/actions/__tests__/findUserProjectEvaluations.test.js b/src/server/actions/__tests__/findMemberProjectEvaluations.test.js similarity index 87% rename from src/server/actions/__tests__/findUserProjectEvaluations.test.js rename to src/server/actions/__tests__/findMemberProjectEvaluations.test.js index 2e3bfb5a..e9800bb9 100644 --- a/src/server/actions/__tests__/findUserProjectEvaluations.test.js +++ b/src/server/actions/__tests__/findMemberProjectEvaluations.test.js @@ -8,7 +8,7 @@ import {resetDB, useFixture} from 'src/test/helpers' import {Project} from 'src/server/services/dataService' import {FEEDBACK_TYPE_DESCRIPTORS} from 'src/common/models/feedbackType' -import findUserProjectEvaluations from '../findUserProjectEvaluations' +import findMemberProjectEvaluations from '../findMemberProjectEvaluations' describe(testContext(__filename), function () { before(resetDB) @@ -44,7 +44,7 @@ describe(testContext(__filename), function () { this.survey = retrospectiveSurvey }) - it('returns correct evaluations for user on project', async function () { + it('returns correct evaluations for member on project', async function () { const {questions, project} = this const {memberIds} = project @@ -63,10 +63,10 @@ describe(testContext(__filename), function () { } }) - const userProjectEvaluations = await findUserProjectEvaluations(subjectId, project) + const memberProjectEvaluations = await findMemberProjectEvaluations(subjectId, project) - expect(userProjectEvaluations.length).to.eq(memberIds.length - 1) - userProjectEvaluations.forEach(evaluation => { + expect(memberProjectEvaluations.length).to.eq(memberIds.length - 1) + memberProjectEvaluations.forEach(evaluation => { const respondentId = evaluation.submittedById expect(evaluation[FEEDBACK_TYPE_DESCRIPTORS.TEAM_PLAY]).to.eq(`${FEEDBACK_TYPE_DESCRIPTORS.TEAM_PLAY}_${respondentId}`) expect(evaluation[FEEDBACK_TYPE_DESCRIPTORS.TECHNICAL_COMPREHENSION]).to.eq(`${FEEDBACK_TYPE_DESCRIPTORS.TECHNICAL_COMPREHENSION}_${respondentId}`) diff --git a/src/server/actions/__tests__/findMemberUsers.test.js b/src/server/actions/__tests__/findMemberUsers.test.js new file mode 100644 index 00000000..646fa59c --- /dev/null +++ b/src/server/actions/__tests__/findMemberUsers.test.js @@ -0,0 +1,82 @@ +/* eslint-env mocha */ +/* global expect testContext */ +/* eslint-disable prefer-arrow-callback, no-unused-expressions, max-nested-callbacks */ +import factory from 'src/test/factories' +import {resetDB, useFixture} from 'src/test/helpers' +import {expectArraysToContainTheSameElements} from 'src/test/helpers/expectations' + +import findMemberUsers from '../findMemberUsers' + +describe(testContext(__filename), function () { + before(resetDB) + + before(async function () { + this.members = await factory.createMany('member', 5) + this.users = this.members.map(member => ({ + id: member.id, + handle: `handle_${member.id}`, + })) + }) + + beforeEach(function () { + useFixture.nockClean() + }) + + it('returns correct users for handle identifiers', async function () { + const users = this.users.slice(0, 2) + useFixture.nockIDMFindUsers(users) + const result = await findMemberUsers(users.map(u => u.handle)) + expectArraysToContainTheSameElements(result.map(u => u.id), users.map(u => u.id)) + }) + + it('returns correct users for UUIDs', async function () { + const users = this.members.slice(0, 5) + useFixture.nockIDMFindUsers(users) + const result = await findMemberUsers(users.map(u => u.id)) + expectArraysToContainTheSameElements(result.map(u => u.id), users.map(u => u.id)) + }) + + it('returns only unique users', async function () { + const user = this.users[0] + const {id, handle} = user + useFixture.nockIDMFindUsers([user, user]) + const result = await findMemberUsers([id, handle]) + expect(result.length).to.equal(1) + }) + + it('returns all IDM fields if none specified', async function () { + useFixture.nockIDMFindUsers(this.users) + const user = this.users[0] + const member = this.members.find(p => p.id === user.id) + const [result] = await findMemberUsers([user.id]) + expect(result.id).to.equal(user.id) + expect(result.handle).to.equal(user.handle) + expect(result.name).to.equal(user.name) + expect(result.email).to.equal(user.email) + expect(result.chapterId).to.equal(member.chapterId) + }) + + it('returns only specified IDM fields', async function () { + useFixture.nockIDMFindUsers(this.users) + const user = this.users[0] + const idmFields = ['handle'] + const [result] = await findMemberUsers([user.id], {idmFields}) + expect(result.id).to.equal(user.id) + expect(result.handle).to.equal(user.handle) + expect(result.name).to.not.exist + expect(result.email).to.not.exist + }) + + it('returns all users if no identifiers specified', async function () { + useFixture.nockIDMFindUsers(this.users) + const result = await findMemberUsers() + expect(result.length).to.equal(this.users.length) + expectArraysToContainTheSameElements(result.map(u => u.id), this.users.map(p => p.id)) + }) + + it('returns no users if no matching identifiers specified', async function () { + useFixture.nockIDMFindUsers([]) + const result = await findMemberUsers([this.users[0].id]) + expect(result.length).to.equal(0) + }) +}) diff --git a/src/server/actions/__tests__/findUsers.test.js b/src/server/actions/__tests__/findUsers.test.js deleted file mode 100644 index 52cbb739..00000000 --- a/src/server/actions/__tests__/findUsers.test.js +++ /dev/null @@ -1,84 +0,0 @@ -/* eslint-env mocha */ -/* global expect testContext */ -/* eslint-disable prefer-arrow-callback, no-unused-expressions, max-nested-callbacks */ -import factory from 'src/test/factories' -import {resetDB, useFixture} from 'src/test/helpers' -import {expectArraysToContainTheSameElements} from 'src/test/helpers/expectations' - -import findUsers from '../findUsers' - -describe(testContext(__filename), function () { - before(resetDB) - - before(async function () { - this.members = await factory.createMany('member', 5) - this.users = this.members.map(member => ({ - id: member.id, - handle: `handle_${member.id}`, - })) - }) - - beforeEach(function () { - useFixture.nockClean() - }) - - describe('findUsers()', function () { - it('returns correct users for handle identifiers', async function () { - const users = this.users.slice(0, 2) - useFixture.nockIDMFindUsers(users) - const result = await findUsers(users.map(u => u.handle)) - expectArraysToContainTheSameElements(result.map(u => u.id), users.map(u => u.id)) - }) - - it('returns correct users for UUIDs', async function () { - const users = this.members.slice(0, 5) - useFixture.nockIDMFindUsers(users) - const result = await findUsers(users.map(u => u.id)) - expectArraysToContainTheSameElements(result.map(u => u.id), users.map(u => u.id)) - }) - - it('returns only unique users', async function () { - const user = this.users[0] - const {id, handle} = user - useFixture.nockIDMFindUsers([user, user]) - const result = await findUsers([id, handle]) - expect(result.length).to.equal(1) - }) - - it('returns all IDM fields if none specified', async function () { - useFixture.nockIDMFindUsers(this.users) - const user = this.users[0] - const member = this.members.find(p => p.id === user.id) - const [result] = await findUsers([user.id]) - expect(result.id).to.equal(user.id) - expect(result.handle).to.equal(user.handle) - expect(result.name).to.equal(user.name) - expect(result.email).to.equal(user.email) - expect(result.chapterId).to.equal(member.chapterId) - }) - - it('returns only specified IDM fields', async function () { - useFixture.nockIDMFindUsers(this.users) - const user = this.users[0] - const idmFields = ['handle'] - const [result] = await findUsers([user.id], {idmFields}) - expect(result.id).to.equal(user.id) - expect(result.handle).to.equal(user.handle) - expect(result.name).to.not.exist - expect(result.email).to.not.exist - }) - - it('returns all users if no identifiers specified', async function () { - useFixture.nockIDMFindUsers(this.users) - const result = await findUsers() - expect(result.length).to.equal(this.users.length) - expectArraysToContainTheSameElements(result.map(u => u.id), this.users.map(p => p.id)) - }) - - it('returns no users if no matching identifiers specified', async function () { - useFixture.nockIDMFindUsers([]) - const result = await findUsers([this.users[0].id]) - expect(result.length).to.equal(0) - }) - }) -}) diff --git a/src/server/actions/__tests__/getUser.test.js b/src/server/actions/__tests__/getMemberUser.test.js similarity index 82% rename from src/server/actions/__tests__/getUser.test.js rename to src/server/actions/__tests__/getMemberUser.test.js index 2d03fedd..2ccbd3b5 100644 --- a/src/server/actions/__tests__/getUser.test.js +++ b/src/server/actions/__tests__/getMemberUser.test.js @@ -4,7 +4,7 @@ import factory from 'src/test/factories' import {resetDB, useFixture} from 'src/test/helpers' -import getUser from '../getUser' +import getMemberUser from '../getMemberUser' describe(testContext(__filename), function () { beforeEach(resetDB) @@ -13,14 +13,14 @@ describe(testContext(__filename), function () { useFixture.nockClean() }) - it('returns correct user for identifier', async function () { + it('returns correct member for identifier w/ IDM user properties', async function () { const user = await factory.build('user') const member = await factory.create('member', {id: user.id}) await factory.createMany('member', 2) // extra members useFixture.nockIDMGetUser(user) - const result = await getUser(user.id) + const result = await getMemberUser(user.id) expect(result.id).to.equal(user.id) expect(result.handle).to.equal(user.handle) expect(result.email).to.equal(user.email) @@ -32,7 +32,7 @@ describe(testContext(__filename), function () { it('returns null if user exists in IDM but not in echo', async function () { const user = await factory.build('user') useFixture.nockIDMGetUser(user) - const result = await getUser(user.id) + const result = await getMemberUser(user.id) return expect(result).to.not.exist }) }) diff --git a/src/server/actions/__tests__/lockSurveyForMember.test.js b/src/server/actions/__tests__/lockSurveyForMember.test.js new file mode 100644 index 00000000..04f8201a --- /dev/null +++ b/src/server/actions/__tests__/lockSurveyForMember.test.js @@ -0,0 +1,45 @@ +/* eslint-env mocha */ +/* global expect testContext */ +/* eslint-disable prefer-arrow-callback, no-unused-expressions, max-nested-callbacks */ +import {useFixture} from 'src/test/helpers' +import {Survey} from 'src/server/services/dataService' + +import lockSurveyForMember from '../lockSurveyForMember' + +describe(testContext(__filename), function () { + useFixture.buildSurvey() + + beforeEach(async function () { + await this.buildSurvey() + this.memberId = this.project.memberIds[0] + this.surveyId = this.project.retrospectiveSurveyId + }) + + context('when the survey has NOT been completed', function () { + it('throws an error', function () { + return expect( + lockSurveyForMember(this.surveyId, this.memberId) + ).to.be.rejectedWith(/has not been completed/) + }) + }) + context('when the survey is completed and unlocked', function () { + beforeEach(async function () { + this.survey.completedBy.push(this.memberId) + this.survey.unlockedFor = [this.memberId] + await Survey.save(this.survey, {conflict: 'update'}) + }) + + it('removed the member from the unlockedFor array', async function () { + await lockSurveyForMember(this.surveyId, this.memberId) + const updatedSurvey = await Survey.get(this.survey.id) + expect(updatedSurvey.unlockedFor).to.not.include(this.memberId) + }) + + it('does not throw an error if the survey is already locked', async function () { + await lockSurveyForMember(this.surveyId, this.memberId) + await lockSurveyForMember(this.surveyId, this.memberId) + const updatedSurvey = await Survey.get(this.survey.id) + expect(updatedSurvey.unlockedFor).to.not.include(this.memberId) + }) + }) +}) diff --git a/src/server/actions/__tests__/retroSurveyLockUnlock.test.js b/src/server/actions/__tests__/retroSurveyLockUnlock.test.js deleted file mode 100644 index 2dccb29f..00000000 --- a/src/server/actions/__tests__/retroSurveyLockUnlock.test.js +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-env mocha */ -/* global expect testContext */ -/* eslint-disable prefer-arrow-callback, no-unused-expressions, max-nested-callbacks */ -import {useFixture} from 'src/test/helpers' -import {Survey} from 'src/server/services/dataService' - -import {lockRetroSurveyForUser, unlockRetroSurveyForUser} from '../retroSurveyLockUnlock' - -describe(testContext(__filename), function () { - useFixture.buildSurvey() - - beforeEach(async function () { - await this.buildSurvey() - this.memberId = this.project.memberIds[0] - this.projectId = this.project.id - }) - - describe('unlockRetroSurveyForUser()', function () { - context('when the survey has been completed', function () { - beforeEach(async function () { - this.survey.completedBy.push(this.memberId) - await Survey.save(this.survey, {conflict: 'update'}) - }) - - it('adds the member to the unlockedFor array', async function () { - await unlockRetroSurveyForUser(this.memberId, this.projectId) - const updatedSurvey = await Survey.get(this.survey.id) - expect(updatedSurvey.unlockedFor).to.include(this.memberId) - }) - - it('adds the member to the unlockedFor array only once', async function () { - await unlockRetroSurveyForUser(this.memberId, this.projectId) - await unlockRetroSurveyForUser(this.memberId, this.projectId) - const updatedSurvey = await Survey.get(this.survey.id) - const updatedSurveyOnce = updatedSurvey.unlockedFor.filter(id => - id === this.memberId - ).length - expect(updatedSurveyOnce).to.eql(1) - }) - }) - - context('when the survey has NOT been completed', function () { - it('throws an error', function () { - return expect( - unlockRetroSurveyForUser(this.memberId, this.projectId) - ).to.be.rejectedWith(/incomplete/) - }) - }) - }) - - describe('lockRetroSurveyForUser()', function () { - context('when the survey has NOT been completed', function () { - it('throws an error', function () { - return expect( - lockRetroSurveyForUser(this.memberId, this.projectId) - ).to.be.rejectedWith(/incomplete/) - }) - }) - context('when the survey is completed and unlocked', function () { - beforeEach(async function () { - this.survey.completedBy.push(this.memberId) - this.survey.unlockedFor = [this.memberId] - await Survey.save(this.survey, {conflict: 'update'}) - }) - - it('removes the member to the unlockedFor array', async function () { - await lockRetroSurveyForUser(this.memberId, this.projectId) - const updatedSurvey = await Survey.get(this.survey.id) - expect(updatedSurvey.unlockedFor).to.not.include(this.memberId) - }) - - it('does not throw an error if the survey is already locked', async function () { - await lockRetroSurveyForUser(this.memberId, this.projectId) - await lockRetroSurveyForUser(this.memberId, this.projectId) - const updatedSurvey = await Survey.get(this.survey.id) - expect(updatedSurvey.unlockedFor).to.not.include(this.memberId) - }) - }) - }) -}) diff --git a/src/server/actions/__tests__/unlockSurveyForMember.test.js b/src/server/actions/__tests__/unlockSurveyForMember.test.js new file mode 100644 index 00000000..1e08ad74 --- /dev/null +++ b/src/server/actions/__tests__/unlockSurveyForMember.test.js @@ -0,0 +1,46 @@ +/* eslint-env mocha */ +/* global expect testContext */ +/* eslint-disable prefer-arrow-callback, no-unused-expressions, max-nested-callbacks */ +import {useFixture} from 'src/test/helpers' +import {Survey} from 'src/server/services/dataService' + +import unlockSurveyForMember from '../unlockSurveyForMember' + +describe(testContext(__filename), function () { + useFixture.buildSurvey() + + beforeEach(async function () { + await this.buildSurvey() + this.memberId = this.project.memberIds[0] + this.surveyId = this.project.retrospectiveSurveyId + }) + + context('when the survey has been completed', function () { + beforeEach(async function () { + this.survey.completedBy.push(this.memberId) + await Survey.save(this.survey, {conflict: 'update'}) + }) + + it('adds the member to the unlockedFor array', async function () { + await unlockSurveyForMember(this.surveyId, this.memberId) + const updatedSurvey = await Survey.get(this.surveyId) + expect(updatedSurvey.unlockedFor).to.include(this.memberId) + }) + + it('adds the member to the unlockedFor array only once', async function () { + await unlockSurveyForMember(this.surveyId, this.memberId) + await unlockSurveyForMember(this.surveyId, this.memberId) + const updatedSurvey = await Survey.get(this.surveyId) + const updatedSurveyCount = updatedSurvey.unlockedFor.filter(memberId => memberId === this.memberId).length + expect(updatedSurveyCount).to.eql(1) + }) + }) + + context('when the survey has NOT been completed', function () { + it('throws an error', function () { + return expect( + unlockSurveyForMember(this.surveyId, this.memberId) + ).to.be.rejectedWith(/has not been completed/) + }) + }) +}) diff --git a/src/server/actions/assertMembersCurrentCycleInState.js b/src/server/actions/assertMembersCurrentCycleInState.js index 119a5ad8..abd4f1ce 100644 --- a/src/server/actions/assertMembersCurrentCycleInState.js +++ b/src/server/actions/assertMembersCurrentCycleInState.js @@ -1,8 +1,8 @@ import {Member, getLatestCycleForChapter} from 'src/server/services/dataService' import {LGForbiddenError} from 'src/server/util/error' -export default async function assertMembersCurrentCycleInState(currentUser, state) { - const member = await Member.get(currentUser.id).getJoin({chapter: true}) +export default async function assertMembersCurrentCycleInState(memberId, state) { + const member = typeof memberId === 'string' ? await Member.get(memberId).getJoin({chapter: true}) : memberId const cycleInReflection = await getLatestCycleForChapter(member.chapter.id)('state').eq(state) if (!cycleInReflection) { throw new LGForbiddenError(`This action is not allowed when the cycle is not in the ${state} state`) diff --git a/src/server/actions/compileSurveyData.js b/src/server/actions/compileSurveyData.js index f2d13069..a7c45cd2 100644 --- a/src/server/actions/compileSurveyData.js +++ b/src/server/actions/compileSurveyData.js @@ -1,12 +1,13 @@ import {renderQuestionBodies} from 'src/common/models/survey' -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' import {Project, getFullRetrospectiveSurveyForMember} from 'src/server/services/dataService' import {customQueryError} from 'src/server/services/dataService/util' import {LGForbiddenError} from 'src/server/util/error' +import {hashById} from 'src/server/util' export async function compileSurveyDataForMember(memberId, projectId) { const survey = await getFullRetrospectiveSurveyForMember(memberId, projectId) - .then(survey => inflateSurveySubjects(survey)) + .then(survey => _inflateSurveySubjects(survey)) .then(survey => Object.assign({}, survey, { questions: renderQuestionBodies(survey.questions) })) @@ -22,20 +23,20 @@ export function compileSurveyQuestionDataForMember(memberId, questionNumber, pro return getFullRetrospectiveSurveyForMember(memberId, projectId)('questions') .nth(questionNumber - 1) .default(customQueryError(`There is no question number ${questionNumber}`)) - .then(question => inflateSurveyQuestionSubjects([question])) + .then(question => _inflateSurveyQuestionSubjects([question])) .then(questions => renderQuestionBodies(questions)) .then(questions => questions[0]) } -function inflateSurveySubjects(survey) { - return inflateSurveyQuestionSubjects(survey.questions) +function _inflateSurveySubjects(survey) { + return _inflateSurveyQuestionSubjects(survey.questions) .then(questions => Object.assign({}, survey, {questions})) } -async function inflateSurveyQuestionSubjects(questions) { - const subjectIds = getSubjects(questions) - const memberInfo = await getMemberInfoByIds(subjectIds) - const projectInfo = await getProjectInfoByIds(subjectIds) +async function _inflateSurveyQuestionSubjects(questions) { + const subjectIds = _getSubjects(questions) + const memberInfo = await _getMemberUsersByIds(subjectIds) + const projectInfo = await _getProjectInfoByIds(subjectIds) const subjectInfo = {...memberInfo, ...projectInfo} const inflatedQuestions = questions.map(question => { @@ -46,20 +47,23 @@ async function inflateSurveyQuestionSubjects(questions) { return inflatedQuestions } -function getSubjects(questions) { +function _getSubjects(questions) { return questions .reduce((prev, question) => prev.concat(question.subjectIds), []) } -async function getProjectInfoByIds(projectIds = []) { +async function _getProjectInfoByIds(projectIds = []) { const projects = await Project.getAll(...projectIds) - return projects.reduce((result, next) => ({...result, [next.id]: {id: next.id, handle: next.name, name: next.name}}), {}) + return projects.reduce((result, project) => ({ + ...result, + [project.id]: { + id: project.id, + handle: project.name, + name: project.name, + }, + }), {}) } -async function getMemberInfoByIds(memberIds) { - const members = await getMemberInfo(memberIds) - return members.reduce((result, member) => { - result[member.id] = member - return result - }, {}) +async function _getMemberUsersByIds(memberIds) { + return hashById(await findMemberUsers(memberIds)) } diff --git a/src/server/actions/deactivateIDMUser.js b/src/server/actions/deactivateIDMUser.js new file mode 100644 index 00000000..3dc9a603 --- /dev/null +++ b/src/server/actions/deactivateIDMUser.js @@ -0,0 +1,13 @@ +import config from 'src/config' +import graphQLFetcher from 'src/server/util/graphql' + +export default async function deactivateIDMUser(userId) { + const mutation = { + query: 'mutation ($memberId: ID!) { deactivateUser(id: $memberId) { id active handle } }', + variables: {memberId: userId}, + } + + const {data: {deactivateUser: updatedUser}} = await graphQLFetcher(config.server.idm.baseURL)(mutation) + + return updatedUser +} diff --git a/src/server/actions/deactivateMember.js b/src/server/actions/deactivateMember.js new file mode 100644 index 00000000..76a675aa --- /dev/null +++ b/src/server/actions/deactivateMember.js @@ -0,0 +1,15 @@ +import config from 'src/config' +import getMemberUser from 'src/server/actions/getMemberUser' +import deactivateIDMUser from 'src/server/actions/deactivateIDMUser' +import {removeUserFromOrganizations} from 'src/server/services/gitHubService' +import {deactivateUser as deactivateChatUser} from 'src/server/services/chatService' +import {logRejection} from 'src/server/util' + +const githubOrgs = config.server.github.organizations + +export default async function deactivateMember(userId) { + const memberUser = await getMemberUser(userId) + await logRejection(removeUserFromOrganizations(memberUser.handle, githubOrgs), 'Error while removing user from GitHub organizations.') + await logRejection(deactivateChatUser(userId), 'Error while deactivating user in the chat system.') + return deactivateIDMUser(userId) +} diff --git a/src/server/actions/deactivateUser.js b/src/server/actions/deactivateUser.js deleted file mode 100644 index 61c8e111..00000000 --- a/src/server/actions/deactivateUser.js +++ /dev/null @@ -1,30 +0,0 @@ -import config from 'src/config' -import getUser from 'src/server/actions/getUser' -import {removeUserFromOrganizations} from 'src/server/services/gitHubService' -import {removeCollaboratorFromApps} from 'src/server/services/herokuService' -import {deactivateUser as deactivateChatUser} from 'src/server/services/chatService' -import {logRejection} from 'src/server/util' -import graphQLFetcher from 'src/server/util/graphql' - -const githubOrgs = config.server.github.organizations -const losPermissions = (config.losPermissions || {}) - -export default async function deactivateUser(userId) { - const user = await getUser(userId) - const memberHerokuApps = (losPermissions.heroku || {}).apps || [] - await logRejection(removeUserFromOrganizations(user.handle, githubOrgs), 'Error while removing user from GitHub organizations.') - await logRejection(removeCollaboratorFromApps(user, memberHerokuApps), 'Error while removing user from Heroku apps.') - await logRejection(deactivateChatUser(userId), 'Error while deactivating user in the chat system.') - - const {data: {deactivateUser: updatedUser}} = await _deactivateUserInIDM(userId) - - return updatedUser -} - -function _deactivateUserInIDM(userId) { - const mutation = { - query: 'mutation ($memberId: ID!) { deactivateUser(id: $memberId) { id active handle } }', - variables: {memberId: userId}, - } - return graphQLFetcher(config.server.idm.baseURL)(mutation) -} diff --git a/src/server/actions/findActiveMembersForPhase.js b/src/server/actions/findActiveMembersForPhase.js index 2d1a1c39..96a6e611 100644 --- a/src/server/actions/findActiveMembersForPhase.js +++ b/src/server/actions/findActiveMembersForPhase.js @@ -1,11 +1,11 @@ -import findUsers from 'src/server/actions/findUsers' +import findMemberUsers from 'src/server/actions/findMemberUsers' import {Member} from 'src/server/services/dataService' import {mapById} from 'src/common/util' export default async function findActiveMembersForPhase(phaseId) { const phaseMembers = await Member.filter({phaseId}) const phaseMemberIds = phaseMembers.map(m => m.id) - const memberUsers = await findUsers(phaseMemberIds) + const memberUsers = await findMemberUsers(phaseMemberIds) const activeMemberUsers = (memberUsers).filter(u => u.active) return _mergeById(activeMemberUsers, phaseMembers) } diff --git a/src/server/actions/findActiveMembersInChapter.js b/src/server/actions/findActiveMembersInChapter.js index f75c3389..30f43daf 100644 --- a/src/server/actions/findActiveMembersInChapter.js +++ b/src/server/actions/findActiveMembersInChapter.js @@ -1,11 +1,11 @@ import {Member} from 'src/server/services/dataService' import {mapById} from 'src/server/util' -import getMemberInfo from './getMemberInfo' +import findMemberUsers from './findMemberUsers' export default async function findActiveMembersInChapter(chapterId) { const members = await Member.filter({chapterId}) - const memberIds = members.map(_ => _.id) - const idmActiveUsers = (await getMemberInfo(memberIds)).filter(_ => _.active) + const memberIds = members.map(m => m.id) + const idmActiveUsers = (await findMemberUsers(memberIds)).filter(mu => mu.active) const idmActiveUserMap = mapById(idmActiveUsers) return members.filter(member => Boolean(idmActiveUserMap.get(member.id))) } diff --git a/src/server/actions/findActiveVotingMembersInChapter.js b/src/server/actions/findActiveVotingMembersInChapter.js index 55f6d7df..140608e6 100644 --- a/src/server/actions/findActiveVotingMembersInChapter.js +++ b/src/server/actions/findActiveVotingMembersInChapter.js @@ -1,6 +1,6 @@ import {r, Member, Phase} from 'src/server/services/dataService' import {mapById} from 'src/server/util' -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' export default async function findActiveVotingMembersInChapter(chapterId) { const votingPhaseIds = (await Phase.filter({hasVoting: true}).pluck('id')).map(p => p.id) @@ -10,6 +10,6 @@ export default async function findActiveVotingMembersInChapter(chapterId) { votingPhaseIdsExpr.contains(row('phaseId')) )) const votingMemberIds = votingMembers.map(m => m.id) - const idmUserMap = mapById(await getMemberInfo(votingMemberIds)) + const idmUserMap = mapById(await findMemberUsers(votingMemberIds, {skipNoMatch: false})) return votingMembers.filter(m => idmUserMap.get(m.id).active) } diff --git a/src/server/actions/findUserProjectEvaluations.js b/src/server/actions/findMemberProjectEvaluations.js similarity index 77% rename from src/server/actions/findUserProjectEvaluations.js rename to src/server/actions/findMemberProjectEvaluations.js index 28d4837e..97c2b06c 100644 --- a/src/server/actions/findUserProjectEvaluations.js +++ b/src/server/actions/findMemberProjectEvaluations.js @@ -10,10 +10,10 @@ const evaluationFeedbackTypeDescriptors = [ FEEDBACK_TYPE_DESCRIPTORS.TECHNICAL_COMPREHENSION, ] -export default async function findUserProjectEvaluations(userIdentifier, projectIdentifier) { - const user = await (typeof userIdentifier === 'string' ? Member.get(userIdentifier) : userIdentifier) - if (!user || !user.id) { - throw new LGBadRequestError(`User not found for identifier: ${userIdentifier}`) +export default async function findMemberProjectEvaluations(memberIdentifier, projectIdentifier) { + const member = await (typeof memberIdentifier === 'string' ? Member.get(memberIdentifier) : memberIdentifier) + if (!member || !member.id) { + throw new LGBadRequestError(`member not found for identifier: ${memberIdentifier}`) } const project = await (typeof projectIdentifier === 'string' ? Project.get(projectIdentifier) : projectIdentifier) @@ -29,12 +29,12 @@ export default async function findUserProjectEvaluations(userIdentifier, project const retroSurveyResponses = groupById( await Response.filter({ surveyId: retrospectiveSurveyId, - subjectId: user.id, + subjectId: member.id, }) .getJoin({question: {feedbackType: true}}) , 'respondentId') - const userProjectEvaluations = [] + const memberProjectEvaluations = [] retroSurveyResponses.forEach((responses, respondentId) => { // choose create time of earliest response as create time for the evaluation const createdAt = responses.sort((r1, r2) => { @@ -46,8 +46,8 @@ export default async function findUserProjectEvaluations(userIdentifier, project evaluationFeedbackTypeDescriptors.forEach(feedbackTypeDescriptor => { evaluation[feedbackTypeDescriptor] = extractValueForReponseQuestionFeedbackType(responses, feedbackTypeDescriptor) }) - userProjectEvaluations.push(evaluation) + memberProjectEvaluations.push(evaluation) }) - return userProjectEvaluations + return memberProjectEvaluations } diff --git a/src/server/actions/findUsers.js b/src/server/actions/findMemberUsers.js similarity index 67% rename from src/server/actions/findUsers.js rename to src/server/actions/findMemberUsers.js index fb83bd35..31469e15 100644 --- a/src/server/actions/findUsers.js +++ b/src/server/actions/findMemberUsers.js @@ -1,5 +1,5 @@ import config from 'src/config' -import mergeUsers from 'src/server/actions/mergeUsers' +import mergeMembers from 'src/server/actions/mergeMembers' import graphQLFetcher from 'src/server/util/graphql' const defaultIdmFields = [ @@ -7,17 +7,17 @@ const defaultIdmFields = [ 'profileUrl', 'timezone', 'active', 'roles', 'inviteCode' ] -export default function findUsers(identifiers, options) { +export default function findMemberUsers(identifiers, options) { if (Array.isArray(identifiers) && identifiers.length === 0) { return [] } - const {idmFields = defaultIdmFields, join} = options || {} + const {idmFields = defaultIdmFields, join, without} = options || {} const queryFields = Array.isArray(idmFields) ? idmFields.join(', ') : idmFields return graphQLFetcher(config.server.idm.baseURL)({ query: `query ($identifiers: [String]) {findUsers(identifiers: $identifiers) {${queryFields}}}`, variables: {identifiers}, }) - .then(result => mergeUsers(result.data.findUsers || [], {skipNoMatch: true, join})) + .then(result => mergeMembers(result.data.findUsers || [], {skipNoMatch: true, join, without})) } diff --git a/src/server/actions/formProjects.js b/src/server/actions/formProjects.js index a5af85e7..d3933717 100644 --- a/src/server/actions/formProjects.js +++ b/src/server/actions/formProjects.js @@ -129,7 +129,7 @@ async function _buildVotingPool(pool) { const votes = poolVotes.map(({goals, memberId}) => ({memberId, votes: goals.map(({url}) => url)})) const goalsByUrl = _extractGoalsFromVotes(poolVotes) const goals = toArray(goalsByUrl).map(goal => ({goalDescriptor: goal.url, ...goal})) - const userFeedback = await _getUserFeedback([...members.keys()]) + const memberFeedback = await _getMemberFeedback([...members.keys()]) return { poolId: pool.id, @@ -137,11 +137,11 @@ async function _buildVotingPool(pool) { cycleId: pool.cycleId, goals, votes, - userFeedback, + memberFeedback, } } -async function _getUserFeedback(memberIds) { +async function _getMemberFeedback(memberIds) { const pairings = flatten(memberIds.map(respondentId => { const teammates = memberIds.filter(id => id !== respondentId) return teammates.map(subjectId => ({respondentId, subjectId})) @@ -153,13 +153,13 @@ async function _getUserFeedback(memberIds) { {concurrency: 20} ) - const userFeedback = feedbackTuples.reduce((result, {respondentId, subjectId, feedback}) => { + const memberFeedback = feedbackTuples.reduce((result, {respondentId, subjectId, feedback}) => { result.respondentIds[respondentId] = result.respondentIds[respondentId] || {subjectIds: {}} result.respondentIds[respondentId].subjectIds[subjectId] = feedback return result }, {respondentIds: {}}) - return userFeedback + return memberFeedback } function _findVotesForPool(poolId) { diff --git a/src/server/actions/getMemberInfo.js b/src/server/actions/getMemberInfo.js deleted file mode 100644 index 7a03669b..00000000 --- a/src/server/actions/getMemberInfo.js +++ /dev/null @@ -1,9 +0,0 @@ -import config from 'src/config' -import graphQLFetcher from 'src/server/util/graphql' - -export default function getMemberInfo(memberIds) { - return graphQLFetcher(config.server.idm.baseURL)({ - query: 'query ($ids: [ID]!) { getUsersByIds(ids: $ids) { id active handle email name roles profileUrl } }', - variables: {ids: memberIds}, - }).then(result => result ? result.data.getUsersByIds : null) -} diff --git a/src/server/actions/getUser.js b/src/server/actions/getMemberUser.js similarity index 63% rename from src/server/actions/getUser.js rename to src/server/actions/getMemberUser.js index 01e801fb..e0fdfddc 100644 --- a/src/server/actions/getUser.js +++ b/src/server/actions/getMemberUser.js @@ -1,5 +1,5 @@ import config from 'src/config' -import mergeUsers from 'src/server/actions/mergeUsers' +import mergeMembers from 'src/server/actions/mergeMembers' import graphQLFetcher from 'src/server/util/graphql' const defaultIdmFields = [ @@ -7,14 +7,14 @@ const defaultIdmFields = [ 'profileUrl', 'timezone', 'active', 'roles', 'inviteCode' ] -export default function getUser(identifier, options) { - const {idmFields = defaultIdmFields} = options || {} +export default function getMemberUser(identifier, options = {}) { + const {idmFields = defaultIdmFields} = options const queryFields = Array.isArray(idmFields) ? idmFields.join(', ') : idmFields return graphQLFetcher(config.server.idm.baseURL)({ query: `query ($identifier: String!) {getUser(identifier: $identifier) {${queryFields}}}`, variables: {identifier}, }) - .then(result => (result && result.data.getUser ? mergeUsers([result.data.getUser], {skipNoMatch: true}) : [])) + .then(result => (result && result.data.getUser ? mergeMembers([result.data.getUser], {skipNoMatch: true, ...options}) : [])) .then(users => users[0]) } diff --git a/src/server/actions/getSurveyCompletedByMember.js b/src/server/actions/getSurveyCompletedByMember.js new file mode 100644 index 00000000..e1079686 --- /dev/null +++ b/src/server/actions/getSurveyCompletedByMember.js @@ -0,0 +1,10 @@ +import {Survey} from 'src/server/services/dataService' +import {LGBadRequestError} from 'src/server/util/error' + +export default async function getSurveyCompletedByMember(surveyId, memberId) { + const survey = await Survey.get(surveyId) + if (!survey.completedBy.includes(memberId)) { + throw new LGBadRequestError('Survey has not been completed by member') + } + return survey +} diff --git a/src/server/actions/getUsersByHandles.js b/src/server/actions/getUsersByHandles.js deleted file mode 100644 index 7fa2b80a..00000000 --- a/src/server/actions/getUsersByHandles.js +++ /dev/null @@ -1,9 +0,0 @@ -import config from 'src/config' -import graphQLFetcher from 'src/server/util/graphql' - -export default function getUsersByHandles(userHandles) { - return graphQLFetcher(config.server.idm.baseURL)({ - query: 'query ($handles: [String]!) { getUsersByHandles(handles: $handles) { id handle name email roles } }', - variables: {handles: userHandles}, - }).then(result => result.data.getUsersByHandles) -} diff --git a/src/server/actions/importProject.js b/src/server/actions/importProject.js index ad6b23ab..220e341d 100644 --- a/src/server/actions/importProject.js +++ b/src/server/actions/importProject.js @@ -1,5 +1,5 @@ import logger from 'src/server/util/logger' -import findUsers from 'src/server/actions/findUsers' +import findMemberUsers from 'src/server/actions/findMemberUsers' import getChapter from 'src/server/actions/getChapter' import saveProject from 'src/server/actions/saveProject' import {Phase, getCycleForChapter, getProject} from 'src/server/services/dataService' @@ -57,7 +57,7 @@ async function _validateMembers(userIdentifiers = []) { } const userOptions = {idmFields: ['id', 'handle']} - const memberUsers = userIdentifiers.length > 0 ? await findUsers(userIdentifiers, userOptions) : [] + const memberUsers = userIdentifiers.length > 0 ? await findMemberUsers(userIdentifiers, userOptions) : [] const memberPhaseIds = new Map() const members = userIdentifiers.map(userIdentifier => { diff --git a/src/server/actions/initializeProject.js b/src/server/actions/initializeProject.js index 6db0c61e..cf812519 100644 --- a/src/server/actions/initializeProject.js +++ b/src/server/actions/initializeProject.js @@ -1,6 +1,6 @@ import {COMPLETE} from 'src/common/models/cycle' import logger from 'src/server/util/logger' -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' import initializeChannel from 'src/server/actions/initializeChannel' import sendProjectWelcomeMessages from 'src/server/actions/sendProjectWelcomeMessages' import {LGBadRequestError} from 'src/server/util/error' @@ -26,13 +26,14 @@ export default async function initializeProject(project) { logger.log(`Initializing project #${project.name}`) - const members = await getMemberInfo(project.memberIds) - const memberHandles = members.map(p => p.handle) + const memberUsers = await findMemberUsers(project.memberIds) + const memberChatUsernames = memberUsers.map(mu => mu.handle) const channelName = String(project.goal.number) const channelTopic = `${project.goal.title} (${project.goal.url})` if (phase.hasVoting) { - await initializeChannel(channelName, {topic: channelTopic, users: memberHandles}) + await initializeChannel(channelName, {topic: channelTopic, users: memberChatUsernames}) } - await sendProjectWelcomeMessages(project, {members}) + + await sendProjectWelcomeMessages(project, {memberUsers}) } diff --git a/src/server/actions/lockSurveyForMember.js b/src/server/actions/lockSurveyForMember.js new file mode 100644 index 00000000..78f31884 --- /dev/null +++ b/src/server/actions/lockSurveyForMember.js @@ -0,0 +1,12 @@ +import getSurveyCompletedByMember from 'src/server/actions/getSurveyCompletedByMember' +import {Survey} from 'src/server/services/dataService' +import {without} from 'src/common/util' + +export default async function lockSurveyForMember(surveyId, memberId) { + const survey = await getSurveyCompletedByMember(surveyId, memberId) + await Survey + .get(survey.id) + .updateWithTimestamp({ + unlockedFor: without(survey.unlockedFor, memberId) + }) +} diff --git a/src/server/actions/mergeUsers.js b/src/server/actions/mergeMembers.js similarity index 59% rename from src/server/actions/mergeUsers.js rename to src/server/actions/mergeMembers.js index cb9212cc..4440d9e7 100644 --- a/src/server/actions/mergeUsers.js +++ b/src/server/actions/mergeMembers.js @@ -2,7 +2,7 @@ import {Member} from 'src/server/services/dataService' import {mapById} from 'src/server/util' import {LGBadRequestError} from 'src/server/util/error' -export default async function mergeUsers(users, options) { +export default async function mergeMembers(users, options) { if (!Array.isArray(users)) { throw new LGBadRequestError('Invalid users input:', users) } @@ -10,9 +10,10 @@ export default async function mergeUsers(users, options) { return [] } - const {skipNoMatch, join} = options || {} + const {skipNoMatch, join, without} = options || {} const userIds = users.map(u => u.id) - const members = mapById(await _getAll(Member, userIds, {join})) + + const members = mapById(await _getAll(Member, userIds, {join, without})) return Object.values(users.reduce((result, user) => { const echoUser = members.get(user.id) @@ -20,14 +21,19 @@ export default async function mergeUsers(users, options) { // only return in results if user has an echo account result[user.id] = Object.assign({}, user, echoUser) } else if (!skipNoMatch) { - throw new LGBadRequestError(`User not found for id ${user.id}, user merge aborted`) + throw new LGBadRequestError(`Member not found for user id ${user.id}, merge aborted`) } return result }, {})) } function _getAll(Model, ids, options = {}) { - return options.join ? - Model.getAll(...ids).getJoin(options.join) : - Model.getAll(...ids) + let query = Model.getAll(...ids) + if (options.join) { + query = query.getJoin(options.join) + } + if (options.without) { + query = query.without(options.without) + } + return query } diff --git a/src/server/actions/retroSurveyLockUnlock.js b/src/server/actions/retroSurveyLockUnlock.js deleted file mode 100644 index 08e35ea4..00000000 --- a/src/server/actions/retroSurveyLockUnlock.js +++ /dev/null @@ -1,30 +0,0 @@ -import {Project, Survey} from 'src/server/services/dataService' -import {LGBadRequestError} from 'src/server/util/error' - -export async function unlockRetroSurveyForUser(memberId, projectId) { - const survey = await _getCompletedRetrospectiveSurvey(memberId, projectId) - const unlockedFor = _without(survey.unlockedFor, memberId) - unlockedFor.push(memberId) - await Survey.get(survey.id).updateWithTimestamp({unlockedFor}) -} - -export async function lockRetroSurveyForUser(memberId, projectId) { - const survey = await _getCompletedRetrospectiveSurvey(memberId, projectId) - await Survey - .get(survey.id) - .updateWithTimestamp({ - unlockedFor: _without(survey.unlockedFor, memberId) - }) -} - -async function _getCompletedRetrospectiveSurvey(memberId, projectId) { - const {retrospectiveSurvey} = await Project.get(projectId).getJoin({retrospectiveSurvey: true}) - if (!retrospectiveSurvey.completedBy.includes(memberId)) { - throw new LGBadRequestError('Cannot lock or unlock an incomplete survey') - } - return retrospectiveSurvey -} - -function _without(values, excludedValue) { - return (values || []).filter(value => value !== excludedValue) -} diff --git a/src/server/actions/sendCycleReflectionAnnouncements.js b/src/server/actions/sendCycleReflectionAnnouncements.js index 7b1ac1a5..255b0809 100644 --- a/src/server/actions/sendCycleReflectionAnnouncements.js +++ b/src/server/actions/sendCycleReflectionAnnouncements.js @@ -1,5 +1,5 @@ import Promise from 'bluebird' -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' import {Cycle, Phase, Project} from 'src/server/services/dataService' export default async function sendCycleReflectionAnnouncements(cycleId) { @@ -30,10 +30,10 @@ async function _sendAnnouncementToPhaseMembers(cycle, phase, message) { result[project.memberIds] = true // in case anyone is in multiple projects return result }, {})) - const phaseMembers = await getMemberInfo(phaseProjectMemberIds) - const phaseMemberHandles = phaseMembers.map(u => u.handle) + const phaseMemberUsers = await findMemberUsers(phaseProjectMemberIds) + const phaseMemberChatUsernames = phaseMemberUsers.map(u => u.handle) try { - await chatService.sendDirectMessage(phaseMemberHandles, message) + await chatService.sendDirectMessage(phaseMemberChatUsernames, message) } catch (err) { console.warn(`Failed to send cycle reflection announcement to Phase ${phase.number} for cycle ${cycle.cycleNumber}: ${err}`) } diff --git a/src/server/actions/sendProjectWelcomeMessages.js b/src/server/actions/sendProjectWelcomeMessages.js index 7580c078..d8c367f8 100644 --- a/src/server/actions/sendProjectWelcomeMessages.js +++ b/src/server/actions/sendProjectWelcomeMessages.js @@ -1,5 +1,5 @@ import config from 'src/config' -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' import {LGBadRequestError} from 'src/server/util/error' export default async function sendProjectWelcomeMessages(project, options = {}) { @@ -17,14 +17,14 @@ export default async function sendProjectWelcomeMessages(project, options = {}) return } - const projectMembers = options.members || await getMemberInfo(project.memberIds) - const projectMemberHandles = projectMembers.map(u => u.handle) + const projectMemberUsers = options.memberUsers || await findMemberUsers(project.memberIds) + const projectMemberUserHandles = projectMemberUsers.map(mu => mu.handle) const message = phase.hasVoting === true ? - _buildGoalProjectMessage(project, projectMembers) : + _buildGoalProjectMessage(project, projectMemberUsers) : _buildPhaseProjectMessage(project, phase) try { - await chatService.sendDirectMessage(projectMemberHandles, message) + await chatService.sendDirectMessage(projectMemberUserHandles, message) } catch (err) { console.warn(err) } @@ -41,11 +41,11 @@ You should find everything you need to guide you in your work at the resources b ` } -function _buildGoalProjectMessage(project, projectMembers) { +function _buildGoalProjectMessage(project, projectMemberUsers) { const goalLink = `<${project.goal.url}|${project.goal.number}: ${project.goal.title}>` - const teamMembers = (projectMembers.length > 1 ? ` + const teamMembers = (projectMemberUsers.length > 1 ? ` *Your team is:* -${projectMembers.map(u => `• _${u.name}_ - @${u.handle}`).join('\n ')} +${projectMemberUsers.map(u => `• _${u.name}_ - @${u.handle}`).join('\n ')} ` : '') return ` diff --git a/src/server/actions/sendRetroCompletedNotification.js b/src/server/actions/sendRetroCompletedNotification.js index 71393eb2..dc46e890 100644 --- a/src/server/actions/sendRetroCompletedNotification.js +++ b/src/server/actions/sendRetroCompletedNotification.js @@ -1,41 +1,23 @@ import config from 'src/config' -import getMemberInfo from 'src/server/actions/getMemberInfo' -import {Member} from 'src/server/services/dataService' +import findMemberUsers from 'src/server/actions/findMemberUsers' export default async function sendRetroCompletedNotification(project) { const chatService = require('src/server/services/chatService') - const projectMembers = await Member.getAll(...project.memberIds) - const projectMemberUsers = await getMemberInfo(project.memberIds) - const members = _mergeMemberUsers(projectMembers, projectMemberUsers) + const projectMemberUsers = await findMemberUsers(project.memberIds) - return Promise.all(members.map(member => { - const retroNotificationMessage = _compileMemberNotificationMessage(member, project) - - return chatService.sendDirectMessage(member.handle, retroNotificationMessage).catch(err => { - console.error(`\n\nThere was a problem while sending a retro notification to member @${member.handle}`) + return Promise.all(projectMemberUsers.map(memberUser => { + const retroNotificationMessage = _compileMemberNotificationMessage(project) + const memberChatUsername = memberUser.handle + return chatService.sendDirectMessage(memberChatUsername, retroNotificationMessage).catch(err => { + console.error(`There was a problem while sending a retro notification to chat user @${memberChatUsername}`) console.error('Error:', err, err.stack) console.error(`Message: "${retroNotificationMessage}"`) }) })) } -function _mergeMemberUsers(members, users) { - const combined = new Map() - - members.forEach(member => combined.set(member.id, Object.assign({}, member))) - - users.forEach(user => { - const values = combined.get(user.id) - if (values) { - combined.set(user.id, Object.assign({}, values, user)) - } - }) - - return Array.from(combined.values()) -} - -function _compileMemberNotificationMessage(member, project) { +function _compileMemberNotificationMessage(project) { return ( `**RETROSPECTIVE COMPLETE:** diff --git a/src/server/actions/unlockSurveyForMember.js b/src/server/actions/unlockSurveyForMember.js new file mode 100644 index 00000000..8116e567 --- /dev/null +++ b/src/server/actions/unlockSurveyForMember.js @@ -0,0 +1,10 @@ +import getSurveyCompletedByMember from 'src/server/actions/getSurveyCompletedByMember' +import {Survey} from 'src/server/services/dataService' +import {without} from 'src/common/util' + +export default async function unlockSurveyForMember(surveyId, memberId) { + const survey = await getSurveyCompletedByMember(surveyId, memberId) + const unlockedFor = without(survey.unlockedFor, memberId) + unlockedFor.push(memberId) + await Survey.get(survey.id).updateWithTimestamp({unlockedFor}) +} diff --git a/src/server/actions/updateUser.js b/src/server/actions/updateMember.js similarity index 53% rename from src/server/actions/updateUser.js rename to src/server/actions/updateMember.js index bd337da6..d6a4df6d 100644 --- a/src/server/actions/updateUser.js +++ b/src/server/actions/updateMember.js @@ -1,8 +1,8 @@ import {Member, Phase} from 'src/server/services/dataService' -export default async function updateUser(values) { +export default async function updateMember(values) { + // FIXME: why does this only update the phase number?? const phaseNumber = values.phaseNumber const phase = phaseNumber ? await Phase.filter({number: phaseNumber}).nth(0) : {id: null} - const member = await Member.get(values.id).update({phaseId: phase.id}) - return member + return Member.get(values.id).update({phaseId: phase.id}) } diff --git a/src/server/cliCommand/commands/__tests__/project.test.js b/src/server/cliCommand/commands/__tests__/project.test.js index 60494465..c9b6c30b 100644 --- a/src/server/cliCommand/commands/__tests__/project.test.js +++ b/src/server/cliCommand/commands/__tests__/project.test.js @@ -10,7 +10,7 @@ import {concatResults} from './helpers' describe(testContext(__filename), function () { useFixture.ensureNoGlobalWindow() - useFixture.setCurrentCycleAndUserForProject() + useFixture.setCurrentCycleAndMemberForProject() beforeEach(resetDB) @@ -24,7 +24,7 @@ describe(testContext(__filename), function () { }) it('returns a "Thanks" message on success with a project name', async function () { - await this.setCurrentCycleAndUserForProject(this.project) + await this.setCurrentCycleAndMemberForProject(this.project) const args = this.commandSpec.parse(['set-artifact', this.project.name, this.url]) const result = await this.commandImpl.invoke(args, {user: this.currentUser}) @@ -35,7 +35,7 @@ describe(testContext(__filename), function () { }) it('returns a "Thanks" message on success without a project name', async function () { - await this.setCurrentCycleAndUserForProject(this.project) + await this.setCurrentCycleAndMemberForProject(this.project) const args = this.commandSpec.parse(['set-artifact', this.url]) const result = await this.commandImpl.invoke(args, {user: this.currentUser}) @@ -46,14 +46,14 @@ describe(testContext(__filename), function () { }) it('throws an error if the member passes an invalid project name', async function () { - await this.setCurrentCycleAndUserForProject(this.project) + await this.setCurrentCycleAndMemberForProject(this.project) const args = this.commandSpec.parse(['set-artifact', 'invalid-name', this.url]) expect(this.commandImpl.invoke(args, {user: this.currentUser})).to.eventually.throw(/No such project/i) }) it('throws an error if the member does not pass a project name and is not on exactly 1 active project', async function () { - await this.setCurrentCycleAndUserForProject(this.project) + await this.setCurrentCycleAndMemberForProject(this.project) await factory.create('project', {chapterId: this.project.chapterId, cycleId: this.project.cycleId, memberIds: [this.member.id]}) const args = this.commandSpec.parse(['set-artifact', this.url]) @@ -61,7 +61,7 @@ describe(testContext(__filename), function () { }) it('throws an error if the member did not work on the given project', async function () { - await this.setCurrentCycleAndUserForProject(this.project) + await this.setCurrentCycleAndMemberForProject(this.project) const inactiveMember = await factory.create('member', {chapterId: this.project.chapterId}) const currentUser = await factory.build('user', {id: inactiveMember.id, roles: ['member']}) diff --git a/src/server/cliCommand/commands/cycle.js b/src/server/cliCommand/commands/cycle.js index 48c868f9..298c68d8 100644 --- a/src/server/cliCommand/commands/cycle.js +++ b/src/server/cliCommand/commands/cycle.js @@ -1,7 +1,5 @@ import {CYCLE_STATES, PRACTICE, REFLECTION, COMPLETE} from 'src/common/models/cycle' import {userCan} from 'src/common/util' -import getUser from 'src/server/actions/getUser' - import assertUserIsMember from 'src/server/actions/assertUserIsMember' import createNextCycleForChapter from 'src/server/actions/createNextCycleForChapter' import {Cycle, getCyclesInStateForChapter, getLatestCycleForChapter} from 'src/server/services/dataService' @@ -14,15 +12,18 @@ import { const subcommands = { async init(args, {user}) { - const mergedUser = await getUser(user.id) + if (!userCan(user, 'createCycle')) { + throw new LGNotAuthorizedError() + } - const currentCycle = await getLatestCycleForChapter(mergedUser.chapterId) + const member = await assertUserIsMember(user.id) + const currentCycle = await getLatestCycleForChapter(member.chapterId) if (currentCycle.state !== REFLECTION && currentCycle.state !== COMPLETE) { throw new LGBadRequestError('Failed to initialize a new cycle because the current cycle is still in progress.') } - await _createCycle(mergedUser) + await createNextCycleForChapter(member.chapterId) return { text: '🔃 Initializing Cycle ... stand by.' @@ -55,15 +56,6 @@ export async function invoke(args, options) { throw new LGCLIUsageError() } -async function _createCycle(user) { - if (!userCan(user, 'createCycle')) { - throw new LGNotAuthorizedError() - } - - const member = await assertUserIsMember(user.id) - return await createNextCycleForChapter(member.chapterId) -} - async function _changeCycleState(user, newState) { const newStateIndex = CYCLE_STATES.indexOf(newState) if (!userCan(user, 'updateCycle')) { diff --git a/src/server/cliCommand/index.js b/src/server/cliCommand/index.js index af0b5da0..64b5b55d 100644 --- a/src/server/cliCommand/index.js +++ b/src/server/cliCommand/index.js @@ -2,7 +2,7 @@ import express from 'express' import raven from 'raven' import config from 'src/config' -import getUser from 'src/server/actions/getUser' +import getMemberUser from 'src/server/actions/getMemberUser' import { LGCLIUsageError, LGNotAuthorizedError, @@ -21,7 +21,7 @@ async function authenticateCommand(req, res, next) { throw new LGNotAuthorizedError('Your CLI authorization token does not match.') } } else { - req.user = await getUser(handle) + req.user = await getMemberUser(handle) } next() } catch (err) { diff --git a/src/server/graphql/mutations/deactivateUser.js b/src/server/graphql/mutations/deactivateMember.js similarity index 58% rename from src/server/graphql/mutations/deactivateUser.js rename to src/server/graphql/mutations/deactivateMember.js index f86efa4f..b5826270 100644 --- a/src/server/graphql/mutations/deactivateUser.js +++ b/src/server/graphql/mutations/deactivateMember.js @@ -1,20 +1,20 @@ import {GraphQLNonNull, GraphQLID} from 'graphql' -import deactivateUser from 'src/server/actions/deactivateUser' +import deactivateMember from 'src/server/actions/deactivateMember' import {userCan} from 'src/common/util' -import {UserProfile} from 'src/server/graphql/schemas' +import {Member} from 'src/server/graphql/schemas' import {LGNotAuthorizedError} from 'src/server/util/error' export default { - type: UserProfile, + type: Member, args: { - identifier: {type: new GraphQLNonNull(GraphQLID), description: 'The user ID'} + identifier: {type: new GraphQLNonNull(GraphQLID), description: 'The member ID'} }, async resolve(source, {identifier}, {rootValue: {currentUser}}) { - if (!userCan(currentUser, 'deactivateUser')) { - throw new LGNotAuthorizedError('You are not authorized to deactivate users.') + if (!userCan(currentUser, 'deactivateMember')) { + throw new LGNotAuthorizedError('You are not authorized to deactivate members.') } - return await deactivateUser(identifier) + return await deactivateMember(identifier) } } diff --git a/src/server/graphql/mutations/lockRetroSurveyForUser.js b/src/server/graphql/mutations/lockRetroSurveyForUser.js deleted file mode 100644 index b8b9dfcd..00000000 --- a/src/server/graphql/mutations/lockRetroSurveyForUser.js +++ /dev/null @@ -1,31 +0,0 @@ -import {GraphQLNonNull, GraphQLID} from 'graphql' -import {lockRetroSurveyForUser} from 'src/server/actions/retroSurveyLockUnlock' -import userCan from 'src/common/util/userCan' -import {Project} from 'src/server/services/dataService/models' -import {LGNotAuthorizedError} from 'src/server/util/error' - -import {ProjectSummary} from 'src/server/graphql/schemas' - -export default { - type: ProjectSummary, - args: { - memberId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The memberId of the member whose survey should be locked', - }, - projectId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The projects id of the survey to lock for this given member', - }, - }, - async resolve(source, {memberId, projectId}, {rootValue: {currentUser}}) { - if (!userCan(currentUser, 'lockAndUnlockSurveys')) { - throw new LGNotAuthorizedError() - } - - await lockRetroSurveyForUser(memberId, projectId) - return { - project: await Project.get(projectId) - } - } -} diff --git a/src/server/graphql/mutations/lockSurveyForMember.js b/src/server/graphql/mutations/lockSurveyForMember.js new file mode 100644 index 00000000..56998a52 --- /dev/null +++ b/src/server/graphql/mutations/lockSurveyForMember.js @@ -0,0 +1,27 @@ +import {GraphQLNonNull, GraphQLID} from 'graphql' +import lockSurveyForMember from 'src/server/actions/lockSurveyForMember' +import userCan from 'src/common/util/userCan' +import {LGNotAuthorizedError} from 'src/server/util/error' + +import {ProjectSummary} from 'src/server/graphql/schemas' + +export default { + type: ProjectSummary, + args: { + surveyId: { + type: new GraphQLNonNull(GraphQLID), + description: 'The ID of the survey to lock for the member', + }, + memberId: { + type: new GraphQLNonNull(GraphQLID), + description: 'The ID of the member to lock the survey for', + }, + }, + async resolve(source, {surveyId, memberId}, {rootValue: {currentUser}}) { + if (!userCan(currentUser, 'lockAndUnlockSurveys')) { + throw new LGNotAuthorizedError() + } + + return lockSurveyForMember(surveyId, memberId) + } +} diff --git a/src/server/graphql/mutations/reassignMembersToChapter.js b/src/server/graphql/mutations/reassignMembersToChapter.js index 625be6c0..9255e8be 100644 --- a/src/server/graphql/mutations/reassignMembersToChapter.js +++ b/src/server/graphql/mutations/reassignMembersToChapter.js @@ -4,11 +4,11 @@ import {GraphQLList} from 'graphql/type' import {userCan} from 'src/common/util' import reassignMembersToChapter from 'src/server/actions/reassignMembersToChapter' import {Chapter, errors} from 'src/server/services/dataService' -import {User} from 'src/server/graphql/schemas' +import {Member} from 'src/server/graphql/schemas' import {LGNotAuthorizedError, LGBadRequestError} from 'src/server/util/error' export default { - type: new GraphQLList(User), + type: new GraphQLList(Member), args: { memberIds: {type: new GraphQLList(GraphQLID)}, chapterId: {type: new GraphQLNonNull(GraphQLID)}, diff --git a/src/server/graphql/mutations/unlockRetroSurveyForUser.js b/src/server/graphql/mutations/unlockRetroSurveyForUser.js deleted file mode 100644 index 0fe645e6..00000000 --- a/src/server/graphql/mutations/unlockRetroSurveyForUser.js +++ /dev/null @@ -1,31 +0,0 @@ -import {GraphQLNonNull, GraphQLID} from 'graphql' -import {unlockRetroSurveyForUser} from 'src/server/actions/retroSurveyLockUnlock' -import userCan from 'src/common/util/userCan' -import {Project} from 'src/server/services/dataService/models' -import {LGNotAuthorizedError} from 'src/server/util/error' - -import {ProjectSummary} from 'src/server/graphql/schemas' - -export default { - type: ProjectSummary, - args: { - memberId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The memberId of the member whose survey should be unlocked', - }, - projectId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The projects id of the survey to unlock for this given member', - }, - }, - async resolve(source, {memberId, projectId}, {rootValue: {currentUser}}) { - if (!userCan(currentUser, 'lockAndUnlockSurveys')) { - throw new LGNotAuthorizedError() - } - - await unlockRetroSurveyForUser(memberId, projectId) - return { - project: await Project.get(projectId) - } - } -} diff --git a/src/server/graphql/mutations/unlockSurveyForMember.js b/src/server/graphql/mutations/unlockSurveyForMember.js new file mode 100644 index 00000000..cdf43123 --- /dev/null +++ b/src/server/graphql/mutations/unlockSurveyForMember.js @@ -0,0 +1,27 @@ +import {GraphQLNonNull, GraphQLID} from 'graphql' +import unlockSurveyForMember from 'src/server/actions/unlockSurveyForMember' +import userCan from 'src/common/util/userCan' +import {LGNotAuthorizedError} from 'src/server/util/error' + +import {ProjectSummary} from 'src/server/graphql/schemas' + +export default { + type: ProjectSummary, + args: { + surveyId: { + type: new GraphQLNonNull(GraphQLID), + description: 'The ID of the survey to unlock for the member', + }, + memberId: { + type: new GraphQLNonNull(GraphQLID), + description: 'The ID of the member to unlock the survey for', + }, + }, + async resolve(source, {surveyId, memberId}, {rootValue: {currentUser}}) { + if (!userCan(currentUser, 'lockAndUnlockSurveys')) { + throw new LGNotAuthorizedError() + } + + return unlockSurveyForMember(surveyId, memberId) + } +} diff --git a/src/server/graphql/mutations/updateMember.js b/src/server/graphql/mutations/updateMember.js new file mode 100644 index 00000000..bbfca293 --- /dev/null +++ b/src/server/graphql/mutations/updateMember.js @@ -0,0 +1,22 @@ +import {GraphQLNonNull} from 'graphql' + +import {userCan} from 'src/common/util' +import {Member, InputMember} from 'src/server/graphql/schemas' +import updateMember from 'src/server/actions/updateMember' +import getMemberUser from 'src/server/actions/getMemberUser' +import {LGNotAuthorizedError} from 'src/server/util/error' + +export default { + type: Member, + args: { + values: {type: new GraphQLNonNull(InputMember)}, + }, + async resolve(source, {values}, {rootValue: {currentUser}}) { + if (!userCan(currentUser, 'updateMember')) { + throw new LGNotAuthorizedError() + } + + await updateMember(values) + return getMemberUser(values.id) + } +} diff --git a/src/server/graphql/mutations/updateUser.js b/src/server/graphql/mutations/updateUser.js deleted file mode 100644 index 77e5cbd6..00000000 --- a/src/server/graphql/mutations/updateUser.js +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLNonNull} from 'graphql' - -import {userCan} from 'src/common/util' -import {UserProfile, InputUser} from 'src/server/graphql/schemas' -import updateUser from 'src/server/actions/updateUser' -import getUser from 'src/server/actions/getUser' -import {LGNotAuthorizedError} from 'src/server/util/error' - -export default { - type: UserProfile, - args: { - values: {type: new GraphQLNonNull(InputUser)}, - }, - async resolve(source, {values}, {rootValue: {currentUser}}) { - if (!userCan(currentUser, 'updateUser')) { - throw new LGNotAuthorizedError() - } - - await updateUser(values) - return await getUser(values.id) - } -} diff --git a/src/server/graphql/queries/__tests__/findMembers.test.js b/src/server/graphql/queries/__tests__/findMembers.test.js index 898703f3..ec680931 100644 --- a/src/server/graphql/queries/__tests__/findMembers.test.js +++ b/src/server/graphql/queries/__tests__/findMembers.test.js @@ -2,41 +2,52 @@ /* global expect, testContext */ /* eslint-disable prefer-arrow-callback, no-unused-expressions */ import factory from 'src/test/factories' -import {resetDB, runGraphQLQuery} from 'src/test/helpers' +import {resetDB, runGraphQLQuery, mockIdmUsersById} from 'src/test/helpers' import fields from '../index' +const query = ` + query($identifiers: [String]) { + findMembers(identifiers: $identifiers) { + id name handle avatarUrl + chapter { id name } + phase { id number } + } + } +` + describe(testContext(__filename), function () { beforeEach(resetDB) - before(function () { - this.graphQLQuery = 'query { findMembers {id} }' - }) - - it('returns all members', async function () { - await factory.createMany('member', 3) - const results = await runGraphQLQuery( - this.graphQLQuery, - fields - ) - expect(results.data.findMembers.length).to.equal(3) + beforeEach('Setup', async function () { + this.currentUser = await factory.build('user') + this.chapter = await factory.create('chapter') + this.phase = await factory.create('phase') + this.members = await factory.createMany('member', 3, {phaseId: this.phase.id, chapterId: this.chapter.id}) + this.users = await mockIdmUsersById(this.members.map(m => m.id)) + await factory.createMany('member', 5) // extra members }) - it('returns an empty array if there are no members', async function () { - const results = await runGraphQLQuery( - this.graphQLQuery, - fields + it('returns correct members for identifiers with chapters and phases', async function () { + const result = await runGraphQLQuery( + query, + fields, + {identifiers: this.members.map(m => m.handle)}, + {currentUser: this.currentUser}, ) - expect(results.data.findMembers.length).to.equal(0) + expect(result.data.findMembers.length).to.equal(this.members.length) + const inputUser = this.users[0] + const inputMember = this.members[0] + const fetchedMember = result.data.findMembers.find(m => m.id === inputUser.id) + expect(fetchedMember.id).to.equal(inputUser.id) + expect(fetchedMember.name).to.equal(inputUser.name) + expect(fetchedMember.avatarUrl).to.equal(inputUser.avatarUrl) + expect(fetchedMember.chapter.id).to.equal(inputMember.chapterId) + expect(fetchedMember.phase.id).to.equal(inputMember.phaseId) }) it('throws an error if user is not signed-in', function () { - const promise = runGraphQLQuery( - this.graphQLQuery, - fields, - {id: 'not.a.real.id'}, - {currentUser: null} - ) - return expect(promise).to.eventually.be.rejectedWith(/not authorized/i) + const result = runGraphQLQuery(query, fields, null, {currentUser: null}) + return expect(result).to.eventually.be.rejectedWith(/not authorized/i) }) }) diff --git a/src/server/graphql/queries/__tests__/findUsers.test.js b/src/server/graphql/queries/__tests__/findUsers.test.js deleted file mode 100644 index 235a2533..00000000 --- a/src/server/graphql/queries/__tests__/findUsers.test.js +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-env mocha */ -/* global expect, testContext */ -/* eslint-disable prefer-arrow-callback, no-unused-expressions */ -import factory from 'src/test/factories' -import {resetDB, runGraphQLQuery, useFixture} from 'src/test/helpers' - -import fields from '../index' - -const query = ` - query($identifiers: [String]) { - findUsers(identifiers: $identifiers) { - id name handle avatarUrl - chapter { id name } - } - } -` - -describe(testContext(__filename), function () { - beforeEach(resetDB) - - beforeEach('Create current user', async function () { - this.currentUser = await factory.build('user') - this.users = await factory.buildMany('user', 3) - this.member = await factory.create('member', {id: this.users[0].id}) - await factory.createMany('member', 5) // extra members - }) - - it('returns correct users with resolved chapters for identifiers', async function () { - const member = this.member - const user = this.users[0] - useFixture.nockIDMFindUsers([user]) - const result = await runGraphQLQuery( - query, - fields, - {identifiers: [member.id]}, - {currentUser: this.currentUser}, - ) - expect(result.data.findUsers.length).to.equal(1) - const [returned] = result.data.findUsers - expect(returned.id).to.equal(user.id) - expect(returned.name).to.equal(user.name) - expect(returned.avatarUrl).to.equal(user.avatarUrl) - expect(returned.chapter.id).to.equal(member.chapterId) - }) - - it('throws an error if user is not signed-in', function () { - const result = runGraphQLQuery(query, fields, null, {currentUser: null}) - return expect(result).to.eventually.be.rejectedWith(/not authorized/i) - }) -}) diff --git a/src/server/graphql/queries/__tests__/getCycleVotingResults.test.js b/src/server/graphql/queries/__tests__/getCycleVotingResults.test.js index 049429fe..0a8e730a 100644 --- a/src/server/graphql/queries/__tests__/getCycleVotingResults.test.js +++ b/src/server/graphql/queries/__tests__/getCycleVotingResults.test.js @@ -50,7 +50,7 @@ describe(testContext(__filename), function () { id number }, - users { id }, + members { id }, voterMemberIds, candidateGoals { goal {url}, @@ -115,7 +115,7 @@ describe(testContext(__filename), function () { const responsePool = response.pools.find(({name}) => name === pool.name) expect(responsePool.name).to.equal(pool.name) expect(responsePool.votingIsStillOpen).to.be.true - expect(responsePool.users.map(_ => _.id).sort(), 'members').to.deep.equal(this.poolMembers[i].map(_ => _.id).sort()) + expect(responsePool.members.map(_ => _.id).sort(), 'members').to.deep.equal(this.poolMembers[i].map(_ => _.id).sort()) expect(responsePool.voterMemberIds.sort(), 'voterMemberIds').to.deep.equal(this.poolVoters[i].map(_ => _.id).sort()) expect(responsePool.candidateGoals[0].goal.url).to.match(new RegExp(`/${voteDataForPools[i].firstPlaceGoalNumber}$`)) expect(responsePool.candidateGoals[1].goal.url).to.match(new RegExp(`/${voteDataForPools[i].secondPlaceGoalNumber}$`)) diff --git a/src/server/graphql/queries/__tests__/getUser.test.js b/src/server/graphql/queries/__tests__/getMember.test.js similarity index 84% rename from src/server/graphql/queries/__tests__/getUser.test.js rename to src/server/graphql/queries/__tests__/getMember.test.js index 2f97a0f4..42996443 100644 --- a/src/server/graphql/queries/__tests__/getUser.test.js +++ b/src/server/graphql/queries/__tests__/getMember.test.js @@ -8,7 +8,7 @@ import fields from '../index' const query = ` query($identifier: String!) { - getUser(identifier: $identifier) { + getMember(identifier: $identifier) { id name handle email avatarUrl profileUrl chapter { id name } } @@ -22,7 +22,7 @@ describe(testContext(__filename), function () { this.currentUser = await factory.build('user') }) - it('returns correct user with chapter for identifier', async function () { + it('returns correct member with chapter for identifier', async function () { const user = this.currentUser const member = await factory.create('member', {id: user.id}) await factory.createMany('member', 2) // extra members @@ -35,7 +35,7 @@ describe(testContext(__filename), function () { {identifier: user.handle}, {currentUser: this.currentUser}, ) - const returned = result.data.getUser + const returned = result.data.getMember expect(returned.id).to.equal(user.id) expect(returned.handle).to.equal(user.handle) expect(returned.email).to.equal(user.email) @@ -44,7 +44,7 @@ describe(testContext(__filename), function () { expect(returned.chapter.id).to.equal(member.chapterId) }) - it('throws an error if user is not found', function () { + it('throws an error if member is not found', function () { useFixture.nockIDMGetUser(null) const result = runGraphQLQuery( query, @@ -52,7 +52,7 @@ describe(testContext(__filename), function () { {identifier: 'fake.identifier'}, {currentUser: this.currentUser}, ) - return expect(result).to.eventually.be.rejectedWith(/User not found/i) + return expect(result).to.eventually.be.rejectedWith(/Member not found/i) }) it('throws an error if user is not signed-in', function () { diff --git a/src/server/graphql/queries/__tests__/getMemberById.test.js b/src/server/graphql/queries/__tests__/getMemberById.test.js deleted file mode 100644 index e01518d7..00000000 --- a/src/server/graphql/queries/__tests__/getMemberById.test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-env mocha */ -/* global expect, testContext */ -/* eslint-disable prefer-arrow-callback, no-unused-expressions */ -import factory from 'src/test/factories' -import {resetDB, runGraphQLQuery} from 'src/test/helpers' - -import fields from '../index' - -describe(testContext(__filename), function () { - beforeEach(resetDB) - - before(function () { - this.graphQLQuery = 'query($id: ID!) { getMemberById(id: $id) {id chapter { id }} }' - }) - - it('returns correct member', async function () { - const member = await factory.create('member') - const results = await runGraphQLQuery( - this.graphQLQuery, - fields, - {id: member.id} - ) - expect(results.data.getMemberById.id).to.equal(member.id) - expect(results.data.getMemberById.chapter.id).to.equal(member.chapterId) - }) - - it('throws an error if no matching user found', function () { - const promise = runGraphQLQuery( - this.graphQLQuery, - fields, - {id: 'not.a.real.id'} - ) - return expect(promise).to.eventually.be.rejectedWith(/no such member/i) - }) - - it('throws an error if user is not signed-in', function () { - const promise = runGraphQLQuery( - this.graphQLQuery, - fields, - {id: 'not.a.real.id'}, - {currentUser: null} - ) - return expect(promise).to.eventually.be.rejectedWith(/not authorized/i) - }) -}) diff --git a/src/server/graphql/queries/__tests__/getUserSummary.test.js b/src/server/graphql/queries/__tests__/getMemberSummary.test.js similarity index 70% rename from src/server/graphql/queries/__tests__/getUserSummary.test.js rename to src/server/graphql/queries/__tests__/getMemberSummary.test.js index 1e59e9ea..c3130933 100644 --- a/src/server/graphql/queries/__tests__/getUserSummary.test.js +++ b/src/server/graphql/queries/__tests__/getMemberSummary.test.js @@ -9,14 +9,14 @@ import fields from '../index' const query = ` query($identifier: String!) { - getUserSummary(identifier: $identifier) { - user { + getMemberSummary(identifier: $identifier) { + member { id handle chapter { id } } - userProjectSummaries { + memberProjectSummaries { project { id name } - userProjectEvaluations { + memberProjectEvaluations { submittedBy { id name handle } ${FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK} } @@ -33,7 +33,7 @@ describe(testContext(__filename), function () { this.user = await factory.build('user') }) - it('returns correct summary for user identifier', async function () { + it('returns correct summary for member identifier', async function () { useFixture.nockIDMGetUser(this.user) const member = await factory.create('member', {id: this.user.id}) const result = await runGraphQLQuery( @@ -42,14 +42,14 @@ describe(testContext(__filename), function () { {identifier: member.id}, {currentUser: this.currentUser}, ) - const returned = result.data.getUserSummary - expect(returned.user.id).to.equal(this.user.id) - expect(returned.user.handle).to.equal(this.user.handle) - expect(returned.user.chapter.id).to.equal(member.chapterId) - expect(returned.userProjectSummaries).to.be.an('array') + const returned = result.data.getMemberSummary + expect(returned.member.id).to.equal(this.user.id) + expect(returned.member.handle).to.equal(this.user.handle) + expect(returned.member.chapter.id).to.equal(member.chapterId) + expect(returned.memberProjectSummaries).to.be.an('array') }) - it('throws an error if user is not found', function () { + it('throws an error if member is not found', function () { useFixture.nockIDMGetUser(this.user) const result = runGraphQLQuery( query, @@ -57,7 +57,7 @@ describe(testContext(__filename), function () { {identifier: ''}, {currentUser: this.currentUser}, ) - return expect(result).to.eventually.be.rejectedWith(/User not found/i) + return expect(result).to.eventually.be.rejectedWith(/Member not found/i) }) it('throws an error if user is not signed-in', function () { diff --git a/src/server/graphql/queries/__tests__/getProjectSummary.test.js b/src/server/graphql/queries/__tests__/getProjectSummary.test.js index 37c12427..9f116744 100644 --- a/src/server/graphql/queries/__tests__/getProjectSummary.test.js +++ b/src/server/graphql/queries/__tests__/getProjectSummary.test.js @@ -16,9 +16,9 @@ const query = ` id chapter { id } } - projectUserSummaries { - user { id handle } - userProjectEvaluations { + projectMemberSummaries { + member { id handle } + memberProjectEvaluations { submittedBy { id handle } createdAt ${FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK} @@ -52,7 +52,7 @@ describe(testContext(__filename), function () { const returned = result.data.getProjectSummary expect(returned.project.id).to.equal(this.project.id) expect(returned.project.chapter.id).to.equal(this.project.chapterId) - expect(returned.projectUserSummaries).to.be.an('array') + expect(returned.projectMemberSummaries).to.be.an('array') }) it('throws an error if project is not found', function () { diff --git a/src/server/graphql/queries/findMembers.js b/src/server/graphql/queries/findMembers.js index e2f67b71..d74294f0 100644 --- a/src/server/graphql/queries/findMembers.js +++ b/src/server/graphql/queries/findMembers.js @@ -1,18 +1,25 @@ +import {GraphQLString} from 'graphql' import {GraphQLList} from 'graphql/type' -import {Member} from 'src/server/services/dataService' -import {User} from 'src/server/graphql/schemas' +import {userCan} from 'src/common/util' +import findMemberUsers from 'src/server/actions/findMemberUsers' +import {Member} from 'src/server/graphql/schemas' import {LGNotAuthorizedError} from 'src/server/util/error' export default { - type: new GraphQLList(User), - async resolve(source, args, {rootValue: {currentUser}}) { - if (!currentUser) { + type: new GraphQLList(Member), + args: { + identifiers: {type: new GraphQLList(GraphQLString)}, + }, + async resolve(source, {identifiers}, {rootValue: {currentUser}}) { + if (!userCan(currentUser, 'findMembers')) { throw new LGNotAuthorizedError() } - return Member.getJoin({chapter: true}) - .without({chapter: {inviteCodes: true}}) - .execute() - }, + return findMemberUsers(identifiers, { + skipNoMatch: true, + join: {phase: true, chapter: true}, + without: {chapter: {inviteCodes: true}}, + }) + } } diff --git a/src/server/graphql/queries/findUsers.js b/src/server/graphql/queries/findUsers.js deleted file mode 100644 index 62b50533..00000000 --- a/src/server/graphql/queries/findUsers.js +++ /dev/null @@ -1,21 +0,0 @@ -import {GraphQLString} from 'graphql' -import {GraphQLList} from 'graphql/type' - -import {userCan} from 'src/common/util' -import findUsers from 'src/server/actions/findUsers' -import {UserProfile} from 'src/server/graphql/schemas' -import {LGNotAuthorizedError} from 'src/server/util/error' - -export default { - type: new GraphQLList(UserProfile), - args: { - identifiers: {type: new GraphQLList(GraphQLString)}, - }, - async resolve(source, {identifiers}, {rootValue: {currentUser}}) { - if (!userCan(currentUser, 'findUsers')) { - throw new LGNotAuthorizedError() - } - - return await findUsers(identifiers, {skipNoMatch: true, join: {phase: true}}) - } -} diff --git a/src/server/graphql/queries/getUser.js b/src/server/graphql/queries/getMember.js similarity index 54% rename from src/server/graphql/queries/getUser.js rename to src/server/graphql/queries/getMember.js index 7f37c094..4a703220 100644 --- a/src/server/graphql/queries/getUser.js +++ b/src/server/graphql/queries/getMember.js @@ -1,12 +1,12 @@ import {GraphQLNonNull, GraphQLString} from 'graphql' -import {resolveUser} from 'src/server/graphql/resolvers' -import {UserProfile} from 'src/server/graphql/schemas' +import {resolveMember} from 'src/server/graphql/resolvers' +import {Member} from 'src/server/graphql/schemas' export default { - type: UserProfile, + type: Member, args: { identifier: {type: new GraphQLNonNull(GraphQLString), description: 'The user ID or handle'}, }, - resolve: resolveUser, + resolve: resolveMember, } diff --git a/src/server/graphql/queries/getMemberById.js b/src/server/graphql/queries/getMemberById.js deleted file mode 100644 index 9c55f268..00000000 --- a/src/server/graphql/queries/getMemberById.js +++ /dev/null @@ -1,23 +0,0 @@ -import {GraphQLNonNull, GraphQLID} from 'graphql' - -import {Member, errors} from 'src/server/services/dataService' -import {User} from 'src/server/graphql/schemas' -import {LGNotAuthorizedError, LGBadRequestError} from 'src/server/util/error' - -export default { - type: User, - args: { - id: {type: new GraphQLNonNull(GraphQLID)} - }, - async resolve(source, args, {rootValue: {currentUser}}) { - if (!currentUser) { - throw new LGNotAuthorizedError() - } - - return Member.get(args.id) - .getJoin({chapter: true}) - .catch(errors.DocumentNotFound, () => { - throw new LGBadRequestError('No such member') - }) - }, -} diff --git a/src/server/graphql/queries/getUserSummary.js b/src/server/graphql/queries/getMemberSummary.js similarity index 60% rename from src/server/graphql/queries/getUserSummary.js rename to src/server/graphql/queries/getMemberSummary.js index 1ee8019f..f9e875a9 100644 --- a/src/server/graphql/queries/getUserSummary.js +++ b/src/server/graphql/queries/getMemberSummary.js @@ -1,22 +1,22 @@ import {GraphQLNonNull, GraphQLString} from 'graphql' import {userCan} from 'src/common/util' -import {resolveUser} from 'src/server/graphql/resolvers' -import {UserSummary} from 'src/server/graphql/schemas' +import {resolveMember} from 'src/server/graphql/resolvers' +import {MemberSummary} from 'src/server/graphql/schemas' import {LGNotAuthorizedError} from 'src/server/util/error' export default { - type: UserSummary, + type: MemberSummary, args: { identifier: {type: new GraphQLNonNull(GraphQLString), description: 'The user ID or handle'}, }, async resolve(source, args, ast) { - if (!userCan(ast.rootValue.currentUser, 'viewUserSummary')) { + if (!userCan(ast.rootValue.currentUser, 'viewMemberSummary')) { throw new LGNotAuthorizedError() } return { - user: await resolveUser(source, args, ast), + member: await resolveMember(source, args, ast), } } } diff --git a/src/server/graphql/queries/getProjectSummaryForMember.js b/src/server/graphql/queries/getProjectSummaryForMember.js index c4daed7b..c7ee0c6c 100644 --- a/src/server/graphql/queries/getProjectSummaryForMember.js +++ b/src/server/graphql/queries/getProjectSummaryForMember.js @@ -1,6 +1,6 @@ import { getLatestCycleForChapter, - findProjectsForUser, + findProjectsForMember, Project, } from 'src/server/services/dataService' import assertUserIsMember from 'src/server/actions/assertUserIsMember' @@ -18,7 +18,7 @@ export default { const cycle = await getLatestCycleForChapter(member.chapterId) const numActiveProjectsForCycle = await Project.filter({chapterId: member.chapterId, cycleId: cycle.id}).count() - const numTotalProjectsForMember = await findProjectsForUser(member.id).count() + const numTotalProjectsForMember = await findProjectsForMember(member.id).count() return {numActiveProjectsForCycle, numTotalProjectsForMember} }, diff --git a/src/server/graphql/resolvers/index.js b/src/server/graphql/resolvers/index.js index 1f855c4f..7b35a544 100644 --- a/src/server/graphql/resolvers/index.js +++ b/src/server/graphql/resolvers/index.js @@ -5,14 +5,15 @@ import {surveyCompletedBy, surveyLockedFor} from 'src/common/models/survey' import findActiveMembersInChapter from 'src/server/actions/findActiveMembersInChapter' import findActiveProjectsForChapter from 'src/server/actions/findActiveProjectsForChapter' import findActiveMembersForPhase from 'src/server/actions/findActiveMembersForPhase' -import getUser from 'src/server/actions/getUser' -import findUsers from 'src/server/actions/findUsers' -import findUserProjectEvaluations from 'src/server/actions/findUserProjectEvaluations' +import getMemberUser from 'src/server/actions/getMemberUser' +import findMemberUsers from 'src/server/actions/findMemberUsers' +import findMemberProjectEvaluations from 'src/server/actions/findMemberProjectEvaluations' + import handleSubmitSurvey from 'src/server/actions/handleSubmitSurvey' import handleSubmitSurveyResponses from 'src/server/actions/handleSubmitSurveyResponses' import { Chapter, Cycle, Member, Project, Survey, Phase, - findProjectsForUser, + findProjectsForMember, getLatestCycleForChapter, } from 'src/server/services/dataService' import {LGBadRequestError, LGNotAuthorizedError, LGInternalServerError} from 'src/server/util/error' @@ -129,27 +130,27 @@ export function resolveProjectMembers(project) { if (project.members) { return project.members } - return findUsers(project.memberIds) + return findMemberUsers(project.memberIds) } -export async function resolveProjectUserSummaries(projectSummary, args, {rootValue: {currentUser}}) { +export async function resolveProjectMemberSummaries(projectSummary, args, {rootValue: {currentUser}}) { const {project} = projectSummary if (!project) { - throw new Error('Invalid project for user summaries') + throw new Error('Invalid project for member summaries') } - if (projectSummary.projectUserSummaries) { - return projectSummary.projectUserSummaries + if (projectSummary.projectMemberSummaries) { + return projectSummary.projectMemberSummaries } - const projectUsers = await findUsers(project.memberIds) + const projectMembers = await findMemberUsers(project.memberIds) - const projectUserMap = mapById(projectUsers) + const projectMemberMap = mapById(projectMembers) - return Promise.map(projectUsers, async user => { - const canViewSummary = user.id === currentUser.id || userCan(currentUser, 'viewProjectUserSummary') - const summary = canViewSummary ? await getUserProjectSummary(user, project, projectUserMap, currentUser) : {} - return {user, ...summary} + return Promise.map(projectMembers, async member => { + const canViewSummary = member.id === currentUser.id || userCan(currentUser, 'viewProjectMemberSummary') + const summary = canViewSummary ? await _getMemberProjectSummary(member, project, projectMemberMap, currentUser) : {} + return {member, ...summary} }) } @@ -166,65 +167,65 @@ export function resolveWeekStartedAt(parent) { parentStartedAt.startOf('isoweek').toDate() } -export async function resolveUser(source, {identifier}, {rootValue: {currentUser}}) { - if (!userCan(currentUser, 'viewUser')) { +export async function resolveMember(source, {identifier}, {rootValue: {currentUser}}) { + if (!userCan(currentUser, 'viewMember')) { throw new LGNotAuthorizedError() } - const user = await getUser(identifier) - if (!user) { - throw new LGBadRequestError(`User not found for identifier ${identifier}`) + const member = await getMemberUser(identifier) + if (!member) { + throw new LGBadRequestError(`Member not found for identifier ${identifier}`) } - return user + return member } -export async function resolveUserProjectSummaries(userSummary, args, {rootValue: {currentUser}}) { - const {user} = userSummary - if (!user) { +export async function resolveMemberProjectSummaries(memberSummary, args, {rootValue: {currentUser}}) { + const {member} = memberSummary + if (!member) { throw new Error('Invalid user for project summaries') } - if (userSummary.userProjectSummaries) { - return userSummary.userProjectSummaries + if (memberSummary.memberProjectSummaries) { + return memberSummary.memberProjectSummaries } - const projects = await findProjectsForUser(user.id) - const projectUserIds = projects.reduce((result, project) => { + const projects = await findProjectsForMember(member.id) + const projectMemberIds = projects.reduce((result, project) => { if (project.memberIds && project.memberIds.length > 0) { result.push(...project.memberIds) } return result }, []) - const projectUserMap = mapById(await findUsers(projectUserIds)) + const projectMemberMap = mapById(await findMemberUsers(projectMemberIds)) const sortedProjects = projects.sort((a, b) => a.createdAt - b.createdAt).reverse() return Promise.map(sortedProjects, async project => { - const summary = await getUserProjectSummary(user, project, projectUserMap, currentUser) + const summary = await _getMemberProjectSummary(member, project, projectMemberMap, currentUser) return {project, ...summary} }) } -async function getUserProjectSummary(user, project, projectUserMap, currentUser) { - if (user.id !== currentUser.id && !userCan(currentUser, 'viewUserFeedback')) { +async function _getMemberProjectSummary(member, project, projectMemberMap, currentUser) { + if (member.id !== currentUser.id && !userCan(currentUser, 'viewMemberFeedback')) { return null } - const userProjectEvaluations = await findUserProjectEvaluations(user, project) - userProjectEvaluations.forEach(evaluation => { - evaluation.submittedBy = projectUserMap.get(evaluation.submittedById) + const memberProjectEvaluations = await findMemberProjectEvaluations(member, project) + memberProjectEvaluations.forEach(evaluation => { + evaluation.submittedBy = projectMemberMap.get(evaluation.submittedById) }) - let userRetrospectiveComplete - let userRetrospectiveUnlocked + let memberRetrospectiveComplete + let memberRetrospectiveUnlocked if (project.retrospectiveSurveyId) { const survey = await Survey.get(project.retrospectiveSurveyId) - userRetrospectiveComplete = surveyCompletedBy(survey, user.id) - userRetrospectiveUnlocked = !surveyLockedFor(survey, user.id) + memberRetrospectiveComplete = surveyCompletedBy(survey, member.id) + memberRetrospectiveUnlocked = !surveyLockedFor(survey, member.id) } return { - userProjectEvaluations, - userRetrospectiveComplete, - userRetrospectiveUnlocked, + memberProjectEvaluations, + memberRetrospectiveComplete, + memberRetrospectiveUnlocked, } } diff --git a/src/server/graphql/schemas/InputUser.js b/src/server/graphql/schemas/InputMember.js similarity index 79% rename from src/server/graphql/schemas/InputUser.js rename to src/server/graphql/schemas/InputMember.js index 710a6101..7dde7c26 100644 --- a/src/server/graphql/schemas/InputUser.js +++ b/src/server/graphql/schemas/InputMember.js @@ -2,11 +2,11 @@ import {GraphQLNonNull, GraphQLID, GraphQLInt} from 'graphql' import {GraphQLInputObjectType} from 'graphql/type' export default new GraphQLInputObjectType({ - name: 'InputUser', - description: 'A user', + name: 'InputMember', + description: 'Input values for member updates', fields: () => { return { - id: {type: new GraphQLNonNull(GraphQLID), description: "The user's ID"}, + id: {type: new GraphQLNonNull(GraphQLID), description: "The member's ID"}, phaseNumber: {type: GraphQLInt, description: 'The Phase Number'}, } }, diff --git a/src/server/graphql/schemas/Member.js b/src/server/graphql/schemas/Member.js new file mode 100644 index 00000000..dab2bc44 --- /dev/null +++ b/src/server/graphql/schemas/Member.js @@ -0,0 +1,35 @@ + +import {GraphQLID, GraphQLString, GraphQLBoolean} from 'graphql' +import {GraphQLObjectType, GraphQLList} from 'graphql/type' +import {GraphQLDateTime, GraphQLEmail} from 'graphql-custom-types' + +import {resolveChapter, resolvePhase} from 'src/server/graphql/resolvers' +import {GraphQLPhoneNumber} from 'src/server/graphql/util' + +export default new GraphQLObjectType({ + name: 'Member', + description: 'A chapter member', + fields: () => { + const {Chapter, Phase} = require('src/server/graphql/schemas') + + return { + id: {type: GraphQLID, description: 'The member\'s UUID'}, + chapterId: {type: GraphQLID, description: 'The member\'s chapter UUID'}, + chapter: {type: Chapter, description: 'The member\'s chapter', resolve: resolveChapter}, + phaseId: {type: GraphQLID, description: 'The member\'s phase UUID'}, + phase: {type: Phase, description: 'The member\'s phase', resolve: resolvePhase}, + active: {type: GraphQLBoolean, description: 'True if the member is active'}, + name: {type: GraphQLString, description: 'The member\'s name'}, + handle: {type: GraphQLString, description: 'The member\'s handle'}, + profileUrl: {type: GraphQLString, description: 'The member\'s profile URL'}, + avatarUrl: {type: GraphQLString, description: 'The member\'s avatar image URL'}, + email: {type: GraphQLEmail, description: 'The member\'s email'}, + phone: {type: GraphQLPhoneNumber, description: 'The member\'s phone number'}, + timezone: {type: GraphQLString, description: 'The member\'s timezone'}, + roles: {type: new GraphQLList(GraphQLString), description: 'The member\'s roles'}, + inviteCode: {type: GraphQLString, description: 'The invite code the member used to sign up'}, + createdAt: {type: GraphQLDateTime, description: 'When the member was created'}, + updatedAt: {type: GraphQLDateTime, description: 'When the member was last updated'}, + } + } +}) diff --git a/src/server/graphql/schemas/UserProjectEvaluation.js b/src/server/graphql/schemas/MemberProjectEvaluation.js similarity index 67% rename from src/server/graphql/schemas/UserProjectEvaluation.js rename to src/server/graphql/schemas/MemberProjectEvaluation.js index 9ea04568..fea1eecd 100644 --- a/src/server/graphql/schemas/UserProjectEvaluation.js +++ b/src/server/graphql/schemas/MemberProjectEvaluation.js @@ -5,13 +5,13 @@ import {GraphQLDateTime} from 'graphql-custom-types' import {FEEDBACK_TYPE_DESCRIPTORS} from 'src/common/models/feedbackType' export default new GraphQLObjectType({ - name: 'UserProjectEvaluation', - description: 'An evaluation of a user\'s performance on a project', + name: 'MemberProjectEvaluation', + description: 'An evaluation of a member\'s performance on a project', fields: () => { - const {UserProfile} = require('src/server/graphql/schemas') + const {Member} = require('src/server/graphql/schemas') return { - submittedBy: {type: UserProfile, description: 'The evaluation submitter'}, + submittedBy: {type: Member, description: 'The evaluation submitter'}, createdAt: {type: GraphQLDateTime, description: 'The datetime of the evaluation creation'}, [FEEDBACK_TYPE_DESCRIPTORS.GENERAL_FEEDBACK]: {type: GraphQLString, description: 'General text feedback'}, } diff --git a/src/server/graphql/schemas/MemberProjectSummary.js b/src/server/graphql/schemas/MemberProjectSummary.js new file mode 100644 index 00000000..86755fe7 --- /dev/null +++ b/src/server/graphql/schemas/MemberProjectSummary.js @@ -0,0 +1,15 @@ +import {GraphQLNonNull} from 'graphql' +import {GraphQLObjectType, GraphQLList} from 'graphql/type' + +export default new GraphQLObjectType({ + name: 'MemberProjectSummary', + description: 'Member project summary', + fields: () => { + const {Project, MemberProjectEvaluation} = require('src/server/graphql/schemas') + + return { + project: {type: new GraphQLNonNull(Project), description: 'The project'}, + memberProjectEvaluations: {type: new GraphQLList(MemberProjectEvaluation), description: 'The member\'s project evaluations'}, + } + } +}) diff --git a/src/server/graphql/schemas/MemberSummary.js b/src/server/graphql/schemas/MemberSummary.js new file mode 100644 index 00000000..a0334c4f --- /dev/null +++ b/src/server/graphql/schemas/MemberSummary.js @@ -0,0 +1,17 @@ +import {GraphQLNonNull} from 'graphql' +import {GraphQLObjectType, GraphQLList} from 'graphql/type' + +import {resolveMemberProjectSummaries} from 'src/server/graphql/resolvers' + +export default new GraphQLObjectType({ + name: 'MemberSummary', + description: 'Member summary', + fields: () => { + const {Member, MemberProjectSummary} = require('src/server/graphql/schemas') + + return { + member: {type: new GraphQLNonNull(Member), description: 'The member'}, + memberProjectSummaries: {type: new GraphQLList(MemberProjectSummary), resolve: resolveMemberProjectSummaries, description: 'The member\'s project summaries'}, + } + } +}) diff --git a/src/server/graphql/schemas/PhaseSummary.js b/src/server/graphql/schemas/PhaseSummary.js index 1879dd18..332824d7 100644 --- a/src/server/graphql/schemas/PhaseSummary.js +++ b/src/server/graphql/schemas/PhaseSummary.js @@ -7,12 +7,12 @@ export default new GraphQLObjectType({ name: 'PhaseSummary', description: 'Phase summary', fields: () => { - const {UserProfile, Phase, Project} = require('src/server/graphql/schemas') + const {Member, Phase, Project} = require('src/server/graphql/schemas') return { phase: {type: new GraphQLNonNull(Phase), description: 'The phase'}, currentProjects: {type: new GraphQLList(Project), resolve: resolvePhaseCurrentProjects, description: 'The phases\'s currently active projects'}, - currentMembers: {type: new GraphQLList(UserProfile), resolve: resolvePhaseCurrentMembers, description: 'The phases\'s currently active members'}, + currentMembers: {type: new GraphQLList(Member), resolve: resolvePhaseCurrentMembers, description: 'The phases\'s currently active members'}, } } }) diff --git a/src/server/graphql/schemas/Project.js b/src/server/graphql/schemas/Project.js index 827993da..24c72b84 100644 --- a/src/server/graphql/schemas/Project.js +++ b/src/server/graphql/schemas/Project.js @@ -14,7 +14,7 @@ export default new GraphQLObjectType({ name: 'Project', description: 'A project engaged in by learners to complete some goal', fields: () => { - const {Chapter, Cycle, Goal, Phase, UserProfile} = require('src/server/graphql/schemas') + const {Chapter, Cycle, Goal, Phase, Member} = require('src/server/graphql/schemas') return { id: {type: new GraphQLNonNull(GraphQLID), description: "The project's UUID"}, @@ -27,7 +27,7 @@ export default new GraphQLObjectType({ phase: {type: Phase, description: 'The phase', resolve: resolvePhase}, goal: {type: Goal, description: 'The project goal', resolve: resolveProjectGoal}, memberIds: {type: new GraphQLList(GraphQLID), description: 'The project member UUIDs'}, - members: {type: new GraphQLList(UserProfile), description: 'The project members', resolve: resolveProjectMembers}, + members: {type: new GraphQLList(Member), description: 'The project members', resolve: resolveProjectMembers}, artifactURL: {type: GraphQLURL, description: 'The URL pointing to the output of this project'}, retrospectiveSurveyId: {type: GraphQLID, description: "The retrospective survey's UUID"}, createdAt: {type: new GraphQLNonNull(GraphQLDateTime), description: 'When this record was created'}, diff --git a/src/server/graphql/schemas/ProjectMemberSummary.js b/src/server/graphql/schemas/ProjectMemberSummary.js new file mode 100644 index 00000000..e7e5cf3c --- /dev/null +++ b/src/server/graphql/schemas/ProjectMemberSummary.js @@ -0,0 +1,17 @@ +import {GraphQLNonNull, GraphQLBoolean} from 'graphql' +import {GraphQLObjectType, GraphQLList} from 'graphql/type' + +export default new GraphQLObjectType({ + name: 'ProjectMemberSummary', + description: 'Project member summary', + fields: () => { + const {Member, MemberProjectEvaluation} = require('src/server/graphql/schemas') + + return { + member: {type: new GraphQLNonNull(Member), description: 'The member'}, + memberProjectEvaluations: {type: new GraphQLList(MemberProjectEvaluation), description: 'The member\'s project evaluations'}, + memberRetrospectiveComplete: {type: GraphQLBoolean, description: 'True if the member has completed their retrospective survey for this project'}, + memberRetrospectiveUnlocked: {type: GraphQLBoolean, description: 'True if the member\'s retrospective survey for this project as been completed but is unlocked.'} + } + } +}) diff --git a/src/server/graphql/schemas/ProjectSummary.js b/src/server/graphql/schemas/ProjectSummary.js index a088a126..1c1d71da 100644 --- a/src/server/graphql/schemas/ProjectSummary.js +++ b/src/server/graphql/schemas/ProjectSummary.js @@ -2,18 +2,17 @@ import {GraphQLNonNull} from 'graphql' import {GraphQLObjectType, GraphQLList} from 'graphql/type' import { - resolveProjectUserSummaries, + resolveProjectMemberSummaries, } from 'src/server/graphql/resolvers' export default new GraphQLObjectType({ name: 'ProjectSummary', description: 'Summary of project details', fields: () => { - const {Project, ProjectUserSummary} = require('src/server/graphql/schemas') - + const {Project, ProjectMemberSummary} = require('src/server/graphql/schemas') return { project: {type: new GraphQLNonNull(Project), description: 'The project'}, - projectUserSummaries: {type: new GraphQLList(ProjectUserSummary), resolve: resolveProjectUserSummaries, description: 'The project\'s user summaries'}, + projectMemberSummaries: {type: new GraphQLList(ProjectMemberSummary), resolve: resolveProjectMemberSummaries, description: 'The project\'s member summaries'}, } } }) diff --git a/src/server/graphql/schemas/ProjectUserSummary.js b/src/server/graphql/schemas/ProjectUserSummary.js deleted file mode 100644 index 31b6baf3..00000000 --- a/src/server/graphql/schemas/ProjectUserSummary.js +++ /dev/null @@ -1,17 +0,0 @@ -import {GraphQLNonNull, GraphQLBoolean} from 'graphql' -import {GraphQLObjectType, GraphQLList} from 'graphql/type' - -export default new GraphQLObjectType({ - name: 'ProjectUserSummary', - description: 'Project user summary', - fields: () => { - const {UserProfile, UserProjectEvaluation} = require('src/server/graphql/schemas') - - return { - user: {type: new GraphQLNonNull(UserProfile), description: 'The user'}, - userProjectEvaluations: {type: new GraphQLList(UserProjectEvaluation), description: 'The user\'s project evaluations'}, - userRetrospectiveComplete: {type: GraphQLBoolean, description: 'True if the user has completed their retrospective survey for this project'}, - userRetrospectiveUnlocked: {type: GraphQLBoolean, description: 'True if the user\'s retrospective survey for this project as been completed but is unlocked.'} - } - } -}) diff --git a/src/server/graphql/schemas/Survey.js b/src/server/graphql/schemas/Survey.js index 8d688d7d..feafa63e 100644 --- a/src/server/graphql/schemas/Survey.js +++ b/src/server/graphql/schemas/Survey.js @@ -11,7 +11,7 @@ export default new GraphQLObjectType({ const {Project, SurveyQuestion} = require('src/server/graphql/schemas') return { - id: {type: new GraphQLNonNull(GraphQLID), description: "The survey's user UUID"}, + id: {type: new GraphQLNonNull(GraphQLID), description: "The survey's UUID"}, createdAt: {type: new GraphQLNonNull(GraphQLDateTime), description: 'When this record was created'}, updatedAt: {type: new GraphQLNonNull(GraphQLDateTime), description: 'When this record was last updated'}, questions: {type: new GraphQLNonNull(new GraphQLList(SurveyQuestion)), description: 'The questions for the survey'}, diff --git a/src/server/graphql/schemas/User.js b/src/server/graphql/schemas/User.js deleted file mode 100644 index 22a7ae41..00000000 --- a/src/server/graphql/schemas/User.js +++ /dev/null @@ -1,18 +0,0 @@ -import {GraphQLNonNull, GraphQLID} from 'graphql' -import {GraphQLObjectType} from 'graphql/type' -import {GraphQLDateTime} from 'graphql-custom-types' - -export default new GraphQLObjectType({ - name: 'User', - description: 'A member', - fields: () => { - const {Chapter} = require('src/server/graphql/schemas') - - return { - id: {type: new GraphQLNonNull(GraphQLID), description: "The user's UUID"}, - chapter: {type: Chapter, description: "The user's chapter"}, - createdAt: {type: new GraphQLNonNull(GraphQLDateTime), description: 'When this record was created'}, - updatedAt: {type: new GraphQLNonNull(GraphQLDateTime), description: 'When this record was last updated'}, - } - }, -}) diff --git a/src/server/graphql/schemas/UserProfile.js b/src/server/graphql/schemas/UserProfile.js deleted file mode 100644 index ba5d7d60..00000000 --- a/src/server/graphql/schemas/UserProfile.js +++ /dev/null @@ -1,35 +0,0 @@ - -import {GraphQLID, GraphQLString, GraphQLBoolean} from 'graphql' -import {GraphQLObjectType, GraphQLList} from 'graphql/type' -import {GraphQLDateTime, GraphQLEmail} from 'graphql-custom-types' - -import {resolveChapter, resolvePhase} from 'src/server/graphql/resolvers' -import {GraphQLPhoneNumber} from 'src/server/graphql/util' - -export default new GraphQLObjectType({ - name: 'UserProfile', - description: 'A complete user profile', - fields: () => { - const {Chapter, Phase} = require('src/server/graphql/schemas') - - return { - id: {type: GraphQLID, description: 'The user\'s UUID'}, - chapterId: {type: GraphQLID, description: 'The user\'s chapter UUID'}, - chapter: {type: Chapter, description: 'The user\'s chapter', resolve: resolveChapter}, - phaseId: {type: GraphQLID, description: 'The user\'s phase UUID'}, - phase: {type: Phase, description: 'The user\'s phase', resolve: resolvePhase}, - active: {type: GraphQLBoolean, description: 'True if the user is active'}, - name: {type: GraphQLString, description: 'The user\'s name'}, - handle: {type: GraphQLString, description: 'The user\'s handle'}, - profileUrl: {type: GraphQLString, description: 'The user\'s profile URL'}, - avatarUrl: {type: GraphQLString, description: 'The user\'s avatar image URL'}, - email: {type: GraphQLEmail, description: 'The user\'s email'}, - phone: {type: GraphQLPhoneNumber, description: 'The user\'s phone number'}, - timezone: {type: GraphQLString, description: 'The user\'s timezone'}, - roles: {type: new GraphQLList(GraphQLString), description: 'The user\'s roles'}, - inviteCode: {type: GraphQLString, description: 'The invite code the user used to sign up'}, - createdAt: {type: GraphQLDateTime, description: 'When the user was created'}, - updatedAt: {type: GraphQLDateTime, description: 'When the user was last updated'}, - } - } -}) diff --git a/src/server/graphql/schemas/UserProjectSummary.js b/src/server/graphql/schemas/UserProjectSummary.js deleted file mode 100644 index 9ddb8ab5..00000000 --- a/src/server/graphql/schemas/UserProjectSummary.js +++ /dev/null @@ -1,15 +0,0 @@ -import {GraphQLNonNull} from 'graphql' -import {GraphQLObjectType, GraphQLList} from 'graphql/type' - -export default new GraphQLObjectType({ - name: 'UserProjectSummary', - description: 'User project summary', - fields: () => { - const {Project, UserProjectEvaluation} = require('src/server/graphql/schemas') - - return { - project: {type: new GraphQLNonNull(Project), description: 'The project'}, - userProjectEvaluations: {type: new GraphQLList(UserProjectEvaluation), description: 'The user\'s project evaluations'}, - } - } -}) diff --git a/src/server/graphql/schemas/UserSummary.js b/src/server/graphql/schemas/UserSummary.js deleted file mode 100644 index a301b48e..00000000 --- a/src/server/graphql/schemas/UserSummary.js +++ /dev/null @@ -1,17 +0,0 @@ -import {GraphQLNonNull} from 'graphql' -import {GraphQLObjectType, GraphQLList} from 'graphql/type' - -import {resolveUserProjectSummaries} from 'src/server/graphql/resolvers' - -export default new GraphQLObjectType({ - name: 'UserSummary', - description: 'User summary', - fields: () => { - const {UserProfile, UserProjectSummary} = require('src/server/graphql/schemas') - - return { - user: {type: new GraphQLNonNull(UserProfile), description: 'The user'}, - userProjectSummaries: {type: new GraphQLList(UserProjectSummary), resolve: resolveUserProjectSummaries, description: 'The user\'s project summaries'}, - } - } -}) diff --git a/src/server/graphql/schemas/Vote.js b/src/server/graphql/schemas/Vote.js index 46207416..004e7cf8 100644 --- a/src/server/graphql/schemas/Vote.js +++ b/src/server/graphql/schemas/Vote.js @@ -4,17 +4,17 @@ import {GraphQLDateTime} from 'graphql-custom-types' export default new GraphQLObjectType({ name: 'Vote', - description: 'An expression of interest in working on a certain Goal as a Project in a Cycle', + description: 'An expression of interest in working on specified goals during a project cycle', fields: () => { - const {Goal, User, Cycle} = require('src/server/graphql/schemas') + const {Goal, Member, Cycle} = require('src/server/graphql/schemas') return { id: {type: new GraphQLNonNull(GraphQLID), description: 'The vote UUID'}, - member: {type: User, description: 'The Member who cast the Vote'}, - cycle: {type: Cycle, description: 'The Cycle '}, - goals: {type: new GraphQLList(Goal), description: 'The list of Goals, in order of preference'}, - createdAt: {type: new GraphQLNonNull(GraphQLDateTime), description: 'When this record was created'}, - updatedAt: {type: new GraphQLNonNull(GraphQLDateTime), description: 'When this record was last updated'}, + member: {type: Member, description: 'The member who submitted the vote'}, + cycle: {type: Cycle, description: 'The cycle '}, + goals: {type: new GraphQLList(Goal), description: 'The list of goals, in order of preference'}, + createdAt: {type: new GraphQLNonNull(GraphQLDateTime), description: 'Created datetime'}, + updatedAt: {type: new GraphQLNonNull(GraphQLDateTime), description: 'Last updated datetime'}, } }, }) diff --git a/src/server/graphql/schemas/VotingPoolResults.js b/src/server/graphql/schemas/VotingPoolResults.js index 58741ab2..5237bf7b 100644 --- a/src/server/graphql/schemas/VotingPoolResults.js +++ b/src/server/graphql/schemas/VotingPoolResults.js @@ -6,14 +6,14 @@ export default new GraphQLObjectType({ name: 'VotingPoolResults', description: 'Results on goal voting for a pool', fields: () => { - const {CandidateGoal, Phase, User} = require('src/server/graphql/schemas') + const {CandidateGoal, Phase, Member} = require('src/server/graphql/schemas') return { id: {type: new GraphQLNonNull(GraphQLID), description: 'The pool id'}, name: {type: new GraphQLNonNull(GraphQLString), description: 'The pool name'}, phase: {type: Phase, desription: "The pool's phase", resolve: resolvePhase}, candidateGoals: {type: new GraphQLList(CandidateGoal), description: 'The candidate goals for the given pool'}, - users: {type: new GraphQLList(User), description: 'A list of all members in this pool'}, + members: {type: new GraphQLList(Member), description: 'A list of all members in this pool'}, voterMemberIds: {type: new GraphQLList(GraphQLID), description: 'The memberId os all members who have voted in this pool'}, votingIsStillOpen: {type: GraphQLBoolean, description: 'True is votes are still being accepted for this pool'}, } diff --git a/src/server/reports/memberRetroFeedback.js b/src/server/reports/memberRetroFeedback.js index b909a689..48de4485 100644 --- a/src/server/reports/memberRetroFeedback.js +++ b/src/server/reports/memberRetroFeedback.js @@ -1,8 +1,8 @@ /* eslint-disable camelcase */ import Promise from 'bluebird' -import getUser from 'src/server/actions/getUser' -import findUsers from 'src/server/actions/findUsers' +import getMemberUser from 'src/server/actions/getMemberUser' +import findMemberUsers from 'src/server/actions/findMemberUsers' import {Project, Response} from 'src/server/services/dataService' import {mapById} from 'src/server/util' import {LGBadRequestError} from 'src/server/util/error' @@ -28,39 +28,39 @@ export default async function handleRequest(req, res) { } const userHandle = String(handle).trim() - const user = await getUser(userHandle) - if (!user) { + const memberUser = await getMemberUser(userHandle) + if (!memberUser) { throw new LGBadRequestError(`User not found for handle ${userHandle}`) } - const reportRows = await generateReport(user) + const reportRows = await generateReport(memberUser) res.setHeader('Content-disposition', `attachment; filename=memberRetroFeedback_${userHandle}.csv`) return writeCSV(reportRows, res, {headers: HEADERS}) } -async function generateReport(user) { - const projects = await findUserProjects(user.id) +async function generateReport(memberUser) { + const projects = await _findMemberProjects(memberUser.id) return Promise.reduce(projects, async (result, project) => { const {cycle} = project const [retroSurveyResponses, projectMembers] = await Promise.all([ - getSurveyResponsesForSubject(user.id, project.retrospectiveSurveyId), - findUsers(project.memberIds), + _getSurveyResponsesForSubject(memberUser.id, project.retrospectiveSurveyId), + findMemberUsers(project.memberIds), ]) - const projectMembersById = mapById(projectMembers) + const projectMemberMap = mapById(projectMembers) const reportRows = retroSurveyResponses.map(response => { - const respondent = projectMembersById.get(response.respondentId) + const respondent = projectMemberMap.get(response.respondentId) if (!respondent) { console.warn('Survey response found for user no longer a member of project') return result } return { - subject_handle: user.handle, - subject_name: user.name, + subject_handle: memberUser.handle, + subject_name: memberUser.name, cycle_number: cycle.cycleNumber, project_name: project.name, respondent_handle: respondent.handle, @@ -76,16 +76,16 @@ async function generateReport(user) { }, []) } -function getSurveyResponsesForSubject(subjectId, surveyId) { +function _getSurveyResponsesForSubject(subjectId, surveyId) { return Response .filter({subjectId, surveyId}) .getJoin({question: {feedbackType: true}}) .orderBy('questionId', 'respondentId') } -function findUserProjects(userId) { +function _findMemberProjects(memberId) { return Project - .filter(row => row('memberIds').contains(userId)) + .filter(row => row('memberIds').contains(memberId)) .getJoin({cycle: true}) .orderBy('createdAt') } diff --git a/src/server/reports/projectTeams.js b/src/server/reports/projectTeams.js index abc3dc2a..cd144fcb 100644 --- a/src/server/reports/projectTeams.js +++ b/src/server/reports/projectTeams.js @@ -1,9 +1,10 @@ +import findMemberUsers from 'src/server/actions/findMemberUsers' import {getPoolByCycleIdAndMemberId, r} from 'src/server/services/dataService' +import {hashById} from 'src/server/util' import { getChapterId, getCycleId, writeCSV, - getMemberInfoByIds, parseCycleReportArgs, } from './util' @@ -23,10 +24,10 @@ export async function runReport(args) { const cycleId = await getCycleId(chapterId, cycleNumber) const memberIds = await r.table('members').filter({chapterId})('id') - const memberInfo = await getMemberInfoByIds(memberIds) + const memberUserHash = hashById(await findMemberUsers(memberIds)) - const query = r.expr(memberInfo).do(memberInfoExpr => { - const getInfo = id => memberInfoExpr(id).default({id, name: '?', email: '?', handle: '?'}) + const query = r.expr(memberUserHash).do(memberUserHashExpr => { + const getInfo = id => memberUserHashExpr(id).default({id, name: '?', email: '?', handle: '?'}) return r.table('projects') .filter({chapterId}) .merge(row => ({projectName: row('name')})) diff --git a/src/server/reports/util.js b/src/server/reports/util.js index bf8b41b9..a95bb43b 100644 --- a/src/server/reports/util.js +++ b/src/server/reports/util.js @@ -1,8 +1,6 @@ import csvWriter from 'csv-write-stream' -import config from 'src/config' import {Chapter, Cycle} from 'src/server/services/dataService' -import graphQLFetcher from 'src/server/util/graphql' export async function getCycleId(chapterId, cycleNumber) { const cycles = await Cycle.filter({chapterId, cycleNumber}) @@ -34,25 +32,6 @@ export function writeCSV(rows, outStream, opts) { writer.end() } -export function getMemberInfoByIds(memberIds) { - return graphQLFetcher(config.server.idm.baseURL)({ - query: ` -query ($memberIds: [ID]!) { - getUsersByIds(ids: $memberIds) { - id - email - name - handle - } -}`, - variables: {memberIds}, - }) - .then(result => result.data.getUsersByIds.reduce( - (prev, member) => ({...prev, [member.id]: member}), - {} - )) -} - export function shortenedMemberId(rethinkDBid) { return rethinkDBid.split('-')(0) } diff --git a/src/server/services/dataService/queries/__tests__/findProjectByNameForMember.test.js b/src/server/services/dataService/queries/__tests__/findProjectByNameForMember.test.js index fa67d832..39095905 100644 --- a/src/server/services/dataService/queries/__tests__/findProjectByNameForMember.test.js +++ b/src/server/services/dataService/queries/__tests__/findProjectByNameForMember.test.js @@ -7,7 +7,7 @@ import factory from 'src/test/factories' import findProjectByNameForMember from '../findProjectByNameForMember' describe(testContext(__filename), function () { - useFixture.setCurrentCycleAndUserForProject() + useFixture.setCurrentCycleAndMemberForProject() beforeEach(resetDB) @@ -16,7 +16,7 @@ describe(testContext(__filename), function () { }) it('finds the project with the given name where the user is or was a team member', async function () { - await this.setCurrentCycleAndUserForProject(this.project) + await this.setCurrentCycleAndMemberForProject(this.project) const project = await findProjectByNameForMember(this.project.name, this.currentUser.id) return expect(project.memberIds).to.contain(this.currentUser.id) }) @@ -28,7 +28,7 @@ describe(testContext(__filename), function () { }) it('throws an error if there is no project with the given name', async function () { - await this.setCurrentCycleAndUserForProject(this.project) + await this.setCurrentCycleAndMemberForProject(this.project) const projectPromise = findProjectByNameForMember('non-existent-project-name', this.currentUser.id) return expect(projectPromise).to.be.rejectedWith(/Project.*.not found/) }) diff --git a/src/server/services/dataService/queries/__tests__/findProjectsForUser.test.js b/src/server/services/dataService/queries/__tests__/findProjectsForMember.test.js similarity index 58% rename from src/server/services/dataService/queries/__tests__/findProjectsForUser.test.js rename to src/server/services/dataService/queries/__tests__/findProjectsForMember.test.js index 2a5c59bf..2c456980 100644 --- a/src/server/services/dataService/queries/__tests__/findProjectsForUser.test.js +++ b/src/server/services/dataService/queries/__tests__/findProjectsForMember.test.js @@ -4,27 +4,27 @@ import {resetDB, useFixture} from 'src/test/helpers' import factory from 'src/test/factories' -import findProjectsForUser from '../findProjectsForUser' +import findProjectsForMember from '../findProjectsForMember' describe(testContext(__filename), function () { - useFixture.setCurrentCycleAndUserForProject() + useFixture.setCurrentCycleAndMemberForProject() beforeEach(resetDB) beforeEach(async function () { this.chapter = await factory.create('chapter') - this.userProject = await factory.create('project', {chapterId: this.chapter.id}) - await this.setCurrentCycleAndUserForProject(this.userProject) + this.memberProject = await factory.create('project', {chapterId: this.chapter.id}) + await this.setCurrentCycleAndMemberForProject(this.memberProject) this.otherProject = await factory.create('project', {chapterId: this.chapter.id}) }) it('returns the projects for the given member', async function () { - const projectIds = (await findProjectsForUser(this.currentUser.id)).map(p => p.id) - return expect(projectIds).to.deep.equal([this.userProject.id]) + const projectIds = (await findProjectsForMember(this.currentUser.id)).map(p => p.id) + return expect(projectIds).to.deep.equal([this.memberProject.id]) }) it('does not return projects with which the member is not involved', async function () { - const projectIds = (await findProjectsForUser(this.currentUser.id)).map(p => p.id) + const projectIds = (await findProjectsForMember(this.currentUser.id)).map(p => p.id) return expect(projectIds).to.not.contain(this.otherProject.id) }) }) diff --git a/src/server/services/dataService/queries/findProjectsForUser.js b/src/server/services/dataService/queries/findProjectsForMember.js similarity index 51% rename from src/server/services/dataService/queries/findProjectsForUser.js rename to src/server/services/dataService/queries/findProjectsForMember.js index 8994f564..789f316d 100644 --- a/src/server/services/dataService/queries/findProjectsForUser.js +++ b/src/server/services/dataService/queries/findProjectsForMember.js @@ -1,5 +1,5 @@ import r from '../r' -export default function findProjectsForUser(userId) { - return r.table('projects').filter(project => (project('memberIds').contains(userId))) +export default function findProjectsForMember(memberId) { + return r.table('projects').filter(project => (project('memberIds').contains(memberId))) } diff --git a/src/server/services/dataService/queries/findVotingResultsForCycle.js b/src/server/services/dataService/queries/findVotingResultsForCycle.js index d03a1f00..899cc10f 100644 --- a/src/server/services/dataService/queries/findVotingResultsForCycle.js +++ b/src/server/services/dataService/queries/findVotingResultsForCycle.js @@ -32,7 +32,7 @@ function _mergeCandidateGoals(pool) { function _mergeUsers(pool) { return { - users: findMembersInPool(pool('id')).coerceTo('array') + members: findMembersInPool(pool('id')).coerceTo('array') } } diff --git a/src/server/services/projectFormationService/lib/ObjectiveAppraiser/MembersGetTeammatesTheyGaveGoodFeedbackAppraiser.js b/src/server/services/projectFormationService/lib/ObjectiveAppraiser/MembersGetTeammatesTheyGaveGoodFeedbackAppraiser.js index 37e69d48..4808046e 100644 --- a/src/server/services/projectFormationService/lib/ObjectiveAppraiser/MembersGetTeammatesTheyGaveGoodFeedbackAppraiser.js +++ b/src/server/services/projectFormationService/lib/ObjectiveAppraiser/MembersGetTeammatesTheyGaveGoodFeedbackAppraiser.js @@ -1,7 +1,7 @@ import {FEEDBACK_TYPE_DESCRIPTORS} from 'src/common/models/feedbackType' import {repeat, flatten, sum} from '../util' -import {getMemberIds, getUserFeedback} from '../pool' +import {getMemberIds, getMemberFeedback} from '../pool' export const FEEDBACK_TYPE_WEIGHTS = { [FEEDBACK_TYPE_DESCRIPTORS.TEAM_PLAY]: 1, @@ -66,6 +66,6 @@ export default class MembersGetTeammatesTheyGaveGoodFeedbackAppraiser { } getFeedback({respondentId, subjectId}) { - return getUserFeedback(this.pool, {respondentId, subjectId}) + return getMemberFeedback(this.pool, {respondentId, subjectId}) } } diff --git a/src/server/services/projectFormationService/lib/ObjectiveAppraiser/__tests__/MembersGetTeammatesTheyGaveGoodFeedbackAppraiser.test.js b/src/server/services/projectFormationService/lib/ObjectiveAppraiser/__tests__/MembersGetTeammatesTheyGaveGoodFeedbackAppraiser.test.js index 9e1ea568..004eb2f4 100644 --- a/src/server/services/projectFormationService/lib/ObjectiveAppraiser/__tests__/MembersGetTeammatesTheyGaveGoodFeedbackAppraiser.test.js +++ b/src/server/services/projectFormationService/lib/ObjectiveAppraiser/__tests__/MembersGetTeammatesTheyGaveGoodFeedbackAppraiser.test.js @@ -36,7 +36,7 @@ describe(testContext(__filename), function () { ([0, 50, 100]).forEach(v => { it(`returns ${v} when everyone rated all their teammates ${v}`, function () { const feedback = {[TEAM_PLAY]: v, [TECHNICAL_COMPREHENSION]: v} - const userFeedback = { + const memberFeedback = { respondentIds: { p0: {subjectIds: {p1: feedback, p2: feedback, p3: feedback}}, p1: {subjectIds: {p0: feedback, p2: feedback, p3: feedback}}, @@ -44,7 +44,7 @@ describe(testContext(__filename), function () { p3: {subjectIds: {p0: feedback, p1: feedback, p2: feedback}}, } } - const pool = {...poolDefaults, userFeedback} + const pool = {...poolDefaults, memberFeedback} const appraiser = new MembersGetTeammatesTheyGaveGoodFeedbackAppraiser(pool) const score = appraiser.score(teamFormationPlan) @@ -56,7 +56,7 @@ describe(testContext(__filename), function () { it('weights each member correctly', function () { const perfectScore = {[TEAM_PLAY]: 100, [TECHNICAL_COMPREHENSION]: 100} const halfScore = {[TEAM_PLAY]: 50, [TECHNICAL_COMPREHENSION]: 50} - const userFeedback = { + const memberFeedback = { respondentIds: { p0: { subjectIds: { @@ -70,7 +70,7 @@ describe(testContext(__filename), function () { p3: {subjectIds: {p0: perfectScore, p1: perfectScore, p2: perfectScore}}, } } - const pool = {...poolDefaults, userFeedback} + const pool = {...poolDefaults, memberFeedback} const appraiser = new MembersGetTeammatesTheyGaveGoodFeedbackAppraiser(pool) const score = appraiser.score(teamFormationPlan) @@ -83,7 +83,7 @@ describe(testContext(__filename), function () { }) context('when the teams are not complete', function () { - const userFeedback = { + const memberFeedback = { respondentIds: { p0: {subjectIds: {p1: 0 , p2: 100, p3: 0}}, p1: {subjectIds: {p0: 0 , p2: 0 , p3: 0}}, @@ -99,7 +99,7 @@ describe(testContext(__filename), function () { {goalDescriptor: 'g3', memberIds: []}, ] } - const pool = {...poolDefaults, userFeedback} + const pool = {...poolDefaults, memberFeedback} const appraiser = new MembersGetTeammatesTheyGaveGoodFeedbackAppraiser(pool) const score = appraiser.score(teamFormationPlan) @@ -113,7 +113,7 @@ describe(testContext(__filename), function () { {goalDescriptor: 'g3', memberIds: ['p1']}, ] } - const pool = {...poolDefaults, userFeedback} + const pool = {...poolDefaults, memberFeedback} const appraiser = new MembersGetTeammatesTheyGaveGoodFeedbackAppraiser(pool) const score = appraiser.score(teamFormationPlan) diff --git a/src/server/services/projectFormationService/lib/pool.js b/src/server/services/projectFormationService/lib/pool.js index 9854bfff..51f39674 100644 --- a/src/server/services/projectFormationService/lib/pool.js +++ b/src/server/services/projectFormationService/lib/pool.js @@ -84,6 +84,6 @@ export function getTeamSizesByGoal(pool) { }, {}) } -export function getUserFeedback(pool, {respondentId, subjectId}) { - return ((((pool.userFeedback || {}).respondentIds || {})[respondentId] || {}).subjectIds || {})[subjectId] +export function getMemberFeedback(pool, {respondentId, subjectId}) { + return ((((pool.memberFeedback || {}).respondentIds || {})[respondentId] || {}).subjectIds || {})[subjectId] } diff --git a/src/server/util/index.js b/src/server/util/index.js index 37d80361..d97d0349 100644 --- a/src/server/util/index.js +++ b/src/server/util/index.js @@ -53,6 +53,7 @@ export { toPairs, pickRandom, mapById, + hashById, groupById, safePushInt, unique, diff --git a/src/server/workers/__tests__/projectCreated.test.js b/src/server/workers/__tests__/projectCreated.test.js index 496d2da4..10c2a21b 100644 --- a/src/server/workers/__tests__/projectCreated.test.js +++ b/src/server/workers/__tests__/projectCreated.test.js @@ -26,8 +26,8 @@ describe(testContext(__filename), function () { nock.cleanAll() this.phase = await factory.create('phase', {hasVoting: true}) this.project = await factory.create('project', {phaseId: this.phase.id}) - this.members = await mockIdmUsersById(this.project.memberIds, null, {times: 10}) - this.memberHandles = this.members.map(p => p.handle) + this.users = await mockIdmUsersById(this.project.memberIds, null, {strict: true, times: 10}) + this.memberHandles = this.users.map(p => p.handle) }) afterEach(function () { diff --git a/src/server/workers/__tests__/surveySubmitted.test.js b/src/server/workers/__tests__/surveySubmitted.test.js index eb2a43dd..18eaaac0 100644 --- a/src/server/workers/__tests__/surveySubmitted.test.js +++ b/src/server/workers/__tests__/surveySubmitted.test.js @@ -36,9 +36,7 @@ describe(testContext(__filename), function () { questionAttrs: {responseType: 'text', subjectType: 'member'}, subjectIds: () => [this.project.memberIds[1]], }) - useFixture.nockClean() - const {memberIds} = this.project - this.users = await mockIdmUsersById(memberIds, null, {times: 10}) + this.users = await mockIdmUsersById(this.project.memberIds, null, {times: 5}) this.handles = this.users.map(user => user.handle) }) diff --git a/src/server/workers/__tests__/userCreated.test.js b/src/server/workers/__tests__/userCreated.test.js index 5ef3ebbf..8abb2720 100644 --- a/src/server/workers/__tests__/userCreated.test.js +++ b/src/server/workers/__tests__/userCreated.test.js @@ -14,7 +14,7 @@ describe(testContext(__filename), function () { beforeEach(resetDB) describe('processUserCreated', function () { - describe('when there is a new user', function () { + describe('when there is a new user w/ a member role', function () { beforeEach(async function () { this.chapter = await factory.create('chapter', { inviteCodes: ['test'] @@ -23,7 +23,7 @@ describe(testContext(__filename), function () { chapterId: this.chapter.id, cycleNumber: 3, }) - this.user = await factory.build('user') + this.user = await factory.build('user', {roles: ['member']}) this.nockGitHub = (user, replyCallback = () => ({})) => { useFixture.nockClean() nock(config.server.github.baseURL) @@ -52,7 +52,6 @@ describe(testContext(__filename), function () { this.nockGitHub(this.user) await processUserCreated(this.user) const user = await Member.get(this.user.id) - expect(user).to.not.be.null }) diff --git a/src/server/workers/memberPhaseChanged.js b/src/server/workers/memberPhaseChanged.js index 572523a5..98ef9d65 100644 --- a/src/server/workers/memberPhaseChanged.js +++ b/src/server/workers/memberPhaseChanged.js @@ -1,4 +1,4 @@ -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' import addMemberToPoolInCycle from 'src/server/actions/addMemberToPoolInCycle' import {Phase, getLatestCycleForChapter} from 'src/server/services/dataService' import {GOAL_SELECTION} from 'src/common/models/cycle' @@ -11,12 +11,12 @@ export function start() { export async function processMemberPhaseChangeCompleted({old_val: oldMember, new_val: newMember}) { const chatService = require('src/server/services/chatService') - const memberInfo = (await getMemberInfo([newMember.id]))[0] + const memberUser = (await findMemberUsers([newMember.id]))[0] const newPhase = await Phase.get(newMember.phaseId) // invite the member to the new phase channel and send welcome message - await chatService.inviteToChannel(newPhase.channelName, [memberInfo.handle]) - await chatService.sendDirectMessage(memberInfo.handle, _phaseWelcomeMessage(newPhase)) + await chatService.inviteToChannel(newPhase.channelName, [memberUser.handle]) + await chatService.sendDirectMessage(memberUser.handle, _phaseWelcomeMessage(newPhase)) // update voting pools if voting is still open for the cycle const currentCycle = await getLatestCycleForChapter(newMember.chapterId) diff --git a/src/server/workers/surveySubmitted.js b/src/server/workers/surveySubmitted.js index ce9b7e3a..431e3ab0 100644 --- a/src/server/workers/surveySubmitted.js +++ b/src/server/workers/surveySubmitted.js @@ -1,5 +1,5 @@ import {mapById} from 'src/common/util' -import getMemberInfo from 'src/server/actions/getMemberInfo' +import findMemberUsers from 'src/server/actions/findMemberUsers' import {Survey, getProjectBySurveyId} from 'src/server/services/dataService' import sendRetroCompletedNotification from 'src/server/actions/sendRetroCompletedNotification' import {entireProjectTeamHasCompletedSurvey} from 'src/server/util/project' @@ -42,8 +42,8 @@ function buildRetroAnnouncement(project, survey) { async function announce(project, announcement) { const chatService = require('src/server/services/chatService') - const projectUsersById = mapById(await getMemberInfo(project.memberIds)) - const handles = project.memberIds.map(memberId => projectUsersById.get(memberId).handle) + const projectMemberMap = mapById(await findMemberUsers(project.memberIds)) + const handles = project.memberIds.map(memberId => projectMemberMap.get(memberId).handle) chatService.sendDirectMessage(handles, announcement) } diff --git a/src/test/helpers/fixtures.js b/src/test/helpers/fixtures.js index 00212952..61b0cfd8 100644 --- a/src/test/helpers/fixtures.js +++ b/src/test/helpers/fixtures.js @@ -56,9 +56,9 @@ export const useFixture = { } }) }, - setCurrentCycleAndUserForProject() { + setCurrentCycleAndMemberForProject() { beforeEach(function () { - this.setCurrentCycleAndUserForProject = async function (project) { + this.setCurrentCycleAndMemberForProject = async function (project) { this.currentCycle = await Cycle.get(project.cycleId) this.currentUser = await factory.build('user', {id: project.memberIds[0]}) this.member = await factory.build('member', {id: project.memberIds[0], chapterId: project.chapterId}) @@ -100,34 +100,39 @@ export const useFixture = { data: {[dataKey]: data}, }) }, - nockIDMGetUser(user) { + nockIDMGetUser(user, {times = 1} = {}) { this.apiScope = nock(config.server.idm.baseURL) .post('/graphql') + .times(times) .reply(200, { data: { getUser: user, }, }) }, - nockIDMGetUsersById(users, {times = 1} = {}) { + nockIDMFindUsers(users, {times = 1} = {}) { this.apiScope = nock(config.server.idm.baseURL) .post('/graphql') .times(times) .reply(200, { data: { - getUsersByIds: users, + findUsers: users, }, }) }, - nockIDMFindUsers(users, {times = 1} = {}) { + nockIDMDeactivateUser(user) { this.apiScope = nock(config.server.idm.baseURL) - .post('/graphql') - .times(times) - .reply(200, { + .persist() + .intercept('/graphql', 'POST') + .reply(200, () => ({ data: { - findUsers: users, + deactivateUser: { + id: user.id, + active: false, + handle: user.handle, + }, }, - }) + })) }, nockGetGoalInfo(goalNumber, {times = 1} = {}, overrideProps = {}) { this.apiScope = nock(config.server.goalLibrary.baseURL)