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 all 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ 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

- Additional test coverage for api

### Fixed

- Seeder generates periods and praise in the past, not future
- Remove check that was throwing an error when not all quantifiers were assigned praise and `PRAISE_QUANTIFIERS_ASSIGN_ALL` was enabled #492 #498

## [0.9.0] - 2022-06-20

Expand All @@ -36,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Switching eth wallet should result in logging out the user #469
- Logging out of MetaMask causes EthAccount in navigation to disappear #470
- Changed lodash imports to single method import #486
- Prevent error when not all quantifers are assigned praise and "Assign praise evenly" setting enabled

## [0.8.0] - 2022-06-06

Expand Down
3 changes: 1 addition & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"test": "NODE_ENV=testing yarn run load-env nyc --exclude '**/*.spec.ts' 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 @@ -85,4 +84,4 @@
"typescript": "^4.5.4",
"yargs": "^17.3.1"
}
}
}
41 changes: 41 additions & 0 deletions packages/api/src/database/migrations/16_settings_assign_evenly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { SettingGroup } from '@settings/types';
import { SettingsModel } from '@settings/entities';
import { PeriodSettingsModel } from '@periodsettings/entities';

const oldSetting = {
key: 'PRAISE_QUANTIFIERS_ASSIGN_ALL',
value: false,
type: 'Boolean',
label: 'Assign praise to all quantifiers',
description:
'Evenly assign praise among all quantifiers. If enabled, the setting "Quantifiers Per Praise" will be ignored',
group: SettingGroup.PERIOD_DEFAULT,
};

const newSetting = {
key: 'PRAISE_QUANTIFIERS_ASSIGN_EVENLY',
value: false,
type: 'Boolean',
label: 'Assign praise evenly among quantifiers',
description:
'Assign praise roughly evenly among all quantifiers (or as many as possible). If enabled, the setting "Quantifiers Per Praise" will be ignored',
group: SettingGroup.PERIOD_DEFAULT,
};

const up = async (): Promise<void> => {
await SettingsModel.updateMany({ key: oldSetting.key }, { $set: newSetting });
await PeriodSettingsModel.updateMany(
{ key: oldSetting.key },
{ $set: newSetting }
);
};

const down = async (): Promise<void> => {
await SettingsModel.updateMany({ key: newSetting.key }, { $set: oldSetting });
await PeriodSettingsModel.updateMany(
{ key: newSetting.key },
{ $set: oldSetting }
);
};

export { up, down };
157 changes: 130 additions & 27 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 @@ -321,7 +323,7 @@ const prepareAssignmentsByTargetPraiseCount = async (
* @param TOLERANCE
* @returns
*/
const prepareAssignmentsByAllQuantifiers = async (
const prepareAssignmentsByEvenDistribution = async (
period: PeriodDocument,
PRAISE_QUANTIFIERS_PER_PRAISE_RECEIVER: number
): Promise<Assignments> => {
Expand Down Expand Up @@ -385,20 +387,6 @@ const prepareAssignmentsByAllQuantifiers = async (
assignments
);

// Verify that all quantifiers were assigned if necessary
logger.info(
`verify even assigment assignments: ${assignments.poolAssignments.length} q pool: ${quantifierPool.length}`
);
if (assignments.remainingAssignmentsCount === 0) {
logger.info(
'All quantifiers that could be assigned, were assigned praise, as expected with PRAISE_QUANTIFIERS_ASSIGN_ALL'
);
} else {
throw new InternalServerError(
`Not all quantifiers were assigned praise, missing ${assignments.remainingAssignmentsCount}, despite PRAISE_QUANTIFIERS_ASSIGN_EVENLY`
);
}

return assignments;
};

Expand All @@ -408,8 +396,8 @@ const assignQuantifiersDryRun = async (
const period = await PeriodModel.findById(periodId);
if (!period) throw new NotFoundError('Period');

const PRAISE_QUANTIFIERS_ASSIGN_ALL = (await settingValue(
'PRAISE_QUANTIFIERS_ASSIGN_ALL',
const PRAISE_QUANTIFIERS_ASSIGN_EVENLY = (await settingValue(
'PRAISE_QUANTIFIERS_ASSIGN_EVENLY',
period._id
)) as boolean;

Expand All @@ -418,8 +406,8 @@ const assignQuantifiersDryRun = async (
period._id
)) as number;

if (PRAISE_QUANTIFIERS_ASSIGN_ALL) {
return prepareAssignmentsByAllQuantifiers(
if (PRAISE_QUANTIFIERS_ASSIGN_EVENLY) {
return prepareAssignmentsByEvenDistribution(
period,
PRAISE_QUANTIFIERS_PER_PRAISE_RECEIVER
);
Expand Down Expand Up @@ -450,8 +438,8 @@ export const verifyQuantifierPoolSize = async (
const period = await PeriodModel.findById(req.params.periodId);
if (!period) throw new NotFoundError('Period');

const PRAISE_QUANTIFIERS_ASSIGN_ALL = (await settingValue(
'PRAISE_QUANTIFIERS_ASSIGN_ALL',
const PRAISE_QUANTIFIERS_ASSIGN_EVENLY = (await settingValue(
'PRAISE_QUANTIFIERS_ASSIGN_EVENLY',
period._id
)) as boolean;

Expand All @@ -461,7 +449,7 @@ export const verifyQuantifierPoolSize = async (

let response;

if (PRAISE_QUANTIFIERS_ASSIGN_ALL) {
if (PRAISE_QUANTIFIERS_ASSIGN_EVENLY) {
response = {
quantifierPoolSize,
quantifierPoolSizeNeeded: quantifierPoolSize,
Expand Down Expand Up @@ -548,3 +536,118 @@ 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 praiseAlreadyAssignedToNewQuantifier = await PraiseModel.find({
// Praise within time period
createdAt: dateRangeQuery,

// Both original and new quantifiers assigned
$and: [
{ 'quantifications.quantifier': currentQuantifierId },
{ 'quantifications.quantifier': newQuantifierId },
],
});

if (praiseAlreadyAssignedToNewQuantifier?.length > 0)
throw new BadRequestError(
"Replacement quantifier is already assigned to some of the original quantifier's praise"
);

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[];
}
Loading