From 887d4372e06345058636d735b819a7f543faaea1 Mon Sep 17 00:00:00 2001 From: Tom Watson Date: Fri, 27 May 2022 20:44:38 -0700 Subject: [PATCH 01/13] Add type to group input schema --- APIs.md | 1 + api/graphql/schema.graphql | 1 + 2 files changed, 2 insertions(+) diff --git a/APIs.md b/APIs.md index 043bb070d..4515e96cc 100644 --- a/APIs.md +++ b/APIs.md @@ -73,6 +73,7 @@ Example GraphQL mutation: "visibility": 1, // 0 => hidden (Only members can see), 1 => protected (only members and members of networked groups can see), 2 => public (anyone can see, including external public) "location": "12345 Farm Street, Farmville, Iowa, 50129, USA", "geoShape": , + "type:": , "groupExtensions": [ { "type": "farm-onboarding", diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 288bfbaed..670d735b6 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -884,6 +884,7 @@ input GroupInput { moderatorDescriptor: String moderatorDescriptorPlural: String name: String + type: String parentIds: [ID] prerequisiteGroupIds: [ID] settings: GroupSettingsInput From 16709450418ddd1910220345e82aeb5dcc5694b6 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Tue, 31 May 2022 21:55:09 -0700 Subject: [PATCH 02/13] Make sure type can be set when creating a group --- api/graphql/schema.graphql | 2 +- api/models/Group.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 670d735b6..b8207d2dd 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -884,7 +884,6 @@ input GroupInput { moderatorDescriptor: String moderatorDescriptorPlural: String name: String - type: String parentIds: [ID] prerequisiteGroupIds: [ID] settings: GroupSettingsInput @@ -892,6 +891,7 @@ input GroupInput { slackTeam: String slackConfigureUrl: String slug: String + type: String typeDescriptor: String typeDescriptorPlural: String visibility: Int diff --git a/api/models/Group.js b/api/models/Group.js index 5146dd636..24e1b29f0 100644 --- a/api/models/Group.js +++ b/api/models/Group.js @@ -472,7 +472,7 @@ module.exports = bookshelf.Model.extend(merge({ pick(data, 'about_video_uri', 'accessibility', 'avatar_url', 'description', 'slug', 'category', 'access_code', 'banner_url', 'location_id', 'location', 'group_data_type', 'moderator_descriptor', - 'moderator_descriptor_plural', 'name', 'type_descriptor', 'type_descriptor_plural', 'visibility' + 'moderator_descriptor_plural', 'name', 'type', 'type_descriptor', 'type_descriptor_plural', 'visibility' ), { 'accessibility': Group.Accessibility.RESTRICTED, From ecf80ef61fd6f00b5aa973294da44416d91dd352 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Tue, 31 May 2022 21:56:27 -0700 Subject: [PATCH 03/13] Tweak API docs --- APIs.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/APIs.md b/APIs.md index 4515e96cc..3e764fbd2 100644 --- a/APIs.md +++ b/APIs.md @@ -73,7 +73,6 @@ Example GraphQL mutation: "visibility": 1, // 0 => hidden (Only members can see), 1 => protected (only members and members of networked groups can see), 2 => public (anyone can see, including external public) "location": "12345 Farm Street, Farmville, Iowa, 50129, USA", "geoShape": , - "type:": , "groupExtensions": [ { "type": "farm-onboarding", @@ -98,7 +97,7 @@ Example GraphQL mutation: locationDisplayPrecision: precise, // precise => precise location displayed, near => location text shows nearby town/city and coordinate shifted, region => location not shown on map at all and location text shows nearby city/region publicMemberDirectory: false, // Boolean }, - "type": "farm", // Optionally set the group type to farm, don't pass in for regular groups + "type": , "typeDescriptor": "Ranch", // Group is the default "typeDescriptorPlural": "Ranches" // Groups is the default }, From 1e3198167b40c0df66fd20aa6a7e7a2ba3e56963 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Thu, 2 Jun 2022 15:47:08 -0700 Subject: [PATCH 04/13] Return groups that match a bounding box if they have a geoShape but no location --- api/services/Search/util.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api/services/Search/util.js b/api/services/Search/util.js index 9f90f78e2..09bd11430 100644 --- a/api/services/Search/util.js +++ b/api/services/Search/util.js @@ -142,7 +142,8 @@ export const filterAndSortUsers = curry(({ autocomplete, boundingBox, order, sea if (boundingBox) { q.join('locations', 'locations.id', '=', 'users.location_id') - q.whereRaw('locations.center && ST_MakeEnvelope(?, ?, ?, ?, 4326)', [boundingBox[0].lng, boundingBox[0].lat, boundingBox[1].lng, boundingBox[1].lat]) + const bb = [boundingBox[0].lng, boundingBox[0].lat, boundingBox[1].lng, boundingBox[1].lat] + q.whereRaw('locations.center && ST_MakeEnvelope(?, ?, ?, ?, 4326)', bb) } }) @@ -157,8 +158,12 @@ export const filterAndSortGroups = curry((opts, q) => { } if (boundingBox) { - q.join('locations', 'locations.id', '=', 'groups.location_id') - q.whereRaw('locations.center && ST_MakeEnvelope(?, ?, ?, ?, 4326)', [boundingBox[0].lng, boundingBox[0].lat, boundingBox[1].lng, boundingBox[1].lat]) + q.leftJoin('locations', 'locations.id', '=', 'groups.location_id') + const bb = [boundingBox[0].lng, boundingBox[0].lat, boundingBox[1].lng, boundingBox[1].lat] + q.where(q2 => + q2.whereRaw('locations.center && ST_MakeEnvelope(?, ?, ?, ?, 4326)', bb) + .orWhereRaw('ST_Intersects(groups.geo_shape, ST_MakeEnvelope(?, ?, ?, ?, 4326))', bb) + ) } if (sortBy === 'size') { From 23ac9f0aa23b5513483a748871ddb86e5d162bdb Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Thu, 2 Jun 2022 16:10:46 -0700 Subject: [PATCH 05/13] Fix geocoding of locations when only a location string is passed in to create a group --- api/models/Group.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/models/Group.js b/api/models/Group.js index 24e1b29f0..2b555dfbd 100644 --- a/api/models/Group.js +++ b/api/models/Group.js @@ -578,11 +578,11 @@ module.exports = bookshelf.Model.extend(merge({ geocoder.forwardGeocode({ mode: 'mapbox.places-permanent', query: group.get('location') - }).send().then(response => { + }).send().then(async (response) => { const match = response.body if (match?.features && match?.features.length > 0) { const locationData = omit(LocationHelpers.convertMapboxToLocation(match.features[0]), 'mapboxId') - const loc = findOrCreateLocation(locationData) + const loc = await findOrCreateLocation(locationData) group.save({ location_id: loc.id }) } }) From 904e7ac575f1ba422cec69263733bd4e5b4bb852 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Thu, 2 Jun 2022 16:10:46 -0700 Subject: [PATCH 06/13] Fix geocoding of locations when only a location string is passed in to create a group --- api/models/Group.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/models/Group.js b/api/models/Group.js index 24e1b29f0..2b555dfbd 100644 --- a/api/models/Group.js +++ b/api/models/Group.js @@ -578,11 +578,11 @@ module.exports = bookshelf.Model.extend(merge({ geocoder.forwardGeocode({ mode: 'mapbox.places-permanent', query: group.get('location') - }).send().then(response => { + }).send().then(async (response) => { const match = response.body if (match?.features && match?.features.length > 0) { const locationData = omit(LocationHelpers.convertMapboxToLocation(match.features[0]), 'mapboxId') - const loc = findOrCreateLocation(locationData) + const loc = await findOrCreateLocation(locationData) group.save({ location_id: loc.id }) } }) From 4a1b8573313cd2f8659612534d837ee7140474c4 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Thu, 2 Jun 2022 16:41:22 -0700 Subject: [PATCH 07/13] Add user id to member export CSV Fixes: https://github.com/Hylozoic/hylo-node/issues/816 --- api/controllers/ExportController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/ExportController.js b/api/controllers/ExportController.js index 5418cabde..842424466 100644 --- a/api/controllers/ExportController.js +++ b/api/controllers/ExportController.js @@ -57,7 +57,7 @@ async function exportMembers(groupId, req, email) { await Promise.all(users.map((u, idx) => { // pluck core user data into results results.push(u.pick([ - 'name', 'contact_email', 'contact_phone', 'avatar_url', 'tagline', 'bio', + 'id', 'name', 'contact_email', 'contact_phone', 'avatar_url', 'tagline', 'bio', 'url', 'twitter_name', 'facebook_url', 'linkedin_url' ])) @@ -122,7 +122,7 @@ async function exportMembers(groupId, req, email) { // send data as CSV response output(results, [ - 'name', 'contact_email', 'contact_phone', 'location', 'avatar_url', 'tagline', 'bio', + 'id', 'name', 'contact_email', 'contact_phone', 'location', 'avatar_url', 'tagline', 'bio', { key: 'url', header: 'personal_url' }, 'twitter_name', 'facebook_url', 'linkedin_url', 'skills', 'skills_to_learn', From 966aeec3a38082e87091cffd87fc3fae6974b455 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Fri, 3 Jun 2022 12:25:42 -0700 Subject: [PATCH 08/13] Update test schema and timeout so tests work again Not sure why timeout for the setup stuff had to be increased but otherwise it wouldnt run locally for me --- migrations/schema.sql | 12 +++++++++--- test/setup/index.js | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/migrations/schema.sql b/migrations/schema.sql index 7910592af..d455da067 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -893,7 +893,12 @@ CREATE TABLE public.groups ( slack_team text, slack_configure_url text, type text, - geo_shape public.geometry(Polygon,4326) + geo_shape public.geometry(Polygon,4326), + type_descriptor character varying(255) DEFAULT NULL::character varying, + type_descriptor_plural character varying(255) DEFAULT NULL::character varying, + moderator_descriptor character varying(255) DEFAULT NULL::character varying, + moderator_descriptor_plural character varying(255) DEFAULT NULL::character varying, + about_video_uri character varying(255) ); @@ -1158,11 +1163,12 @@ CREATE TABLE public.locations ( region character varying(255), neighborhood character varying(255), postcode character varying(255), - country character varying(255), + country_code character varying(255), accuracy character varying(255), wikidata character varying(255), created_at timestamp with time zone, - updated_at timestamp with time zone + updated_at timestamp with time zone, + country character varying(255) ); diff --git a/test/setup/index.js b/test/setup/index.js index 9de5e6488..7085a6527 100644 --- a/test/setup/index.js +++ b/test/setup/index.js @@ -16,7 +16,7 @@ var TestSetup = function () { var setup = new TestSetup() before(function (done) { - this.timeout(30000) + this.timeout(50000) var i18n = require('i18n') i18n.configure(require(root('config/i18n')).i18n) From 7a9930b640b58f2b02e91a66199a619607321122 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Mon, 6 Jun 2022 16:27:30 -0700 Subject: [PATCH 09/13] Add role parameter to createUser api call For SurveyStack --- api/controllers/UserController.js | 18 +++++++++++++----- api/models/User.js | 6 +++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/api/controllers/UserController.js b/api/controllers/UserController.js index a838397f3..9ad837dd6 100644 --- a/api/controllers/UserController.js +++ b/api/controllers/UserController.js @@ -4,7 +4,7 @@ import OIDCAdapter from '../services/oidc/KnexAdapter' module.exports = { create: async function (req, res) { - const { name, email, groupId } = req.allParams() + const { name, email, groupId, isModerator } = req.allParams() const group = groupId && await Group.find(groupId) let user = await User.find(email, {}, false) @@ -26,18 +26,26 @@ module.exports = { const inviteBy = await group.moderators().fetchOne() await InvitationService.create({ - sessionUserId: inviteBy?.id, groupId: group.id, - userIds: [user.id], + isModerator, message, - subject + sessionUserId: inviteBy?.id, + subject, + userIds: [user.id] }) + return res.ok({ message: `User already exists, invite sent to group ${group.get('name')}` }) } + return res.ok({ message: `User already exists, and is already a member of this group` }) } return res.ok({ message: "User already exists" }) } - return User.create({name, email: email ? email.toLowerCase() : null, email_validated: false, active: false, group }) + const attrs = { name, email: email ? email.toLowerCase() : null, email_validated: false, active: false, group } + if (isModerator) { + attrs['role'] = GroupMembership.Role.MODERATOR + } + + return User.create(attrs) .then(async (user) => { Queue.classMethod('Email', 'sendFinishRegistration', { email, diff --git a/api/models/User.js b/api/models/User.js index 12f3896b2..b6af1e29e 100644 --- a/api/models/User.js +++ b/api/models/User.js @@ -600,7 +600,7 @@ module.exports = bookshelf.Model.extend(merge({ }, create: function (attributes) { - const { account, group } = attributes + const { account, group, role } = attributes attributes = merge({ avatar_url: User.gravatar(attributes.email), @@ -613,7 +613,7 @@ module.exports = bookshelf.Model.extend(merge({ comment_notifications: 'both' }, active: true - }, omit(attributes, 'account', 'group')) + }, omit(attributes, 'account', 'group', 'role')) if (account) { merge( @@ -628,7 +628,7 @@ module.exports = bookshelf.Model.extend(merge({ .then(async (user) => { await Promise.join( account && LinkedAccount.create(user.id, account, {transacting}), - group && group.addMembers([user.id], {}, {transacting}), + group && group.addMembers([user.id], { role: role || GroupMembership.Role.DEFAULT }, {transacting}), group && user.markInvitationsUsed(group.id, transacting) ) return user From bec281ee6fdb1d234572087a92ca468ab171af97 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Mon, 6 Jun 2022 16:40:56 -0700 Subject: [PATCH 10/13] Make sure isModerator: false is interpreted correctly Add API docs --- APIs.md | 1 + api/controllers/UserController.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/APIs.md b/APIs.md index 3e764fbd2..6ad836980 100644 --- a/APIs.md +++ b/APIs.md @@ -35,6 +35,7 @@ __Parameters:__ - name (required) = Judy Mangrove - email (required) = email@email.com - groupId (optional) = the id of a group to add the user to +- isModerator (optional) = true to add the user to the group specified by groupId as a moderator __Return value__: diff --git a/api/controllers/UserController.js b/api/controllers/UserController.js index 9ad837dd6..1398fd961 100644 --- a/api/controllers/UserController.js +++ b/api/controllers/UserController.js @@ -6,6 +6,7 @@ module.exports = { create: async function (req, res) { const { name, email, groupId, isModerator } = req.allParams() const group = groupId && await Group.find(groupId) + const isModeratorVal = isModerator && isModerator === 'true' let user = await User.find(email, {}, false) if (user) { @@ -27,7 +28,7 @@ module.exports = { await InvitationService.create({ groupId: group.id, - isModerator, + isModerator: isModeratorVal, message, sessionUserId: inviteBy?.id, subject, @@ -41,7 +42,7 @@ module.exports = { } const attrs = { name, email: email ? email.toLowerCase() : null, email_validated: false, active: false, group } - if (isModerator) { + if (isModeratorVal) { attrs['role'] = GroupMembership.Role.MODERATOR } From f65ef2ba7a943c6bf22293da2acc05e149a90832 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Mon, 6 Jun 2022 17:03:27 -0700 Subject: [PATCH 11/13] Add new API call addMember to add person to a group Role can be set to 1 to add them as a moderator --- APIs.md | 20 ++++++++++++++++++++ api/graphql/index.js | 2 ++ api/graphql/mutations/group.js | 13 +++++++++++++ api/graphql/mutations/index.js | 1 + api/graphql/schema.graphql | 1 + 5 files changed, 37 insertions(+) diff --git a/APIs.md b/APIs.md index 6ad836980..40167aa48 100644 --- a/APIs.md +++ b/APIs.md @@ -129,6 +129,26 @@ Example GraphQL mutation: } ``` +### Add a Person to a Group + +`POST to https://hylo.com/noo/graphql` + +__Headers:__ +Content-Type: application/json + +This is a GraphQL based endpoint so you will want the pass in a raw POST data +Example GraphQL mutation: +``` +{ + "query": "mutation ($userId: ID, $groupId: ID, $role: Int) { addMember(userId: $userId, groupId: $groupId, role: $role) { success error } }", + "variables": { + "groupId": USER_ID, + "groupId": GROUP_ID, + "role": 0 // 0 = regular member, 1 = Moderator + } +} +``` + ### Query a Group `POST to https://hylo.com/noo/graphql` diff --git a/api/graphql/index.js b/api/graphql/index.js index aabf5d977..256ae6b51 100644 --- a/api/graphql/index.js +++ b/api/graphql/index.js @@ -6,6 +6,7 @@ import { presentQuerySet } from '../../lib/graphql-bookshelf-bridge/util' import { acceptGroupRelationshipInvite, acceptJoinRequest, + addMember, addModerator, addPeopleToProjectRole, addSkill, @@ -435,6 +436,7 @@ export function makeApiQueries (fetchOne) { export function makeApiMutations () { return { + addMember: (root, { userId, groupId, role }) => addMember(userId, groupId, role), createGroup: (root, { asUserId, data }) => createGroup(asUserId, data), updateGroup: (root, { asUserId, id, changes }) => updateGroup(asUserId, id, changes) } diff --git a/api/graphql/mutations/group.js b/api/graphql/mutations/group.js index 4ac2acaaf..575787cd9 100644 --- a/api/graphql/mutations/group.js +++ b/api/graphql/mutations/group.js @@ -200,3 +200,16 @@ export async function rejectGroupRelationshipInvite (userId, groupRelationshipIn throw new Error(`Invalid parameters to reject invite`) } } + +// API only Group Mutations +export async function addMember (userId, groupId, role) { + const group = await Group.find(groupId) + if (!group) { + return { success: false, error: 'Group not found' } + } + + if (group) { + await group.addMembers([userId], { role: role || GroupMembership.Role.DEFAULT }, {}) + } + return { success: true } +} diff --git a/api/graphql/mutations/index.js b/api/graphql/mutations/index.js index 8846b781a..4e185d635 100644 --- a/api/graphql/mutations/index.js +++ b/api/graphql/mutations/index.js @@ -23,6 +23,7 @@ export { } from './event' export { acceptGroupRelationshipInvite, + addMember, addModerator, cancelGroupRelationshipInvite, createGroup, diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index b8207d2dd..8bbfceff9 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -966,6 +966,7 @@ input GroupWidgetSettingsInput { type Mutation { acceptGroupRelationshipInvite(groupRelationshipInviteId: ID): AcceptGroupRelationshipInviteResult acceptJoinRequest(joinRequestId: ID): JoinRequest + addMember(userId: ID, groupId: ID, role: Int): GenericResult addModerator(personId: ID, groupId: ID): Group addPeopleToProjectRole(peopleIds: [ID], projectRoleId: ID): GenericResult addSkill(name: String, type: Int): Skill From 88ec4ec492c83eb5162c367b1648140920a36a6d Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Mon, 6 Jun 2022 17:09:55 -0700 Subject: [PATCH 12/13] Docs tweak --- APIs.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/APIs.md b/APIs.md index 40167aa48..015308c90 100644 --- a/APIs.md +++ b/APIs.md @@ -48,9 +48,14 @@ On success this will return a JSON object that looks like: } ``` -If there is already a user with this email registered you will receive: -`{ "message": "User already exists" }` +If there is already a user with this email but they are a not member of the group, this call will send them an invitation to join the group. You will receive: +{ message: `User already exists, invite sent to group GROUP_NAME` } + +If there is already a user with this email and they are already a member of the group: +{ message: `User already exists, and is already a member of this group` } +If there is already a user with this email and you didn't pass in a group you will receive: +`{ "message": "User already exists" }` ### Create a Group From 26da5f444d6c9010972e9f62b17d11e159d601ed Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Wed, 8 Jun 2022 21:28:56 -0700 Subject: [PATCH 13/13] Update to version 4.1.2 --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f3d16ed..1214c4e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [4.1.2] - 2022-06-08 + +### Added +- Add user id to member export CSV +- Add role parameter to createUser api call +- New API call addMember to add person to a group +- Group type can be set when creating a group + +### Fixed +- Return groups that match a bounding box if they have a geoShape but no location +- Geocoding of locations when only a location string is passed in to create a group + ## [4.1.1] - 2022-05-26 ### Added diff --git a/package.json b/package.json index c91df92fa..5b2114c3b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "Tibet Sprague ", "license": "GNU AFFERO GENERAL PUBLIC LICENSE v3", "private": true, - "version": "4.1.1", + "version": "4.1.2", "repository": { "type": "git", "url": "git://github.com/Hylozoic/hylo-node.git"