Skip to content

Commit

Permalink
Merge pull request #549 from commons-stack/enh/custom_export_formats
Browse files Browse the repository at this point in the history
Custom export format
  • Loading branch information
kristoferlund authored Sep 23, 2022
2 parents 78f1f3b + 4574a9b commit 6d120e7
Show file tree
Hide file tree
Showing 39 changed files with 1,364 additions and 353 deletions.
3 changes: 3 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["ms-vsliveshare.vsliveshare"]
}
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@faker-js/faker": "^7.2.0",
"@types/express-fileupload": "^1.2.2",
"@types/mime-types": "^2.1.1",
"axios": "^0.27.2",
"bin-packer": "^1.6.1",
"cors": "^2.8.5",
"date-fns": "^2.28.0",
Expand All @@ -53,6 +54,7 @@
"mongoose": "^6.1.2",
"mongoose-paginate-ts": "^1.2.0",
"morgan": "^1.10.0",
"safe-eval": "^0.4.1",
"umzug": "^3.0.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { SettingGroup } from '@/settings/types';
import { SettingsModel } from '../../settings/entities';

const settings = [
{
key: 'CUSTOM_EXPORT_MAP',
value:
'https://raw.githubusercontent.com/commons-stack/praise-exports/main/aragon-fixed-budget.json',
type: 'String',
label: 'Transformation map',
description:
'The transformation map describes the transform to be performed. See documentation for details on how to create a transformation map.',
group: SettingGroup.CUSTOM_EXPORT,
},
{
key: 'CUSTOM_EXPORT_CONTEXT',
value: '{ "budget": 100, "token": "TOKEN_NAME" } ',
type: 'JSON',
label: 'Export context',
description:
'Default values for the export context used by the transformation map.',
group: SettingGroup.CUSTOM_EXPORT,
},
{
key: 'CUSTOM_EXPORT_FORMAT',
value: 'csv',
type: 'Radio',
label: 'Export format',
description: '',
options: '["csv", "json"]',
group: SettingGroup.CUSTOM_EXPORT,
},
{
key: 'CS_SUPPORT_PERCENTAGE',
value: 2,
type: 'Integer',
label: 'Praise development support percentage',
description:
'Support the development of Praise, consider donating a percentage of the distribution to the development team.',
group: SettingGroup.CUSTOM_EXPORT,
},
];

const up = async (): Promise<void> => {
const settingUpdates = settings.map((s) => ({
updateOne: {
filter: { key: s.key },

// Insert setting if not found, otherwise continue
update: { $setOnInsert: { ...s } },
upsert: true,
},
}));

await SettingsModel.bulkWrite(settingUpdates);
};

const down = async (): Promise<void> => {
const allKeys = settings.map((s) => s.key);
await SettingsModel.deleteMany({ key: { $in: allKeys } });
};

