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: replace active quantifier #503

Merged
merged 32 commits into from
Jun 29, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9332450
fix(api): remove check to verify that *all* quantifiers were assigned…
mattyg Jun 22, 2022
fa1d935
chore(api): remove unnecessary logging
mattyg Jun 22, 2022
14709b0
fix(api): replace setting PRAISE_QUANTIIFIERS_ASSIGN_ALL with PRAISE_…
mattyg Jun 22, 2022
594ae3f
chore: changelog
mattyg Jun 22, 2022
74c2814
chore: changelog re-wording
mattyg Jun 22, 2022
af61fab
chore(frontend): delete unused component
mattyg Jun 26, 2022
604aa63
chore(frontend): remove unncessary function call wrappers
mattyg Jun 26, 2022
8a7f06e
refactor(frontend): delete MarkDuplicateButton, MarkDismissedButton -…
mattyg Jun 26, 2022
a02d54b
feat(api): period controller to replace a quantifier
mattyg Jun 26, 2022
a60dc4f
feat(frontend): UX to replace an active quantifier with another that …
mattyg Jun 26, 2022
f95da2a
chore: remove no longer needed cli command 'replace-quantifier'
mattyg Jun 26, 2022
9551bc9
fix(frontend): typo on tailwind class name
mattyg Jun 27, 2022
090110e
style(frontend): make SelectUserRadioGroup expand height up to max size
mattyg Jun 27, 2022
269be5d
refactor(frontend): remove headlessui Popover from UserPopover, as it…
mattyg Jun 27, 2022
0e26fb7
style(frontend): support dark mode in SelectUserRadioGroup
mattyg Jun 27, 2022
7597797
fix(api): ensure that quantification scores are completely reset when…
mattyg Jun 27, 2022
eba1fa2
refactor(api): use getPeriodDateRangeQuery to reduce duplicate logic
mattyg Jun 27, 2022
b963d21
fix(frontend): display Notice when no unassigned quantifiers are avai…
mattyg Jun 27, 2022
8d86635
chore: changelog
mattyg Jun 27, 2022
9ab0fac
fix: after replacing quantifier, update both period and affected prai…
mattyg Jun 27, 2022
25426e4
feat(frontend): support replacing quantifier with another active quan…
mattyg Jun 27, 2022
915ef74
style(frontend): specify dark / light mode text color in UserPopover
mattyg Jun 27, 2022
7f55486
fix(api): log the user who called replaceQuantifier
mattyg Jun 27, 2022
d229cdc
Styling adjustments
kristoferlund Jun 28, 2022
e3c7fe4
Merge branch 'main' into fix/allow_even_quantifier_assignment_when_to…
kristoferlund Jun 28, 2022
f5fe398
test(api): fix settings and periodsettings tests - use seeded setting…
mattyg Jun 28, 2022
076ba41
Merge branch 'fix/allow_even_quantifier_assignment_when_too_many_quan…
mattyg Jun 28, 2022
a6b57f6
fix(api): prevent replacing quantifier with another that is already a…
mattyg Jun 28, 2022
63e45e4
fix(frontend): clear selected user after submit in ReplaceQuantifierD…
mattyg Jun 28, 2022
75c975b
feat(frontend): exclude the original quantifier from the list of poss…
mattyg Jun 28, 2022
dd7601f
fix(frontend): update replace quantifier success message to indicate …
mattyg Jun 28, 2022
830d0cb
test(api): tests for replaceQuantifiers controller
mattyg Jun 28, 2022
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Admins can replace an actively assigned quantifier with another

### Fixed

