Skip to content

Commit

Permalink
Merge pull request #231 from matchID-project/feat/edits-v1
Browse files Browse the repository at this point in the history
✨ handles advanced edit functions (messages, reviews of proof quality)
  • Loading branch information
rhanka authored Apr 22, 2021
2 parents 153c282 + 51c8d77 commit 7886751
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 78 deletions.
121 changes: 80 additions & 41 deletions backend/src/controllers/search.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import express from 'express';
import { writeFile, access, mkdir, createReadStream } from 'fs';
import { promisify } from 'util';
import { resultsHeader, jsonPath, prettyString } from '../processStream';
import { runRequest } from '../runRequest';
import { runRequest, runBulkRequest } from '../runRequest';
import { buildRequest } from '../buildRequest';
import { RequestInput, RequestBody } from '../models/requestInput';
import { StrAndNumber, UpdateFields } from '../models/entities';
import { buildResult, Result, ErrorResponse } from '../models/result';
import { StrAndNumber, Modification, UpdateRequest, UpdateUserRequest, Review, ReviewsStringified, statusAuthMap } from '../models/entities';
import { buildResult, buildResultSingle, Result, ErrorResponse } from '../models/result';
import { format } from '@fast-csv/format';
import { updatedFields } from '../updatedIds';
import { sendUpdateConfirmation } from '../mail';
Expand Down Expand Up @@ -248,7 +248,7 @@ export class SearchController extends Controller {
@Post('/id/{id}')
public async updateId(
@Path() id: string,
@Body() updateFields: UpdateFields,
@Body() updateRequest: UpdateRequest,
@Request() request: express.Request
): Promise<any> {
// get user & rights from Security
Expand All @@ -260,34 +260,39 @@ export class SearchController extends Controller {
const result = await runRequest(requestBuild, requestInput.scroll);
const builtResult = buildResult(result.data, requestInput)
if (builtResult.response.persons.length > 0) {
let proof
const date = new Date(Date.now()).toISOString()
const bytes = forge.random.getBytesSync(24);
const randomId = forge.util.bytesToHex(bytes);
if (!isAdmin) {
updateFields = {...await request.body};
updateRequest = {...await request.body} as UpdateUserRequest;
let proof;
const message = updateRequest.message;
delete updateRequest.message;
if (request.files && request.files.length > 0) {
const [ file ]: any = request.files
proof = file.path
} else if (updateFields.proof) {
({ proof } = updateFields);
delete updateFields.proof
if (Object.keys(updateFields).length === 0) {
} else if (updateRequest.proof) {
({ proof } = updateRequest);
if (Object.keys(updateRequest).length === 0) {
this.setStatus(400);
return { msg: 'A field at least must be provided' }
}
} else {
this.setStatus(400);
return { msg: 'Proof must be provided' }
}
const correctionData = {
delete updateRequest.proof;
const correctionData: Modification = {
id: randomId,
date,
proof,
auth: 0,
author,
fields: updateFields
fields: updateRequest
};
if (message) {
correctionData.message = message;
}
try {
await accessAsync(`./data/proofs/${id}`);
} catch(err) {
Expand All @@ -302,30 +307,31 @@ export class SearchController extends Controller {
this.setStatus(406);
return { msg: "Id exists but no update to validate" }
} else {
const checkedIds = {...await request.body};
const checkedIds = {...await request.body} as ReviewsStringified;
const checks = Object.keys(checkedIds).length;
let validated = 0;
let rejected = 0;
let noChange = 0;
const count:any = {
rejected: 0,
validated: 0,
closed: 0,
noChange: 0
};
await Promise.all(updatedFields[id].map(async (update: any) => {
if (checkedIds[update.id] === 'true') {
if (update.auth !== 1) {
update.auth = 1;
validated++;
const review: Review = JSON.parse(checkedIds[update.id]);
review.date = date;
if (review.status) {
const auth = statusAuthMap[review.status];
const reviewChange = update.review &&
['proofQuality','proofScript','proofType','silent','message'].some(k => (review as any)[k] !== update.review[k])
if ((update.auth !== auth) || reviewChange) {
update.auth = auth;
count[review.status]++;
update.review = review;
await writeFileAsync(`./data/proofs/${id}/${update.date as string}_${id}.json`, JSON.stringify(update));
await sendUpdateConfirmation(update.author, true, undefined, id);
if (!review.silent) {
await sendUpdateConfirmation(update.author, review.status, review.message, id);
}
} else {
noChange++;
}
delete checkedIds[update.id];
} else if (checkedIds[update.id] !== undefined) {
if (update.auth !== -1) {
update.auth = -1;
rejected++;
await writeFileAsync(`./data/proofs/${id}/${update.date as string}_${id}.json`, JSON.stringify(update));
await sendUpdateConfirmation(update.author, false, checkedIds[update.id] || undefined, id);
} else {
noChange++;
count.noChange++;
}
delete checkedIds[update.id];
}
Expand All @@ -336,17 +342,13 @@ export class SearchController extends Controller {
} else if (Object.keys(checkedIds).length > 0) {
return {
msg: "Partial validation could be achieved",
validated,
rejected,
noChange,
...count,
invalidIds: Object.keys(checkedIds)
}
} else {
return {
msg: "All validations processed",
validated,
rejected,
noChange
...count
}
}
}
Expand All @@ -367,8 +369,45 @@ export class SearchController extends Controller {
@Security('jwt',['user'])
@Tags('Simple')
@Get('/updated')
public updateList(): any {
return updatedFields
public async updateList(@Request() request: express.Request): Promise<any> {
const author = (request as any).user && (request as any).user.user
const isAdmin = (request as any).user && (request as any).user.scopes && (request as any).user.scopes.includes('admin');
let updates:any = {};
if (isAdmin) {
updates = {...updatedFields};
} else {
Object.keys(updatedFields).forEach((id:any) => {
let filter = false;
const modifications = updatedFields[id].map((m:any) => {
const modif:any = {...m}
if (modif.author !== author) {
modif.author = modif.author.substring(0,2)
+ '...' + modif.author.replace(/@.*/,'').substring(modif.author.replace(/@.*/,'').length-2)
+ '@' + modif.author.replace(/.*@/,'');
modif.message = undefined;
modif.review = undefined;
} else {
filter=true
}
return modif;
});
if (filter) {
updates[id] = modifications;
}
})
}
const bulkRequest = Object.keys(updates).map((id: any) =>
[JSON.stringify({index: "deces"}), JSON.stringify(buildRequest(new RequestInput({id})))]
);
const msearchRequest = bulkRequest.map((x: any) => x.join('\n\r')).join('\n\r') + '\n';
const result = await runBulkRequest(msearchRequest);
return result.data.responses.map((r:any) => buildResultSingle(r.hits.hits[0]))
.map((r:any) => {
delete r.score;
delete r.scores;
r.modifications = updates[r.id];
return r;
});
}

private async handleFile(request: express.Request): Promise<any> {
Expand Down Expand Up @@ -441,7 +480,7 @@ export class SearchController extends Controller {
end
});
stream.pipe(request.res);
await new Promise((resolve, reject) => {
await new Promise((resolve) => {
stream.on('end', () => {
request.res.end();
resolve(true);
Expand Down
16 changes: 10 additions & 6 deletions backend/src/mail.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SMTPClient } from 'emailjs';
import { ReviewStatus } from './models/entities';

const client = new SMTPClient({
host: process.env.SMTP_HOST,
Expand Down Expand Up @@ -40,29 +41,32 @@ export const validateOTP = (email:string,otp:string): boolean => {
return false;
}

export const sendUpdateConfirmation = async (email:string, validation: boolean, rejectMsg: string, id: string): Promise<boolean> => {
export const sendUpdateConfirmation = async (email:string, status: ReviewStatus, rejectMsg: string, id: string): Promise<boolean> => {
try {
const message: any = {
from: process.env.API_EMAIL,
to: `${email}`,
}
if (validation) {
if (status === 'validated') {
message.subject = `Suggestion validée ! - ${process.env.APP_DNS}`;
message.attachment = { data: `<html style="font-family:Helvetica;">
<h4> Merci de votre contibution !</h4>
Votre proposition de correction a été acceptée.<br>
Retrouvez <a href="https://${process.env.APP_DNS}/id/${id}"> la fiche modifiée </a>.<br>
<br>
Vous pouvez à tout moment <a href="https://${process.env.APP_DNS}/edits">revenir sur vos contributions</a>.<br>
<br>
l'équipe matchID
</html>
`, alternative: true};
} else {
message.subject = `Suggestion non retenue - ${process.env.APP_DNS}`;
} else if (status === 'rejected') {
message.subject = `Suggestion incomplète - ${process.env.APP_DNS}`;
message.attachment = { data: `<html style="font-family:Helvetica;">
Nous vous remercions de votre contribution,<br>
<br>
Néanmoins les éléments fournis ne nous ont pas permis de retenir votre proposition<br>
${rejectMsg ? rejectMsg : ''}<br>
Néanmoins les éléments fournis ne nous ont pas permis de retenir votre proposition à ce stade.<br>
${rejectMsg ? '<br>' + rejectMsg + '<br>' : ''}<br>
Vous pourrez de nouveau soumettre une nouvelle proposition sur la fiche: <a href="https://${process.env.APP_DNS}/edits#${id}"></a>.<br>
<br>
l'équipe matchID
</html>
Expand Down
104 changes: 73 additions & 31 deletions backend/src/models/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,78 @@ export interface Location {
longitude?: number;
};

/**
* Identity modification
* @tsoaModel
* @example
* {
* "firstName": "Paul"
* }
*/
export interface UpdateFields {
firstName?: string;
lastName?: string;
birthDate?: string;
birthCity?: string;
birthCountry?: string;
birthLocationCode?: string;
deathAge?: number;
deathDate?: string;
deathCity?: string;
deathCountry?: string;
deathLocationCode?: string;
}

export interface UpdateUserRequest extends UpdateFields {
proof: string;
message?: string;
};

export type ReviewStatus = "rejected"|"validated"|"closed";

export const statusAuthMap = {
rejected: -1,
validated: 1,
closed: -2,
};

export type ProofType = "french death certificate"|"french birth certificate"|"other french document"|"foreign document"|"grave"|"other";

export type ProofScript = "manuscript"|"typed"|"numerical";

export type ProofQuality = "poor"|"good";

export interface Review {
status: ReviewStatus;
date?: string;
message?: string;
silent?: boolean;
proofType?: ProofType;
proofScrupt?: ProofScript;
proofQuality?: ProofQuality;
};

export interface Reviews {
[key: string]: Review;
};

export interface ReviewsStringified {
[key: string]: string;
};

export type UpdateRequest = UpdateUserRequest | ReviewsStringified ;

export interface Modification {
id: string;
date: string;
author: string;
fields: UpdateFields;
proof: string;
auth: number;
message?: string;
review?: Review;
};

export interface Person {
score: number;
source: string;
Expand All @@ -83,14 +155,7 @@ export interface Person {
wikidata?: string;
wikimedia?: string;
};
modifications?: {
id: string;
date: string;
auth: number;
proof: string;
author: string;
fields: UpdateFields;
};
modifications?: Modification[];
};

export interface ScoreParams {
Expand All @@ -99,27 +164,4 @@ export interface ScoreParams {
candidateNumber?: number;
};

/**
* Identity modification
* @tsoaModel
* @example
* {
* "firstName": "Paul"
* }
*/
export interface UpdateFields {
firstName?: string;
lastName?: string;
birthDate?: string;
birthCity?: string;
birthCountry?: string;
birthLocationCode?: string;
deathAge?: number;
deathDate?: string;
deathCity?: string;
deathCountry?: string;
deathLocationCode?: string;
proof?: string;
}

export type StrAndNumber = string | number;

0 comments on commit 7886751

Please sign in to comment.