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

keyword search for quantifiers & Quantify multiple praise #499

Merged
merged 11 commits into from
Jun 30, 2022
142 changes: 33 additions & 109 deletions packages/api/src/praise/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import {
TypedRequestQuery,
TypedResponse,
} from '@shared/types';
import { EventLogTypeKey } from '@eventlog/types';
import { logEvent } from '@eventlog/utils';
import { Request } from 'express';
import { Types } from 'mongoose';
import { PraiseModel } from './entities';
import {
praiseDocumentListTransformer,
Expand All @@ -24,12 +21,12 @@ import {
import {
PraiseAllInput,
PraiseDetailsDto,
PraiseDocument,
PraiseDto,
QuantificationCreateUpdateInput,
QuantifyMultiplePraiseInput,
} from './types';
import { praiseWithScore, getPraisePeriod } from './utils/core';
import { PeriodStatusType } from '@period/types';
import { praiseWithScore } from './utils/core';
import { quantifyPraise } from '@praise/utils/quantify';
interface PraiseAllInputParsedQs extends Query, QueryInput, PraiseAllInput {}

/**
Expand Down Expand Up @@ -89,112 +86,39 @@ export const quantify = async (
req: TypedRequestBody<QuantificationCreateUpdateInput>,
res: TypedResponse<PraiseDto[]>
): Promise<void> => {
const praise = await PraiseModel.findById(req.params.id).populate(
'giver receiver forwarder'
);
if (!praise) throw new NotFoundError('Praise');

const period = await getPraisePeriod(praise);
if (!period)
throw new BadRequestError('Praise does not have an associated period');

if (period.status !== PeriodStatusType.QUANTIFY)
throw new BadRequestError(
'Period associated with praise does have status QUANTIFY'
);

const { score, dismissed, duplicatePraise } = req.body;

const quantification = praise.quantifications.find((q) =>
q.quantifier.equals(res.locals.currentUser._id)
);

if (!quantification)
throw new BadRequestError('User not assigned as quantifier for praise.');

let eventLogMessage = '';

// Collect all affected praises (i.e. any praises whose scoreRealized will change as a result of this change)
const affectedPraises: PraiseDocument[] = [praise];

const praisesDuplicateOfThis = await PraiseModel.find({
quantifications: {
$elemMatch: {
quantifier: res.locals.currentUser._id,
duplicatePraise: praise._id,
},
},
}).populate('giver receiver forwarder');

if (praisesDuplicateOfThis?.length > 0)
affectedPraises.push(...praisesDuplicateOfThis);

// Modify praise quantification values
if (duplicatePraise) {
if (duplicatePraise === praise._id.toString())
throw new BadRequestError('Praise cannot be a duplicate of itself');

const dp = await PraiseModel.findById(duplicatePraise);
if (!dp) throw new BadRequestError('Duplicate praise item not found');

if (praisesDuplicateOfThis?.length > 0)
throw new BadRequestError(
'Praise cannot be marked duplicate when it is the original of another duplicate'
);

const praisesDuplicateOfAnotherDuplicate = await PraiseModel.find({
_id: duplicatePraise,
quantifications: {
$elemMatch: {
quantifier: res.locals.currentUser._id,
duplicatePraise: { $exists: 1 },
},
},
});

if (praisesDuplicateOfAnotherDuplicate?.length > 0)
throw new BadRequestError(
'Praise cannot be marked duplicate of another duplicate'
);

quantification.score = 0;
quantification.dismissed = false;
quantification.duplicatePraise = dp._id;

eventLogMessage = `Marked the praise with id "${(
praise._id as Types.ObjectId
).toString()}" as duplicate of the praise with id "${(
dp._id as Types.ObjectId
).toString()}"`;
} else if (dismissed) {
quantification.score = 0;
quantification.dismissed = true;
quantification.duplicatePraise = undefined;

eventLogMessage = `Dismissed the praise with id "${(
praise._id as Types.ObjectId
).toString()}"`;
} else {
quantification.score = score;
quantification.dismissed = false;
quantification.duplicatePraise = undefined;

eventLogMessage = `Gave a score of ${
quantification.score
} to the praise with id "${(praise._id as Types.ObjectId).toString()}"`;
}
const affectedPraises = await quantifyPraise({
id: req.params.id,
bodyParams: req.body,
currentUser: res.locals.currentUser,
});

await praise.save();
const response = await praiseDocumentListTransformer(affectedPraises);
res.status(200).json(response);
};

await logEvent(
EventLogTypeKey.QUANTIFICATION,
eventLogMessage,
{
userId: res.locals.currentUser._id,
},
period._id
/**
* Quantify multiple praise items
* @param req
* @param res
*/
export const quantifyMultiple = async (
Copy link
Collaborator

@mattyg mattyg Jun 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function will need to have same logic used in the quantify function to determine all the praises with an affected scoreRealized. It probably makes sense to pull that logic out into a utility function and use it in both controllers.

The reason for that logic is so the "Duplicate Score" display is updated when the original praise's score is updated. See the screencast below:

Kazam_screencast_00003.mp4

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function will need to have same logic used in the quantify function to determine all the praises with an affected scoreRealized. It probably makes sense to pull that logic out into a utility function and use it in both controllers.

The reason for that logic is so the "Duplicate Score" display is updated when the original praise's score is updated. See the screencast below:

Kazam_screencast_00003.mp4

☝️☝️ @nebs-dev, this remains to be done.

req: TypedRequestBody<QuantifyMultiplePraiseInput>,
res: TypedResponse<PraiseDto[]>
): Promise<void> => {
const { praiseIds } = req.body;

const praiseItems = await Promise.all(
praiseIds.map(async (id) => {
const affectedPraises = await quantifyPraise({
id,
bodyParams: req.body,
currentUser: res.locals.currentUser,
});

return affectedPraises;
})
);

const response = await praiseDocumentListTransformer(affectedPraises);
const response = await praiseDocumentListTransformer(praiseItems.flat());
res.status(200).json(response);
};
5 changes: 5 additions & 0 deletions packages/api/src/praise/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@ praiseRouter.patchAsync(
authMiddleware(UserRole.QUANTIFIER),
controller.quantify
);
praiseRouter.patchAsync(
'/quantify',
authMiddleware(UserRole.QUANTIFIER),
controller.quantifyMultiple
);

export { praiseRouter };
5 changes: 5 additions & 0 deletions packages/api/src/praise/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export interface QuantificationCreateUpdateInput {
duplicatePraise: string;
}

export interface QuantifyMultiplePraiseInput {
score: number;
praiseIds: string[];
}

export interface Receiver {
_id: string;
praiseCount: number;
Expand Down
135 changes: 135 additions & 0 deletions packages/api/src/praise/utils/quantify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { BadRequestError, NotFoundError } from '@error/errors';
import { EventLogTypeKey } from '@eventlog/types';
import { logEvent } from '@eventlog/utils';
import { PeriodStatusType } from '@period/types';
import { PraiseModel } from '@praise/entities';
import { PraiseDocument } from '@praise/types';
import { getPraisePeriod } from '@praise/utils/core';
import { UserDocument } from '@user/types';
import { Types } from 'mongoose';

interface BodyParams {
score: number;
dismissed?: boolean;
duplicatePraise?: string;
}

interface QuantifyPraiseProps {
id: string;
bodyParams: BodyParams;
currentUser: UserDocument;
}

export const quantifyPraise = async ({
id,
bodyParams,
currentUser,
}: QuantifyPraiseProps): Promise<PraiseDocument[]> => {
const { score, dismissed, duplicatePraise } = bodyParams;

const praise = await PraiseModel.findById(id).populate(
'giver receiver forwarder'
);
if (!praise) throw new NotFoundError('Praise');

const period = await getPraisePeriod(praise);
if (!period)
throw new BadRequestError('Praise does not have an associated period');

if (period.status !== PeriodStatusType.QUANTIFY)
throw new BadRequestError(
'Period associated with praise does have status QUANTIFY'
);

const quantification = praise.quantifications.find((q) =>
q.quantifier.equals(currentUser._id)
);

if (!quantification)
throw new BadRequestError('User not assigned as quantifier for praise.');

let eventLogMessage = '';

// Collect all affected praises (i.e. any praises whose scoreRealized will change as a result of this change)
const affectedPraises: PraiseDocument[] = [praise];

const praisesDuplicateOfThis = await PraiseModel.find({
quantifications: {
$elemMatch: {
quantifier: currentUser._id,
duplicatePraise: praise._id,
},
},
}).populate('giver receiver forwarder');

if (praisesDuplicateOfThis?.length > 0)
affectedPraises.push(...praisesDuplicateOfThis);

// Modify praise quantification values
if (duplicatePraise) {
if (duplicatePraise === praise._id.toString())
throw new BadRequestError('Praise cannot be a duplicate of itself');

const dp = await PraiseModel.findById(duplicatePraise);
if (!dp) throw new BadRequestError('Duplicate praise item not found');

if (praisesDuplicateOfThis?.length > 0)
throw new BadRequestError(
'Praise cannot be marked duplicate when it is the original of another duplicate'
);

const praisesDuplicateOfAnotherDuplicate = await PraiseModel.find({
_id: duplicatePraise,
quantifications: {
$elemMatch: {
quantifier: currentUser._id,
duplicatePraise: { $exists: 1 },
},
},
});

if (praisesDuplicateOfAnotherDuplicate?.length > 0)
throw new BadRequestError(
'Praise cannot be marked duplicate of another duplicate'
);

quantification.score = 0;
quantification.dismissed = false;
quantification.duplicatePraise = dp._id;

eventLogMessage = `Marked the praise with id "${(
praise._id as Types.ObjectId
).toString()}" as duplicate of the praise with id "${(
dp._id as Types.ObjectId
).toString()}"`;
} else if (dismissed) {
quantification.score = 0;
quantification.dismissed = true;
quantification.duplicatePraise = undefined;

eventLogMessage = `Dismissed the praise with id "${(
praise._id as Types.ObjectId
).toString()}"`;
} else {
quantification.score = score;
quantification.dismissed = false;
quantification.duplicatePraise = undefined;

eventLogMessage = `Gave a score of ${
quantification.score
} to the praise with id "${(praise._id as Types.ObjectId).toString()}"`;
}

await praise.save();

await logEvent(
EventLogTypeKey.QUANTIFICATION,
eventLogMessage,
{
userId: currentUser._id,
},
period._id
);

return affectedPraises;
};
10 changes: 5 additions & 5 deletions packages/frontend/src/components/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { classNames } from '../utils';

interface Props {
disabled?: boolean;
Expand All @@ -17,11 +18,10 @@ const IconButton = ({
return (
<button
disabled={disabled}
className={
disabled
? 'praise-button-disabled space-x-2'
: 'praise-button space-x-2'
}
className={classNames(
'space-x-2',
disabled ? 'praise-button-disabled' : 'praise-button'
)}
onClick={onClick}
>
<FontAwesomeIcon icon={icon} size="1x" />
Expand Down
Loading