Expand Down
3 changes: 1 addition & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"test": "NODE_ENV=testing yarn run load-env nyc mocha -r ts-node/register -r tsconfig-paths/register -r src/tests/setup.ts 'src/tests/**/*.spec.ts' --exit",
"import-praise": "yarn run load-env ts-node -r tsconfig-paths/register src/scripts/import-praise.ts",
"not-yet-activated": "yarn run load-env ts-node -r tsconfig-paths/register src/scripts/not-yet-activated.ts",
"replace-quantifier": "yarn run load-env ts-node -r tsconfig-paths/register src/scripts/replace-quantifier.ts",
"migrator": "yarn run load-env ts-node -r tsconfig-paths/register src/scripts/umzug-cli.ts",
"drop-database": "yarn run load-env ts-node -r tsconfig-paths/register src/scripts/drop-database.ts"
},
Expand Down Expand Up @@ -80,4 +79,4 @@
"typescript": "^4.5.4",
"yargs": "^17.3.1"
}
}
}
110 changes: 105 additions & 5 deletions packages/api/src/period/controllers/assignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ import {
PeriodDetailsDto,
PeriodStatusType,
VerifyQuantifierPoolSizeResponse,
PeriodReplaceQuantifierDto,
} from '../types';
import {
findPeriodDetailsDto,
getPreviousPeriodEndDate,
verifyAnyPraiseAssigned,
getPeriodDateRangeQuery,
} from '../utils';
import { PeriodModel } from '../entities';
import { praiseDocumentListTransformer } from '@praise/transformers';

