Commit 63b6464e authored by Augustin LECONTE's avatar Augustin LECONTE
Browse files

feat(orientation-form): structure search apply global filters

parent 8b1c94c2
......@@ -19,4 +19,7 @@ export class CategoriesFormationsService {
public findOne(categoryId: string): Promise<CategoriesFormations> {
return this.structureModel.findOne({ id: categoryId }).select({ 'modules.id': 1 }).exec();
}
public findOneComplete(categoryId: string): Promise<CategoriesFormations> {
return this.structureModel.findOne({ id: categoryId }).exec();
}
}
import { Db } from 'mongodb';
import { getDb } from '../migrations-utils/db';
export const up = async () => {
const db: Db = await getDb();
const cursor = db.collection('categoriesformations').find({});
let document;
while ((document = await cursor.next())) {
if (document.id == 'baseSkills') {
const newDoc = {
surname: 'ordinateur, smartphone, internet',
name: 'Compétences de base',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
} else if (document.id == 'accessRight') {
const newDoc = {
surname: 'Pôle emploi, CAF',
name: 'Accès aux droits',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
} else if (document.id == 'parentingHelp') {
const newDoc = {
surname: 'temps d’écran, scolarité',
name: 'Aide à la parentalité',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
} else if (document.id == 'socialAndProfessional') {
const newDoc = {
surname: 'CV, tableur',
name: 'Insertion professionnelle',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
} else if (document.id == 'digitalCultureSecurity') {
const newDoc = {
surname: 'réseaux sociaux, visio',
name: 'Culture numérique',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
}
}
console.log(`Update done`);
};
export const down = async () => {
const db: Db = await getDb();
const cursor = db.collection('categoriesformations').find({});
let document;
while ((document = await cursor.next())) {
if (document.id == 'socialAndProfessional') {
const newDoc = {
surname: null,
name: 'Insertion sociale et professionnelle',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
} else if (document.id == 'baseSkills') {
const newDoc = {
surname: null,
name: 'Les compétences de base',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
} else if (document.id == 'parentingHelp') {
const newDoc = {
surname: null,
name: 'Aide à la parentalité',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
} else if (document.id == 'digitalCultureSecurity') {
const newDoc = {
surname: null,
name: 'Culture et sécurité numérique',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
} else if (document.id == 'accessRight') {
const newDoc = {
surname: null,
name: 'Accès aux droits',
};
await db.collection('categoriesformations').updateOne({ _id: document._id }, [{ $set: newDoc }]);
}
}
console.log(`Update done`);
};
import { HttpModule, HttpStatus } from '@nestjs/common';
import { HttpModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ElasticsearchService } from '@nestjs/elasticsearch';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { CategoriesFormationsServiceMock } from '../../../test/mock/services/categoriesFormations.mock.service';
import { UsersServiceMock } from '../../../test/mock/services/user.mock.service';
import { CategoriesFormationsService } from '../../categories/services/categories-formations.service';
import { ConfigurationService } from '../../configuration/configuration.service';
import { MailerModule } from '../../mailer/mailer.module';
import { MailerService } from '../../mailer/mailer.service';
import { SearchModule } from '../../search/search.module';
import { UsersService } from '../../users/users.service';
import { structureDto } from '../dto/structure.dto';
import { Structure } from '../schemas/structure.schema';
import { Structure, StructureDocument } from '../schemas/structure.schema';
import { StructuresSearchService } from './structures-search.service';
import { StructuresService } from './structures.service';
describe('StructuresService', () => {
......@@ -25,17 +25,210 @@ describe('StructuresService', () => {
find: jest.fn(),
};
const structuresSearchServiceMock = {
search: jest.fn().mockReturnValue([
{
_id: '6903ba0e2ab5775cfc01ed4d',
structureId: '6903ba0e2ab5775cfc01ed4d',
structureType: null,
digitalCultureSecurity: ['2', '5', '9', '28', '34', '39', '42', '51', '52', '54', '65', '96', '97', '98'],
parentingHelp: ['3', '22', '82', '94'],
socialAndProfessional: ['6', '20', '66', '67', '68', '69', '124', '125', '127'],
accessRight: ['84', '85', '86', '87', '88', '89', '93', '95'],
baseSkills: ['260', '1', '11', '38', '48', '74', '77'],
equipmentsAndServices: ['ordinateurs', 'imprimantes'],
proceduresAccompaniment: ['cpam', 'impots', 'carsat', 'poleEmploi'],
publics: ['toutPublic'],
labelsQualifications: ['passNumerique', 'espacePublicNumeriqueepn'],
accessModality: ['accesLibre', 'telephoneVisio', 'surRdv'],
freeWorkShop: false,
createdAt: '2020-11-16T09:30:00.000Z',
updatedAt: '2021-04-12T08:48:00.000Z',
structureName: "L'Atelier Numérique",
description:
"L'Atelier Numérique est l'Espace Public Numérique des Centres Sociaux de Rillieux-la-Pape, ayant pour mission la médiation numérique pour toutes et tous.",
lockdownActivity:
'accesLibres, permanences numériques téléphoniques, cours et ateliers à distance, formations professionnelles.',
contactPhone: '',
contactMail: '',
website: '',
facebook: null,
twitter: null,
instagram: null,
pmrAccess: true,
exceptionalClosures: '',
jaccompagneLesUsagersDansLeursDemarchesEnLigne: true,
publicsAccompaniment: [],
autresAccompagnements: '',
nbComputers: 16,
nbPrinters: 1,
nbTablets: 1,
nbNumericTerminal: 1,
hours: {
monday: {
open: true,
time: [
{
closing: '12:30',
opening: '9:00',
},
{
closing: '17:00',
opening: '13:30',
},
],
},
tuesday: {
open: true,
time: [
{
closing: '12:30',
opening: '9:00',
},
{
closing: '17:00',
opening: '13:30',
},
],
},
wednesday: {
open: true,
time: [
{
closing: '12:30',
opening: '9:00',
},
{
closing: '17:00',
opening: '13:30',
},
],
},
thursday: {
open: true,
time: [
{
closing: '12:30',
opening: '9:00',
},
{
closing: '17:00',
opening: '13:30',
},
],
},
friday: {
open: true,
time: [
{
closing: '12:30',
opening: '9:00',
},
],
},
saturday: {
open: false,
time: [],
},
sunday: {
open: false,
time: [],
},
},
__v: 0,
address: {
numero: '30 bis',
street: 'Avenue Leclerc',
commune: 'Rillieux-la-Pape',
},
coord: [4.9036773, 45.8142196],
accountVerified: true,
linkedin: null,
nbScanners: 1,
otherDescription: null,
},
]),
};
const mockCategoriesFormationsService = {
findOneComplete: jest.fn().mockReturnValue({
_id: '5fbb934180a5c257dc0161f6',
modules: [
{
id: '260',
display_id: '260',
display_name: 'Modules APTIC - n°260',
digest: 'Maitrise de l’environnement d’un ordinateur (clavier, souris)',
text: 'Maitrise de l’environnement d’un ordinateur (clavier, souris)',
},
{
id: '1',
display_id: '1',
display_name: 'Modules APTIC - n°1',
digest: 'Composantes et facettes de l’identité numérique',
text: 'Composantes et facettes de l’identité numérique',
},
{
id: '11',
display_id: '11',
display_name: 'Modules APTIC - n°11',
digest: 'Internet : fonctionnement et outils de navigation web',
text: 'Internet : fonctionnement et outils de navigation web',
},
{
id: '38',
display_id: '38',
display_name: 'Modules APTIC - n°38',
digest: 'Le smartphone : principes de fonctionnement',
text: 'Le smartphone : principes de fonctionnement',
},
{
id: '48',
display_id: '48',
display_name: 'Modules APTIC - n°48',
digest: 'Internet : envoyer, recevoir, gérer ses emails',
text: 'Internet : envoyer, recevoir, gérer ses emails',
},
{
id: '74',
display_id: '74',
display_name: 'Modules APTIC - n°74',
digest: 'Smartphones et Tablettes sous Androïd',
text: 'Smartphones et Tablettes sous Androïd',
},
{
id: '77',
display_id: '77',
display_name: 'Modules APTIC - n°77',
digest: "Smartphone : Les principaux gestes pour l'écran tactile",
text: "Smartphone : Les principaux gestes pour l'écran tactile",
},
],
name: 'Les compétences de base',
id: 'baseSkills',
__v: 0,
}),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [HttpModule, MailerModule, SearchModule, ConfigModule],
providers: [
StructuresService,
ConfigurationService,
StructuresSearchService,
{
provide: StructuresSearchService,
useValue: structuresSearchServiceMock,
},
CategoriesFormationsService,
{
provide: getModelToken(Structure.name),
useValue: mockStructureModel,
},
{
provide: CategoriesFormationsService,
useValue: mockCategoriesFormationsService,
},
{
provide: UsersService,
useClass: UsersServiceMock,
......@@ -56,11 +249,92 @@ describe('StructuresService', () => {
expect(res).toBeTruthy();
});
it('should searchForStructures', () => {
let res = service.searchForStructures('a', [{ nbPrinters: '1' }]);
expect(res).toBeTruthy();
res = service.searchForStructures('a');
expect(res).toBeTruthy();
describe('should searchForStructures', () => {
jest.setTimeout(30000);
mockStructureModel.find.mockReturnThis();
mockStructureModel.exec.mockResolvedValue([
{
_id: '6903ba0e2ab5775cfc01ed4d',
structureId: '6903ba0e2ab5775cfc01ed4d',
structureType: null,
digitalCultureSecurity: ['2', '5', '9', '28', '34', '39', '42', '51', '52', '54', '65', '96', '97', '98'],
parentingHelp: ['3', '22', '82', '94'],
socialAndProfessional: ['6', '20', '66', '67', '68', '69', '124', '125', '127'],
accessRight: ['84', '85', '86', '87', '88', '89', '93', '95'],
baseSkills: ['260', '1', '11', '38', '48', '74', '77'],
equipmentsAndServices: ['ordinateurs', 'imprimantes'],
proceduresAccompaniment: ['cpam', 'impots', 'carsat', 'poleEmploi'],
publics: ['toutPublic'],
labelsQualifications: ['passNumerique', 'espacePublicNumeriqueepn'],
accessModality: ['accesLibre', 'telephoneVisio', 'surRdv'],
freeWorkShop: false,
createdAt: '2020-11-16T09:30:00.000Z',
updatedAt: '2021-04-12T08:48:00.000Z',
structureName: "L'Atelier Numérique",
description:
"L'Atelier Numérique est l'Espace Public Numérique des Centres Sociaux de Rillieux-la-Pape, ayant pour mission la médiation numérique pour toutes et tous.",
lockdownActivity:
'accesLibres, permanences numériques téléphoniques, cours et ateliers à distance, formations professionnelles.',
contactPhone: '',
contactMail: '',
website: '',
facebook: null,
twitter: null,
instagram: null,
pmrAccess: true,
exceptionalClosures: '',
jaccompagneLesUsagersDansLeursDemarchesEnLigne: true,
publicsAccompaniment: [],
autresAccompagnements: '',
nbComputers: 16,
nbPrinters: 1,
nbTablets: 1,
nbNumericTerminal: 1,
hours: {
monday: {
open: true,
time: [
{
closing: '12:30',
opening: '9:00',
},
{
closing: '17:00',
opening: '13:30',
},
],
},
},
__v: 0,
address: {
numero: '30 bis',
street: 'Avenue Leclerc',
commune: 'Rillieux-la-Pape',
},
coord: [4.9036773, 45.8142196],
accountVerified: true,
linkedin: null,
nbScanners: 1,
otherDescription: null,
},
]);
it('should find 1 structure', async () => {
const res = await service.searchForStructures('a', [{ nbPrinters: '1' }]);
expect(res.length).toBe(1);
});
it('should find 1 structure', async () => {
const res = await service.searchForStructures('a', [{ nbPrinters: '1' }, { '': 'baseSkills' }]);
expect(res.length).toBe(1);
});
it('should find 1 structure', async () => {
const res = await service.searchForStructures('a', [{ '': 'baseSkills' }]);
expect(res.length).toBe(1);
});
it('should find 1 structure', async () => {
const res = await service.searchForStructures('a');
expect(res.length).toBe(1);
});
});
it('should create structure', () => {
......
......@@ -22,6 +22,7 @@ import { CategoriesFormations } from '../../categories/schemas/categoriesFormati
import { CategoriesOthers } from '../../categories/schemas/categoriesOthers.schema';
import { UnclaimedStructureDto } from '../../admin/dto/unclaimed-structure-dto';
import { depRegex } from '../common/regex';
import { CategoriesFormationsService } from '../../categories/services/categories-formations.service';
@Injectable()
export class StructuresService {
......@@ -30,6 +31,7 @@ export class StructuresService {
private readonly userService: UsersService,
private readonly mailerService: MailerService,
private structuresSearchService: StructuresSearchService,
private categoriesFormationsService: CategoriesFormationsService,
@InjectModel(Structure.name) private structureModel: Model<StructureDocument>
) {}
......@@ -40,22 +42,57 @@ export class StructuresService {
return this.populateES();
}
public fillFilters(filters: Array<any>): Promise<{}[]>[] {
return filters?.map(async (elem) => {
const key = Object.keys(elem)[0];
const modules = (await this.categoriesFormationsService.findOneComplete(elem[key])).modules;
return modules.map((module) => {
return { [elem[key]]: module.id };
});
});
}
async searchForStructures(text: string, filters?: Array<any>): Promise<StructureDocument[]> {
const results = await this.structuresSearchService.search(text);
const ids = results.map((result) => result.structureId);
let multipleFilters = filters ? filters.filter((elem) => Object.keys(elem)[0].length == 0) : null;
filters = filters?.filter((elem) => Object.keys(elem)[0].length != 0);
if (multipleFilters) {
const filtersArrays = await Promise.all(this.fillFilters(multipleFilters));
multipleFilters = [].concat.apply([], filtersArrays);
}
if (!ids.length) {
return [];
}
//we match ids from Elasticsearch with ids from mongoDB (and filters) and sort the result according to ElasticSearch order.
if (filters.length > 0) {
if (filters?.length > 0 && multipleFilters?.length == 0) {
return (
await this.structureModel
.find({
_id: { $in: ids },
$and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }],
})
.exec()
).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
} else if (filters?.length > 0 && multipleFilters?.length > 0) {
return (
await this.structureModel
.find({
_id: { $in: ids },
$or: [...this.parseFilter(multipleFilters)],
$and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }],
})
.exec()
).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
} else if (filters?.length == 0 && multipleFilters?.length > 0) {
return (
await this.structureModel
.find({
_id: { $in: ids },
$or: [...this.parseFilter(multipleFilters), { deletedAt: { $exists: false }, accountVerified: true }],
})
.exec()
).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
} else {
return (
await this.structureModel
......
......@@ -12,6 +12,9 @@ import { TempUserService } from '../temp-user/temp-user.service';
import { User } from './schemas/user.schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { CategoriesFormationsService } from '../categories/services/categories-formations.service';
import { CategoriesModule } from '../categories/categories.module';
import { CategoriesFormationsServiceMock } from '../../test/mock/services/categoriesFormations.mock.service';
describe('UsersController', () => {
let controller: UsersController;
......@@ -25,6 +28,10 @@ describe('UsersController', () => {
StructuresSearchService,
MailerService,
TempUserService,
{
provide: CategoriesFormationsService,
useValue: CategoriesFormationsServiceMock,
},
{
provide: getModelToken('TempUser'),
useValue: TempUser,
......
......@@ -10,6 +10,7 @@ export class StructuresForSearchServiceMock {
street: 'Avenue Edouard Aynard',
commune: 'Écully',
},
nbPrinters: 1,
description:
'Nous sommes une équipe de 6 personnes accompagnant les usagers dans leur démarche de découverte des outils numériques, mettant à disposition sous forme de prêt des liseuses et du livre numérique et organisant des ateliers individuels de prise en main des outils numériques et tentant de répondre aux questions des usages sur des sujets divers.',
},
......@@ -22,6 +23,7 @@ export class StructuresForSearchServiceMock {
street: " Place de l'Abbe Launay",
commune: 'Grézieu-la-Varenne',
},
nbPrinters: 1,
description: null,
},
{
......@@ -33,6 +35,7 @@ export class StructuresForSearchServiceMock {
street: 'Place de la Mairie',
commune: 'La Tour-de-Salvagny',
},
nbPrinters: 1,
description: null,
},
{
......@@ -44,6 +47,7 @@ export class StructuresForSearchServiceMock {
street: 'Chemin Jean-Marie Vianney',
commune: 'Écully',
},
nbPrinters: 1,
description: null,
},
{
......@@ -56,6 +60,9 @@ export class StructuresForSearchServiceMock {
commune: 'Oullins',
},
description: null,
nbPrinters: 1,
baseSkills: ['260', '1', '11', '38', '48', '74', '77'],
equipmentsAndServices: ['ordinateurs', 'imprimantes'],
},
];
}
......
......@@ -214,6 +214,7 @@ export class StructuresServiceMock {
socialAndProfessional: ['6', '20', '66', '67', '68', '69', '124', '125', '127'],
accessRight: ['84', '85', '86', '87', '88', '89', '93', '95'],
baseSkills: ['260', '1', '11', '38', '48', '74', '77'],
equipmentsAndServices: ['ordinateurs', 'imprimantes'],
proceduresAccompaniment: ['cpam', 'impots', 'carsat', 'poleEmploi'],
publics: ['toutPublic'],
labelsQualifications: ['passNumerique', 'espacePublicNumeriqueepn'],
......@@ -237,7 +238,6 @@ export class StructuresServiceMock {
jaccompagneLesUsagersDansLeursDemarchesEnLigne: true,
publicsAccompaniment: [],
autresAccompagnements: '',