export { up, down };
173 changes: 2 additions & 171 deletions packages/api/src/period/controllers/core.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import { Types } from 'mongoose';
import { StatusCodes } from 'http-status-codes';
import { Request, Response } from 'express';
import { Parser } from 'json2csv';
import { Request } from 'express';
import { add, compareAsc, parseISO } from 'date-fns';
import { BadRequestError, NotFoundError } from '@/error/errors';
import { PraiseDtoExtended, PraiseDetailsDto, PraiseDto } from '@/praise/types';
import { PraiseDetailsDto, PraiseDto } from '@/praise/types';
import {
praiseListTransformer,
praiseTransformer,
} from '@/praise/transformers';
import { calculateQuantificationScore } from '@/praise/utils/score';
import { UserModel } from '@/user/entities';
import { UserAccountModel } from '@/useraccount/entities';
import { insertNewPeriodSettings } from '@/periodsettings/utils';
import { settingValue } from '@/shared/settings';
import {
TypedRequestBody,
TypedResponse,
QueryInput,
PaginatedResponseBody,
QueryInputParsedQs,
TypedRequestQuery,
Expand Down Expand Up @@ -338,166 +332,3 @@ export const quantifierPraise = async (
const response = await praiseListTransformer(praiseList);
res.status(StatusCodes.OK).json(response);
};

/**
* Generate a CSV of Praise and quantification data for a period
*
* @param {TypedRequestBody<QueryInput>} req
* @param {Response} res
* @returns {Promise<void>}
*/
export const exportPraise = async (
req: TypedRequestBody<QueryInput>,
res: Response
): Promise<void> => {
const period = await PeriodModel.findOne({ _id: req.params.periodId });
if (!period) throw new NotFoundError('Period');
const periodDateRangeQuery = await getPeriodDateRangeQuery(period);

const praises = await PraiseModel.find({
createdAt: periodDateRangeQuery,
}).populate('giver receiver forwarder');

const quantificationsColumnsCount = (await settingValue(
'PRAISE_QUANTIFIERS_PER_PRAISE_RECEIVER',
period._id
)) as number;

const docs: PraiseDetailsDto[] = [];
if (praises) {
for (const praise of praises) {
const pws: PraiseDtoExtended = await praiseTransformer(praise);

const receiver = await UserModel.findById(pws.receiver.user);
if (receiver) {
pws.receiverUserDocument = receiver;
}

if (pws.giver && pws.giver.user) {
const giver = await UserModel.findById(pws.giver.user);
if (giver) {
pws.giverUserDocument = giver;
}
}

pws.quantifications = await Promise.all(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pws.quantifications.map(async (q: any) => {
const quantifier = await UserModel.findById(q.quantifier._id);
const account = await UserAccountModel.findOne({
user: q.quantifier._id,
});

q.quantifier = quantifier;
q.account = account;

q.scoreRealized = await calculateQuantificationScore(q);

return q;
})
);

docs.push(pws);
}
}

const fields = [
{
label: 'ID',
value: '_id',
},
{
label: 'DATE',
value: 'createdAt',
},
{
label: 'TO USER ACCOUNT',
value: 'receiver.name',
},
{
label: 'TO USER ACCOUNT ID',
value: 'receiver._id',
},
{
label: 'TO ETH ADDRESS',
value: 'receiverUserDocument.ethereumAddress',
},
{
label: 'FROM USER ACCOUNT',
value: 'giver.name',
},
{
label: 'FROM USER ACCOUNT ID',
value: 'giver._id',
},
{
label: 'FROM ETH ADDRESS',
value: 'giverUserDocument.ethereumAddress',
},
{
label: 'REASON',
value: 'reasonRealized',
},
{
label: 'SOURCE ID',
value: 'sourceId',
},
{
label: 'SOURCE NAME',
value: 'sourceName',
},
];

for (let index = 0; index < quantificationsColumnsCount; index++) {
const quantObj = {
label: `SCORE ${index + 1}`,
value: `quantifications[${index}].score`,
};

fields.push(quantObj);
}

for (let index = 0; index < quantificationsColumnsCount; index++) {
const quantObj = {
label: `DUPLICATE ID ${index + 1}`,
value: `quantifications[${index}].duplicatePraise`,
};

fields.push(quantObj);
}

for (let index = 0; index < quantificationsColumnsCount; index++) {
const quantObj = {
label: `DISMISSED ${index + 1}`,
value: `quantifications[${index}].dismissed`,
};

fields.push(quantObj);
}

for (let index = 0; index < quantificationsColumnsCount; index++) {
const quantUserUsernameObj = {
label: `QUANTIFIER ${index + 1} USERNAME`,
value: `quantifications[${index}].account.name`,
};

fields.push(quantUserUsernameObj);

const quantUserEthAddressObj = {
label: `QUANTIFIER ${index + 1} ETH ADDRESS`,
value: `quantifications[${index}].quantifier.ethereumAddress`,
};

fields.push(quantUserEthAddressObj);
}

fields.push({
label: 'AVG SCORE',
value: 'scoreRealized',
});

const json2csv = new Parser({ fields: fields });
const csv = json2csv.parse(docs);

res.status(200).contentType('text/csv').attachment('data.csv').send(csv);
};
Loading

0 comments on commit 6d120e7

Please sign in to comment.