Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server
1 result
Show changes
Commits on Source (13)
Showing
with 17144 additions and 11748 deletions
......@@ -2,6 +2,22 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.1.2](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v2.1.1...v2.1.2) (2023-02-23)
### Features
* **carto:** add CTM territory filter ([572e214](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/572e2147d81107ccaadea8ea7548b02114aad94a))
### Bug Fixes
* **carto:** search town names without hyphens or diacritics for a better match ([d81fc92](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/d81fc92c7c93e34ad799405aba4f48898fa46e28))
* free workshop under condition ([452e36f](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/452e36f25768672a9a270b1654466bfc53b5e55e))
* mediation display uppercase ([1ff51f6](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/1ff51f6b42e105288ead1f395f0b49028f9350b0))
* **registry:** alphabetical sorting ([163ab8b](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/163ab8baf5b952def1b5c67e8efc84b6278a99ca))
* **registry:** sort jobs by alphabetical order ([bb4f969](https://forge.grandlyon.com/web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/commit/bb4f969764b231a36695ed6af7d08a6db6a4c52d))
### [2.1.1](https://forge.grandlyon.com/web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server/compare/v2.1.0...v2.1.1) (2023-01-26)
......
This diff is collapsed.
{
"name": "ram_server",
"private": true,
"version": "2.1.1",
"version": "2.1.2",
"description": "Nest TypeScript starter repository",
"license": "MIT",
"scripts": {
......@@ -60,6 +60,7 @@
"rimraf": "^3.0.2",
"rxjs": "^7.5.5",
"sanitize-html": "^2.7.3",
"slugify": "^1.6.5",
"swagger-ui-express": "^4.6.0"
},
"devDependencies": {
......
......@@ -141,6 +141,7 @@ module.exports = {
street: 'Rue de la Mairie',
commune: 'Feyzin',
postcode: '69320',
inseeCode: '69291',
},
accountVerified: true,
coord: [4.8563106, 45.6693619],
......@@ -257,7 +258,8 @@ module.exports = {
numero: '27',
street: 'Avenue de la République',
commune: 'Vénissieux',
postcode: '69259',
postcode: '69200',
inseeCode: '69259',
},
accountVerified: true,
coord: [4.8678548, 45.7075853],
......@@ -305,6 +307,7 @@ module.exports = {
street: 'Rue Saint Polycarpe',
commune: 'Lyon 1er',
postcode: '69001',
inseeCode: '69381',
},
hours: {
monday: {
......@@ -465,7 +468,8 @@ module.exports = {
numero: '30 bis',
street: 'Avenue Leclerc',
commune: 'Rillieux-la-Pape',
postcode: '69286',
postcode: '69140',
inseeCode: '69286',
},
coord: [4.9036773, 45.8142196],
accountVerified: true,
......@@ -590,6 +594,7 @@ module.exports = {
street: 'Avenue Franklin Roosevelt',
commune: 'Bron',
postcode: '69500',
inseeCode: '69029',
},
coord: [4.9149121, 45.7311648],
accountVerified: true,
......@@ -693,6 +698,7 @@ module.exports = {
street: 'Rue Louis Normand',
commune: 'Oullins',
postcode: '69600',
inseeCode: '69149',
},
coord: [4.8188378, 45.7157896],
accountVerified: true,
......@@ -803,7 +809,8 @@ module.exports = {
numero: '7',
street: 'Rue Robert et Reynier ',
commune: 'Saint-Fons',
postcode: '69199',
postcode: '69190',
inseeCode: '69199',
},
coord: [4.8608585, 45.7086482],
accountVerified: true,
......@@ -880,6 +887,7 @@ module.exports = {
street: 'Avenue Général Frère',
commune: 'Lyon 8e',
postcode: '69008',
inseeCode: '69388',
},
createdAt: '2020-11-13T14:13:00.000Z',
updatedAt: '2022-04-13T14:13:00.000Z',
......@@ -948,6 +956,7 @@ module.exports = {
street: 'Rue Neuve',
commune: 'Fleurieu-sur-Saône',
postcode: '69250',
inseeCode: '69085',
},
createdAt: '2022-01-13T14:13:00.000Z',
updatedAt: '2022-02-13T14:13:00.000Z',
......
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
axios
.post('http://localhost:3000/api/auth/login', {
email: 'admin@admin.com',
password: process.env.USER_PWD,
})
.then((res) => {
const config = {
headers: { Authorization: `Bearer ${res.data.accessToken}` },
};
axios
.get('http://localhost:3000/api/structures/ctm/update', config)
.then((res) => {
console.log(`CTM territories | statusCode: ${res.status}`);
})
.catch((error) => {
console.error('Error in fetching CTM territories');
console.error(error);
});
})
.catch((error) => {
console.error('Error in auth');
console.error(error);
});
......@@ -3,9 +3,10 @@ import { MongooseModule } from '@nestjs/mongoose';
import { CategoriesService } from './services/categories.service';
import { Categories, CategoriesSchema } from './schemas/categories.schema';
import { CategoriesController } from './controllers/categories.controller';
import { HttpModule } from '@nestjs/axios';
@Module({
imports: [MongooseModule.forFeature([{ name: Categories.name, schema: CategoriesSchema }])],
imports: [MongooseModule.forFeature([{ name: Categories.name, schema: CategoriesSchema }]), HttpModule],
controllers: [CategoriesController],
exports: [CategoriesService],
providers: [CategoriesService],
......
......@@ -2,4 +2,5 @@ export class Module {
id: string;
name: string;
apticIds?: string[];
communes?: string[];
}
import { HttpModule } from '@nestjs/axios';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { CreateCategories } from '../dto/create-categories.dto';
......@@ -16,7 +17,7 @@ describe('CategoriesService', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
imports: [HttpModule],
providers: [
CategoriesService,
{
......
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { lastValueFrom } from 'rxjs';
import slugify from 'slugify';
import { Categories, CategoriesDocument } from '../schemas/categories.schema';
import { Module } from '../schemas/module.class';
@Injectable()
export class CategoriesService {
private readonly logger = new Logger(CategoriesService.name);
constructor(@InjectModel(Categories.name) private categoriesModel: Model<CategoriesDocument>) {}
constructor(
@InjectModel(Categories.name) private categoriesModel: Model<CategoriesDocument>,
private httpService: HttpService
) {}
public async findAll(): Promise<Categories[]> {
this.logger.debug(`findAll`);
......@@ -22,4 +29,40 @@ export class CategoriesService {
this.logger.debug(`findOneComplete`);
return this.categoriesModel.findOne({ id: categoryId }).exec();
}
public async findCtmByInseeCode(inseeCode: string): Promise<Module> {
const ctm = await this.findOneComplete('ctm');
return ctm.modules.find((ctm) => ctm.communes.includes(inseeCode));
}
/**
* Clear 'categories.ctm' and fill it with data from Data Grand Lyon
*/
public async updateCTM(): Promise<Categories> {
this.logger.debug('updateCTM');
const dataCTMs = await lastValueFrom(
this.httpService.get(
`https://download.data.grandlyon.com/ws/grandlyon/ter_territoire.direction_territoriale/all.json?maxfeatures=-1`
)
).then(async (res) => res.data.values);
dataCTMs.sort((a, b) => a.nom.localeCompare(b.nom));
const newDocCTM: Categories = {
modules: [],
name: 'Territoires métropolitains',
theme: 'Territoires métropolitains',
id: 'ctm',
};
for (const ctm of dataCTMs) {
newDocCTM.modules.push({
id: slugify(ctm.nom, { lower: true, strict: true, locale: 'fr' }),
name: ctm.nom,
communes: ctm.communes,
});
}
await this.categoriesModel.updateOne({ id: 'ctm' }, [{ $set: newDocCTM }], { upsert: true });
return newDocCTM;
}
}
......@@ -7,8 +7,8 @@ Veuillez trouver ci-joint la liste des demandes d'aide en ligne du jour :
<table style="border: solid 1px">
<thead>
<tr>
<th style="padding: 8px">Nom</th>
<th style="padding: 8px">Prénom</th>
<th style="padding: 8px">Nom</th>
<th style="padding: 8px">Téléphone</th>
<th style="padding: 8px">Besoin(s)</th>
<th style="padding: 8px">Créneau</th>
......
import { Db } from 'mongodb';
import { getDb } from '../migrations-utils/db';
export const up = async () => {
const db: Db = await getDb();
const cursor = db.collection('structures').find({});
let document;
while ((document = await cursor.next())) {
await db
.collection('structures')
.updateOne({ _id: document._id }, [{ $set: { freeWorkShop: document.freeWorkShop ? 'Oui' : 'Non' } }]);
}
console.log(`Update done : 'freeWorkshop' updated to string`);
};
export const down = async () => {
const db: Db = await getDb();
const cursor = db.collection('structures').find({});
let document;
while ((document = await cursor.next())) {
await db
.collection('structures')
.updateOne({ _id: document._id }, [
{ $set: { freeWorkShop: ['Oui', 'Oui, sous condition'].includes(document.freeWorkShop) ? true : false } },
]);
}
console.log(`Revert done : 'freeWorkshop' updated to boolean`);
};
......@@ -8,7 +8,7 @@ export class OnlineMediationDto {
@IsNotEmpty()
@ApiProperty({ type: String })
readonly surname: string;
surname: string;
@IsNotEmpty()
@ApiProperty({ type: String })
......
......@@ -24,6 +24,7 @@ export class OnlineMediationService {
*/
public async create(newMediation: OnlineMediationDto): Promise<IOnlineMediation> {
try {
newMediation.surname = newMediation.surname.toUpperCase();
return await this.OnlineMediationModel.create(newMediation);
} catch (err) {
this.logger.error(`Creation Error : ${err}`);
......
......@@ -13,6 +13,17 @@ export class Address {
commune: string;
postcode: string;
inseeCode: string;
static equals = function (a: Address, b: Address) {
// ignore inseeCode if not both set
return (
a.numero === b.numero &&
a.street === b.street &&
a.commune === b.commune &&
(a.inseeCode && b.inseeCode ? a.inseeCode === b.inseeCode : true)
);
};
}
export const AddressSchema = SchemaFactory.createForClass(Address);
......@@ -106,7 +106,8 @@ export class Structure {
@Prop()
@IsNotEmpty()
freeWorkShop: boolean;
/** 'Oui' | 'Oui sous condition' | 'Non' */
freeWorkShop: string;
@Prop()
nbComputers: number;
......
......@@ -87,7 +87,7 @@ export class ApticStructuresService {
createdStructure.remoteAccompaniment = false;
createdStructure.categories.accessModality = ['accesLibre'];
createdStructure.accountVerified = true;
createdStructure.freeWorkShop = false;
createdStructure.freeWorkShop = 'Non';
createdStructure.nbComputers = 0;
createdStructure.nbPrinters = 0;
createdStructure.nbScanners = 0;
......
......@@ -159,7 +159,8 @@ const structuresSearchServiceMock = {
numero: '30 bis',
street: 'Avenue Leclerc',
commune: 'Rillieux-la-Pape',
postcode: '69250',
postcode: '69140',
inseeCode: '69286',
},
coord: [4.9036773, 45.8142196],
accountVerified: true,
......@@ -350,7 +351,8 @@ describe('StructuresService', () => {
numero: '30 bis',
street: 'Avenue Leclerc',
commune: 'Rillieux-la-Pape',
postcode: '69250',
postcode: '69140',
inseeCode: '69286',
},
coord: [4.9036773, 45.8142196],
accountVerified: true,
......
......@@ -7,7 +7,7 @@ import * as ejs from 'ejs';
import * as _ from 'lodash';
import { DateTime } from 'luxon';
import { DocumentDefinition, FilterQuery, Model, Types } from 'mongoose';
import { map, Observable, tap } from 'rxjs';
import { lastValueFrom, map, Observable, tap } from 'rxjs';
import { PendingStructureDto } from '../../admin/dto/pending-structure.dto';
import { UnclaimedStructureDto } from '../../admin/dto/unclaimed-structure-dto';
import { Categories } from '../../categories/schemas/categories.schema';
......@@ -24,6 +24,7 @@ import { StructureDto } from '../dto/structure.dto';
import { UpdateStructureDto } from '../dto/update-structure.dto';
import { EquipmentsServicesEnum } from '../enum/equipmentsServices';
import { CNFSStructure } from '../interfaces/cnfs-structure.interface';
import { Address } from '../schemas/address.schema';
import { Structure, StructureDocument } from '../schemas/structure.schema';
import { StructuresSearchService } from './structures-search.service';
......@@ -46,17 +47,6 @@ export class StructuresService {
return this.populateES();
}
//TODO: refactor
public fillFilters(filters: Array<any>): Promise<any[]>[] {
return filters?.map(async (elem) => {
const key = Object.keys(elem)[0];
const modules = (await this.categoriesService.findOneComplete(elem[key])).modules;
return modules.map((module) => {
return { [elem[key]]: module.id };
});
});
}
async searchForStructures(
text: string,
filters?: Array<any>,
......@@ -69,47 +59,41 @@ export class StructuresService {
const results = await this.structuresSearchService.search(text, fields);
const ids = results.map((result) => result.structureId);
let structures;
let multipleFilters = filters ? filters.filter((elem) => Object.keys(elem)[0]?.length == 0) : null;
filters = filters?.filter((elem) => {
return Object.keys(elem)[0]?.length != 0;
});
const andFilters = filters ? filters[0] : null;
const orFilters = filters ? filters[1] : null;
//TODO: remove useless code ?
if (multipleFilters) {
const filtersArrays = await Promise.all(this.fillFilters(multipleFilters));
multipleFilters = [...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 && multipleFilters?.length == 0) {
if (andFilters?.length > 0 && orFilters?.length == 0) {
structures = await this.structureModel
.find({
_id: { $in: ids },
$and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }],
$and: [...this.parseFilter(andFilters), { deletedAt: { $exists: false }, accountVerified: true }],
})
.populate('personalOffers')
.populate('structureType')
.limit(limit)
.exec();
} else if (filters?.length > 0 && multipleFilters?.length > 0) {
} else if (andFilters?.length > 0 && orFilters?.length > 0) {
structures = await this.structureModel
.find({
_id: { $in: ids },
$or: [...this.parseFilter(multipleFilters)],
$and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }],
$or: [...this.parseFilter(orFilters)],
$and: [...this.parseFilter(andFilters), { deletedAt: { $exists: false }, accountVerified: true }],
})
.populate('personalOffers')
.populate('structureType')
.limit(limit)
.exec();
} else if (filters?.length == 0 && multipleFilters?.length > 0) {
} else if (andFilters?.length == 0 && orFilters?.length > 0) {
structures = await this.structureModel
.find({
_id: { $in: ids },
$or: [...this.parseFilter(multipleFilters), { deletedAt: { $exists: false }, accountVerified: true }],
$or: [...this.parseFilter(orFilters)],
$and: [{ deletedAt: { $exists: false }, accountVerified: true }],
})
.populate('personalOffers')
.populate('structureType')
......@@ -140,19 +124,13 @@ export class StructuresService {
structure.accountVerified = true;
const createdStructure = new this.structureModel(structure);
createdStructure._id = new Types.ObjectId();
createdStructure.structureName = createdStructure.structureName.trim();
createdStructure.categories.selfServiceMaterial = this.getSelfServiceMaterial(createdStructure);
await createdStructure.save();
await this.getStructurePosition(createdStructure).then(async (position: StructureDocument) => {
await this.getStructurePosition(createdStructure).then(async (structureWithPosition: StructureDocument) => {
return this.structuresSearchService.indexStructure(
await this.structureModel
.findByIdAndUpdate(
new Types.ObjectId(createdStructure._id),
{
address: position.address,
coord: position.coord,
},
{ new: true }
)
.findByIdAndUpdate(new Types.ObjectId(createdStructure._id), structureWithPosition, { new: true })
.populate('personalOffers')
.populate('structureType')
.exec()
......@@ -334,8 +312,15 @@ export class StructuresService {
public async update(idStructure: string, updatedFields: UpdateStructureDto): Promise<Structure> {
this.logger.debug(`Updating structure ${idStructure}`);
const oldStructure = await this.findOne(idStructure);
const oldCategories = { ...oldStructure.categories };
// Update position only if address has changed (to not erase coord which may have been set manually in database)
if (updatedFields.address && !Address.equals(updatedFields.address, oldStructure.address)) {
oldStructure.address = updatedFields.address;
await this.getStructurePosition(oldStructure);
}
// Store updated categories because it will be erased by Object.assign which is a shallow copy
const oldCategories = { ...oldStructure.categories };
const updatedCategories = { ...updatedFields.categories };
const deepClone: StructureDocument = Object.assign(oldStructure, updatedFields);
// Update structure categories in order to not override it
......@@ -346,8 +331,8 @@ export class StructuresService {
});
}
if (updatedFields.address) {
await this.getStructurePosition(deepClone);
if (updatedFields.structureName) {
deepClone.structureName = deepClone.structureName.trim();
}
const parsedStructure = deepClone;
parsedStructure.categories.selfServiceMaterial = this.getSelfServiceMaterial(deepClone);
......@@ -379,65 +364,101 @@ export class StructuresService {
/**
* Get structures positions and add marker corresponding to those positons on the map
*/
private getStructurePosition(structure: Structure): Promise<Structure> {
private async getStructurePosition(structure: Structure): Promise<Structure> {
this.logger.debug('getStructurePosition');
return new Promise((resolve, reject) => {
this.getCoord(structure.address.numero, structure.address.street, structure.address.commune, 'photon').subscribe(
(res) => {
this.getCoord(
structure.address.numero,
structure.address.street,
structure.address.commune,
'photon-bal'
).subscribe((resbal) => {
// check if photon-bal is more precise than photon ban
if (resbal.data.features.length > 0) {
resbal.data.features = resbal.data.features.sort((_a, b) => {
return b.properties.housenumber ? 1 : -1;
});
const address = resbal.data.features[0];
if (address && address.geometry) {
structure.coord = address.geometry.coordinates;
structure.address.postcode = address.properties.postcode;
} else {
this.logger.error(
`No coord found for: ${structure.address.numero} ${structure.address.street} ${structure.address.commune}`,
StructuresService.name
);
structure.coord = [];
}
resolve(structure);
// else pick structure from photonban if it exists or throw error
} else {
if (res.data.features.length > 0) {
const address = res.data.features[0];
if (address && address.geometry) {
structure.coord = address.geometry.coordinates;
structure.address.postcode = address.properties.postcode;
} else {
this.logger.error(
`No coord found for: ${structure.address.numero} ${structure.address.street} ${structure.address.commune}`,
StructuresService.name
);
structure.coord = [];
}
resolve(structure);
} else {
this.logger.error(
`No structure found for: ${structure.address.numero} ${structure.address.street} ${structure.address.commune}`,
StructuresService.name
);
}
}
});
},
(err) => {
this.logger.error(`Request error: ${err.config.url}`, 'StructureService');
this.logger.error(err);
reject(err);
}
try {
let address: {
geometry: {
coordinates: [number, number];
};
properties: {
[key: string]: string;
};
};
// check if photon-bal is more precise than photon ban
const photonBalCoord = await this.getCoord(
structure.address.numero || '',
structure.address.street,
structure.address.commune,
'photon-bal'
);
});
if (photonBalCoord.data.features.length > 0) {
address = photonBalCoord.data.features.find((feature) => feature.properties.hasOwnProperty('housenumber'));
if (!address) {
address = photonBalCoord.data.features[0];
}
} else {
// else pick structure from photonban if it exists or log error
const photonCoord = await this.getCoord(
structure.address.numero || '',
structure.address.street,
structure.address.commune,
'photon'
);
address = photonCoord.data.features.find((feature) => feature.properties.hasOwnProperty('housenumber'));
}
if (!address || !address.geometry) {
this.logger.error(
`No coord found for: ${structure.address.numero || ''} ${structure.address.street} ${
structure.address.commune
}`,
StructuresService.name
);
structure.coord = [];
} else {
structure.coord = address.geometry.coordinates;
structure.address.postcode = address.properties.postcode;
}
// set territory for CTM filter
await this.setCtmTerritory(structure);
} catch (err) {
this.logger.error(err);
}
return structure;
}
/**
* Get structure territory
*/
private async setCtmTerritory(structure: Structure): Promise<Structure> {
structure.categories['ctm'] = [];
if (structure.address?.postcode) {
let inseeCode: string = null;
const req = `https://apicarto.ign.fr/api/codes-postaux/communes/${structure.address.postcode}`;
this.logger.debug(`Request : ${req}`);
const res = await lastValueFrom(this.httpService.get(encodeURI(req)));
if (res.data?.length) {
if (res.data.length == 1) {
this.logger.debug(`Insee code ${res.data[0].codeCommune} for ${structure.address.commune}`);
inseeCode = res.data[0].codeCommune;
} else {
// if several commune for this postcode, find which commune we are looking for the insee code
const communes = res.data.filter((commune) => commune.nomCommune == structure.address.commune);
if (communes.length) {
this.logger.debug(`Insee code ${communes[0].codeCommune} for ${structure.address.commune}`);
inseeCode = res.data[0].codeCommune;
} else {
this.logger.warn(
`Commune not found for ${structure.address.commune} in ${JSON.stringify(res.data)} (${req})`
);
}
}
}
if (inseeCode) {
structure.address.inseeCode = inseeCode;
const ctm: Module = await this.categoriesService.findCtmByInseeCode(inseeCode);
if (ctm) {
this.logger.debug(`${structure.structureName} : CTM ${ctm.id}`);
structure.categories['ctm'] = [ctm.id];
}
}
}
return structure;
}
public async isClaimed(structureId: string, user: User): Promise<boolean> {
......@@ -564,12 +585,12 @@ export class StructuresService {
);
}
public getCoord(
public async getCoord(
numero: string,
address: string,
commune: string,
scope: string
): Observable<AxiosResponse<PhotonResponse>> {
): Promise<AxiosResponse<PhotonResponse>> {
const req =
`https://download.data.grandlyon.com/geocoding/${scope}/api?q=` +
(numero == null ? '' : numero + ' ') +
......@@ -578,7 +599,7 @@ export class StructuresService {
commune;
this.logger.debug('Print getCoord ' + req);
return this.httpService.get(encodeURI(req));
return await lastValueFrom(this.httpService.get(encodeURI(req)));
}
/**
......@@ -1112,4 +1133,18 @@ export class StructuresService {
.populate('personalOffers')
.exec();
}
/**
* Set structure CTM Territory with data from Data Grand Lyon
*/
public async setCTMs(): Promise<void> {
this.logger.debug('setCTMs');
const structures: StructureDocument[] = await this.findAll();
for (const structure of structures) {
this.setCtmTerritory(structure).then(async (updatedStructure) => {
await this.structureModel.findByIdAndUpdate(new Types.ObjectId(structure._id), updatedStructure).exec();
});
}
return null;
}
}
......@@ -15,7 +15,7 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Types } from 'mongoose';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CategoriesService } from '../categories/services/categories.service';
......@@ -37,6 +37,7 @@ import { QueryStructure } from './dto/query-structure.dto';
import { UpdateStructureDto } from './dto/update-structure.dto';
import { Structure, StructureDocument } from './schemas/structure.schema';
import { StructuresService } from './services/structures.service';
import * as _ from 'lodash';
@ApiTags('structures')
@Controller('structures')
......@@ -66,8 +67,15 @@ export class StructuresController {
.then((data) =>
data.filter(
(cityPoint) =>
/* check if query sting = postcode
OR query string = city (without hyphens and diacritics)
OR query string = name (in 2 cases, Charly et Rochetaillée-sur-saône, the city name is in the name field and the city field is missing) */
(cityPoint.properties.postcode == city ||
cityPoint.properties.city?.toLowerCase().includes(city.toLowerCase())) &&
_.deburr(cityPoint.properties.city?.toLowerCase().replace(/\-/g, ' ')).includes(
_.deburr(city).toLowerCase().replace(/\-/g, ' ')
) ||
_.deburr(cityPoint.properties.name?.toLowerCase().replace(/\-/g, ' ')) ===
_.deburr(city).toLowerCase().replace(/\-/g, ' ')) &&
cityPoint.properties.postcode.match(depRegex)
)
)
......@@ -169,6 +177,22 @@ export class StructuresController {
return this.tempUserService.getStructureTempUsers(id);
}
@ApiOperation({
description: `Mettre à jour les territoires CTM des structures à partir de Data Grand Lyon`,
})
@ApiResponse({
status: 204,
description: 'The CTM territories have been updated successfully.',
})
@Get('/ctm/update')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
public async updateCTM(): Promise<void> {
this.logger.debug('updateCTM');
await this.categoriesService.updateCTM();
return this.structureService.setCTMs();
}
@Delete(':id')
@UseGuards(JwtAuthGuard, IsStructureOwnerGuard)
@ApiParam({ name: 'id', type: String, required: true })
......
......@@ -19,7 +19,10 @@ describe('JobsService', () => {
const mockJobModel = {
create: jest.fn(),
find: jest.fn(),
find: jest.fn().mockReturnThis(),
collation: jest.fn().mockReturnThis(),
sort: jest.fn().mockReturnThis(),
exec: jest.fn().mockReturnThis(),
findOne: jest.fn(),
findById: jest.fn(),
deleteOne: jest.fn(),
......@@ -66,26 +69,28 @@ describe('JobsService', () => {
describe('findAll', () => {
it('should findAll validated jobs', async () => {
mockJobModel.find.mockResolvedValue([
const result = [
{
_id: new Types.ObjectId('6231aefe76598527c8d0b5bc'),
name: 'CNFS',
validated: true,
hasPersonalOffer: true,
},
]);
];
mockJobModel.exec.mockResolvedValueOnce(result);
const reply = await service.findAll();
expect(reply.length).toBe(1);
});
it('should findAll all jobs', async () => {
mockJobModel.find.mockResolvedValue([
const result = [
{
_id: new Types.ObjectId('6231aefe76598527c8d0b5bc'),
name: 'CNFSssss',
validated: false,
hasPersonalOffer: true,
},
]);
];
mockJobModel.exec.mockResolvedValueOnce(result);
const reply = await service.findAll(false);
expect(reply.length).toBe(1);
});
......