/**
* Get all receivers with praise data
Expand All @@ -44,12 +46,12 @@ import { PeriodModel } from '../entities';
const queryReceiversWithPraise = async (
period: PeriodDocument
): Promise<Receiver[]> => {
const previousPeriodEndDate = await getPreviousPeriodEndDate(period);
const dateRangeQuery = await getPeriodDateRangeQuery(period);

return PraiseModel.aggregate([
{
$match: {
createdAt: { $gt: previousPeriodEndDate, $lte: period.endDate },
createdAt: dateRangeQuery,
},
},
{
Expand Down Expand Up @@ -215,10 +217,10 @@ const verifyAssignments = async (
PRAISE_QUANTIFIERS_PER_PRAISE_RECEIVER: number,
assignments: Assignments
): Promise<void> => {
const previousPeriodEndDate = await getPreviousPeriodEndDate(period);
const dateRangeQuery = await getPeriodDateRangeQuery(period);

const totalPraiseCount: number = await PraiseModel.count({
createdAt: { $gt: previousPeriodEndDate, $lte: period.endDate },
createdAt: dateRangeQuery,
});
const expectedAccountedPraiseCount: number =
totalPraiseCount * PRAISE_QUANTIFIERS_PER_PRAISE_RECEIVER;
Expand Down Expand Up @@ -561,3 +563,101 @@ export const assignQuantifiers = async (
const periodDetailsDto = await findPeriodDetailsDto(periodId);
res.status(StatusCodes.OK).json(periodDetailsDto);
};

export const replaceQuantifier = async (
req: Request,
res: TypedResponse<PeriodReplaceQuantifierDto>
): Promise<void> => {
const { periodId } = req.params;
const { currentQuantifierId, newQuantifierId } = req.body;
const period = await PeriodModel.findById(periodId);
if (!period) throw new NotFoundError('Period');
if (period.status !== 'QUANTIFY')
throw new BadRequestError(
'Quantifiers can only be replaced on periods with status QUANTIFY.'
);

if (!currentQuantifierId || !newQuantifierId)
throw new BadRequestError(
'Both originalQuantifierId and newQuantifierId must be specified'
);

if (currentQuantifierId === newQuantifierId)
throw new BadRequestError('Cannot replace a quantifier with themselves');

const currentQuantifier = await UserModel.findById(currentQuantifierId);
if (!currentQuantifier)
throw new BadRequestError('Current quantifier does not exist');

const newQuantifier = await UserModel.findById(newQuantifierId);
if (!newQuantifier)
throw new BadRequestError('Replacement quantifier does not exist');

if (!newQuantifier.roles.includes(UserRole.QUANTIFIER))
throw new BadRequestError(
'Replacement quantifier does not have role QUANTIFIER'
);

const dateRangeQuery = await getPeriodDateRangeQuery(period);
const affectedPraiseIds = await PraiseModel.find({
// Praise within time period
createdAt: dateRangeQuery,

// Original quantifier
'quantifications.quantifier': currentQuantifierId,
}).distinct('_id');

await PraiseModel.updateMany(
{
// Praise within time period
createdAt: dateRangeQuery,

// Original quantifier
'quantifications.quantifier': currentQuantifierId,
},
{
$set: {
// Reset score
'quantifications.$[elem].score': 0,
'quantifications.$[elem].dismissed': false,

// Assign new quantifier
'quantifications.$[elem].quantifier': newQuantifierId,
},
$unset: {
'quantifications.$[elem].duplicatePraise': 1,
},
},
{
arrayFilters: [
{
'elem.quantifier': currentQuantifierId,
},
],
}
);

await logEvent(
EventLogTypeKey.PERIOD,
`Reassigned all praise in period "${
period.name
}" that is currently assigned to user with id "${
currentQuantifierId as string
}", to user with id "${newQuantifierId as string}"`,
{
userId: res.locals.currentUser._id,
}
);

const updatedPraises = await PraiseModel.find({
_id: { $in: affectedPraiseIds },
}).populate('giver receiver forwarder');

const affectedPraises = await praiseDocumentListTransformer(updatedPraises);
const periodDetailsDto = await findPeriodDetailsDto(periodId);

res.status(StatusCodes.OK).json({
period: periodDetailsDto,
praises: affectedPraises,
});
};
12 changes: 0 additions & 12 deletions packages/api/src/period/controllers/index.ts

This file was deleted.

33 changes: 16 additions & 17 deletions packages/api/src/period/routes.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
import { Router } from '@awaitjs/express';
import * as periodController from '@period/controllers';
import * as core from '@period/controllers/core';
import * as assignment from '@period/controllers/assignment';

// Period-routes
const periodRouter = Router();

periodRouter.getAsync('/all', periodController.all);
periodRouter.getAsync('/:periodId', periodController.single);
periodRouter.getAsync(
'/:periodId/receiverPraise',
periodController.receiverPraise
);
periodRouter.getAsync(
'/:periodId/quantifierPraise',
periodController.quantifierPraise
);
periodRouter.getAsync('/all', core.all);
periodRouter.getAsync('/:periodId', core.single);
periodRouter.getAsync('/:periodId/receiverPraise', core.receiverPraise);
periodRouter.getAsync('/:periodId/quantifierPraise', core.quantifierPraise);

// ADMIN Period-routes
const adminPeriodRouter = Router();
adminPeriodRouter.postAsync('/create', periodController.create);
adminPeriodRouter.patchAsync('/:periodId/update', periodController.update);
adminPeriodRouter.patchAsync('/:periodId/close', periodController.close);
adminPeriodRouter.postAsync('/create', core.create);
adminPeriodRouter.patchAsync('/:periodId/update', core.update);
adminPeriodRouter.patchAsync('/:periodId/close', core.close);
adminPeriodRouter.getAsync(
'/:periodId/verifyQuantifierPoolSize',
periodController.verifyQuantifierPoolSize
assignment.verifyQuantifierPoolSize
);
adminPeriodRouter.patchAsync(
'/:periodId/assignQuantifiers',
periodController.assignQuantifiers
assignment.assignQuantifiers
);
adminPeriodRouter.patchAsync(
'/:periodId/replaceQuantifier',
assignment.replaceQuantifier
);
adminPeriodRouter.getAsync('/:periodId/export', periodController.exportPraise);
adminPeriodRouter.getAsync('/:periodId/export', core.exportPraise);

export { periodRouter, adminPeriodRouter };
6 changes: 6 additions & 0 deletions packages/api/src/period/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
QuantificationDocument,
QuantificationDto,
Quantification,
PraiseDto,
} from '@praise/types';
import { Query } from '@shared/types';
import { UserAccountDocument, UserAccountDto } from '@useraccount/types';
Expand Down Expand Up @@ -103,3 +104,8 @@ export interface PeriodDateRange {
$gt: Date;
$lte: Date;
}

export interface PeriodReplaceQuantifierDto {
period: PeriodDetailsDto;
praises: PraiseDto[];
}
104 changes: 0 additions & 104 deletions packages/api/src/scripts/replace-quantifier.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCopy } from '@fortawesome/free-solid-svg-icons';

interface Props {
disabled?: boolean;
icon: IconProp;
text: string;
onClick();
}

const MarkDuplicateButton = ({
const IconButton = ({
Copy link
Member

Choose a reason for hiding this comment

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

Nice!

disabled = false,
icon,
text,
onClick,
}: Props): JSX.Element => {
return (
Expand All @@ -20,10 +24,10 @@ const MarkDuplicateButton = ({
}
onClick={onClick}
>
<FontAwesomeIcon icon={faCopy} size="1x" />
<span>Mark as duplicates</span>
<FontAwesomeIcon icon={icon} size="1x" />
<span>{text}</span>
</button>
);
};

export default MarkDuplicateButton;
export default IconButton;
Loading