Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add functionality for adding people to a tag (GSoC) #2612

Merged
merged 4 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ enum ActionItemsOrderByInput {
dueDate_DESC
}

input AddPeopleToUserTagInput {
tagId: ID!
userIds: [ID!]!
}

type Address {
city: String
countryCode: String
Expand Down Expand Up @@ -1058,6 +1063,7 @@ type Mutation {
addLanguageTranslation(data: LanguageInput!): Language!
addOrganizationCustomField(name: String!, organizationId: ID!, type: String!): OrganizationCustomField!
addOrganizationImage(file: String!, organizationId: String!): Organization!
addPeopleToUserTag(input: AddPeopleToUserTagInput!): UserTag
addPledgeToFundraisingCampaign(campaignId: ID!, pledgeId: ID!): FundraisingCampaignPledge!
addUserCustomData(dataName: String!, dataValue: Any!, organizationId: ID!): UserCustomData!
addUserImage(file: String!): User!
Expand Down Expand Up @@ -1914,6 +1920,12 @@ type UserTag {
to.
"""
usersAssignedTo(after: String, before: String, first: PositiveInt, last: PositiveInt): UsersConnection

"""
A connection field to traverse a list of Users this UserTag is not assigned
to, to see and select among them and assign this tag.
"""
usersToAssignTo(after: String, before: String, first: PositiveInt, last: PositiveInt): UsersConnection
}

"""A default connection on the UserTag type."""
Expand Down
202 changes: 202 additions & 0 deletions src/resolvers/Mutation/addPeopleToUserTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { Types } from "mongoose";
import {
TAG_NOT_FOUND,
USER_DOES_NOT_BELONG_TO_TAGS_ORGANIZATION,
USER_NOT_AUTHORIZED_ERROR,
USER_NOT_FOUND_ERROR,
} from "../../constants";
import { errors, requestContext } from "../../libraries";
import type { InterfaceAppUserProfile, InterfaceUser } from "../../models";
import {
AppUserProfile,
OrganizationTagUser,
TagUser,
User,
} from "../../models";
import { cacheAppUserProfile } from "../../services/AppUserProfileCache/cacheAppUserProfile";
import { findAppUserProfileCache } from "../../services/AppUserProfileCache/findAppUserProfileCache";
import { cacheUsers } from "../../services/UserCache/cacheUser";
import { findUserInCache } from "../../services/UserCache/findUserInCache";
import type { MutationResolvers } from "../../types/generatedGraphQLTypes";

/**
* This function enables an admin to assign a tag to multiple users.
* @param _parent - parent of current request
* @param args - payload provided with the request
* @param context - context of entire application
* @remarks The following checks are done:
* 1. If the current user exists and has a profile.
* 2. If the tag object exists.
* 3. If the current user is an admin for the organization of the tag.
* 4. If each user to be assigned the tag exists and belongs to the tag's organization.
* 5. Assign the tag only to users who do not already have it.
* @returns Array of users to whom the tag was assigned.
*/

export const addPeopleToUserTag: MutationResolvers["addPeopleToUserTag"] =
async (_parent, args, context) => {
let currentUser: InterfaceUser | null;
const userFoundInCache = await findUserInCache([context.userId]);
currentUser = userFoundInCache[0];
if (currentUser === null) {
currentUser = await User.findOne({
_id: context.userId,
}).lean();
if (currentUser !== null) {
await cacheUsers([currentUser]);
}
}

// Checks whether the currentUser exists.
if (!currentUser) {
throw new errors.NotFoundError(
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
USER_NOT_FOUND_ERROR.CODE,
USER_NOT_FOUND_ERROR.PARAM,
);
}

// Get the tag object
const tag = await OrganizationTagUser.findOne({
_id: args.input.tagId,
}).lean();

if (!tag) {
throw new errors.NotFoundError(
requestContext.translate(TAG_NOT_FOUND.MESSAGE),
TAG_NOT_FOUND.CODE,
TAG_NOT_FOUND.PARAM,
);
}

let currentUserAppProfile: InterfaceAppUserProfile | null;

const appUserProfileFoundInCache = await findAppUserProfileCache([
currentUser.appUserProfileId.toString(),
]);

currentUserAppProfile = appUserProfileFoundInCache[0];
if (currentUserAppProfile === null) {
currentUserAppProfile = await AppUserProfile.findOne({
userId: currentUser._id,
}).lean();
if (currentUserAppProfile !== null) {
await cacheAppUserProfile([currentUserAppProfile]);
}
}
if (!currentUserAppProfile) {
throw new errors.UnauthorizedError(
requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE),
USER_NOT_AUTHORIZED_ERROR.CODE,
USER_NOT_AUTHORIZED_ERROR.PARAM,
);
}

// Boolean to determine whether user is an admin of the organization of the tag.
const currentUserIsOrganizationAdmin = currentUserAppProfile.adminFor.some(
(orgId) =>
orgId &&
new Types.ObjectId(orgId.toString()).equals(tag.organizationId),
);
//check whether current user can assign tag to users or not
if (
!(currentUserIsOrganizationAdmin || currentUserAppProfile.isSuperAdmin)
) {
throw new errors.UnauthorizedError(
requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE),
USER_NOT_AUTHORIZED_ERROR.CODE,
USER_NOT_AUTHORIZED_ERROR.PARAM,
);
}

// Check if the request users exist
const requestUsers = await User.find({
_id: { $in: args.input.userIds },
}).lean();

const requestUserMap = new Map(
requestUsers.map((user) => [user._id.toString(), user]),
);

// Validate each user to be assigned the tag
const validRequestUsers = [];
for (const userId of args.input.userIds) {
const requestUser = requestUserMap.get(userId);

if (!requestUser) {
throw new errors.NotFoundError(
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
USER_NOT_FOUND_ERROR.CODE,
USER_NOT_FOUND_ERROR.PARAM,
);
}

// Check that the user to which the tag is to be assigned is a member of the tag's organization
const requestUserBelongsToTagOrganization =
requestUser.joinedOrganizations.some((orgId) =>
orgId.equals(tag.organizationId),
);

if (!requestUserBelongsToTagOrganization) {
throw new errors.UnauthorizedError(
requestContext.translate(
USER_DOES_NOT_BELONG_TO_TAGS_ORGANIZATION.MESSAGE,
),
USER_DOES_NOT_BELONG_TO_TAGS_ORGANIZATION.CODE,
USER_DOES_NOT_BELONG_TO_TAGS_ORGANIZATION.PARAM,
);
}

validRequestUsers.push(requestUser);
}

// Check existing tag assignments
const existingTagAssignments = await TagUser.find({
userId: { $in: validRequestUsers.map((user) => user._id) },
tagId: tag._id,
}).lean();

const existingAssignmentsMap = existingTagAssignments.map((assign) =>
assign.userId.toString(),
);

// Filter out users who already have the tag
const newAssignments = validRequestUsers.filter(
(user) => !existingAssignmentsMap.includes(user._id.toString()),
);

if (newAssignments.length === 0) {
return tag; // No new assignments to be made
}

// Assign all the ancestor tags
const allAncestorTags = [tag._id];
let currentTag = tag;
while (currentTag?.parentTagId) {
const currentParentTag = await OrganizationTagUser.findOne({
_id: currentTag.parentTagId,
}).lean();

if (currentParentTag) {
allAncestorTags.push(currentParentTag?._id);
currentTag = currentParentTag;
}
}

const tagUserDocs = newAssignments.flatMap((user) =>
allAncestorTags.map((tagId) => ({
updateOne: {
filter: { userId: user._id, tagId },
update: { $setOnInsert: { userId: user._id, tagId } },
upsert: true,
setDefaultsOnInsert: true,
},
})),
);

if (tagUserDocs.length > 0) {
await TagUser.bulkWrite(tagUserDocs);
}

return tag;
};
2 changes: 2 additions & 0 deletions src/resolvers/Mutation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { addOrganizationImage } from "./addOrganizationImage";
import { addUserCustomData } from "./addUserCustomData";
import { addUserImage } from "./addUserImage";
import { addUserToUserFamily } from "./addUserToUserFamily";
import { addPeopleToUserTag } from "./addPeopleToUserTag";
import { assignUserTag } from "./assignUserTag";
import { blockPluginCreationBySuperadmin } from "./blockPluginCreationBySuperadmin";
import { blockUser } from "./blockUser";
Expand Down Expand Up @@ -125,6 +126,7 @@ export const Mutation: MutationResolvers = {
addUserCustomData,
addUserImage,
addUserToUserFamily,
addPeopleToUserTag,
removeUserFamily,
removeUserFromUserFamily,
createUserFamily,
Expand Down
2 changes: 2 additions & 0 deletions src/resolvers/UserTag/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { childTags } from "./childTags";
import { organization } from "./organization";
import { parentTag } from "./parentTag";
import { usersAssignedTo } from "./usersAssignedTo";
import { usersToAssignTo } from "./usersToAssignTo";

export const UserTag: UserTagResolvers = {
childTags,
organization,
parentTag,
usersAssignedTo,
usersToAssignTo,
};
Loading
Loading