mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-03-17 04:02:30 +00:00
refactor: use range query for canteen module
This commit is contained in:
999
package-lock.json
generated
999
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -57,9 +57,9 @@
|
|||||||
"@ionic/storage": "2.2.0",
|
"@ionic/storage": "2.2.0",
|
||||||
"@ngx-translate/core": "11.0.1",
|
"@ngx-translate/core": "11.0.1",
|
||||||
"@ngx-translate/http-loader": "4.0.0",
|
"@ngx-translate/http-loader": "4.0.0",
|
||||||
"@openstapps/api": "0.26.0",
|
"@openstapps/api": "0.29.0",
|
||||||
"@openstapps/configuration": "0.25.0",
|
"@openstapps/configuration": "0.25.0",
|
||||||
"@openstapps/core": "0.42.0",
|
"@openstapps/core": "0.46.0",
|
||||||
"cordova-android": "9.0.0",
|
"cordova-android": "9.0.0",
|
||||||
"cordova-browser": "6.0.0",
|
"cordova-browser": "6.0.0",
|
||||||
"cordova-ios": "6.2.0",
|
"cordova-ios": "6.2.0",
|
||||||
@@ -74,6 +74,7 @@
|
|||||||
"core-js": "2.6.5",
|
"core-js": "2.6.5",
|
||||||
"deepmerge": "3.3.0",
|
"deepmerge": "3.3.0",
|
||||||
"form-data": "2.5.0",
|
"form-data": "2.5.0",
|
||||||
|
"lodash-es": "4.17.21",
|
||||||
"moment": "2.29.1",
|
"moment": "2.29.1",
|
||||||
"ngx-logger": "4.1.9",
|
"ngx-logger": "4.1.9",
|
||||||
"ngx-markdown": "9.1.1",
|
"ngx-markdown": "9.1.1",
|
||||||
@@ -95,6 +96,7 @@
|
|||||||
"@ionic/angular-toolkit": "2.3.3",
|
"@ionic/angular-toolkit": "2.3.3",
|
||||||
"@types/deepmerge": "2.2.0",
|
"@types/deepmerge": "2.2.0",
|
||||||
"@types/form-data": "2.5.0",
|
"@types/form-data": "2.5.0",
|
||||||
|
"@types/lodash-es": "4.17.4",
|
||||||
"@types/jasmine": "3.3.12",
|
"@types/jasmine": "3.3.12",
|
||||||
"@types/jasminewd2": "2.0.6",
|
"@types/jasminewd2": "2.0.6",
|
||||||
"@types/node": "14.14.37",
|
"@types/node": "14.14.37",
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
*/
|
*/
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {Client} from '@openstapps/api/lib/client';
|
import {Client} from '@openstapps/api/lib/client';
|
||||||
import {SCDish, SCMessage, SCSaveableThing, SCSearchQuery,
|
import {
|
||||||
SCSearchResponse, SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core';
|
SCDish, SCMessage, SCMultiSearchRequest, SCSaveableThing, SCSearchQuery,
|
||||||
|
SCSearchResponse, SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType,
|
||||||
|
} from '@openstapps/core';
|
||||||
import {sampleThingsMap} from '../../_helpers/data/sample-things';
|
import {sampleThingsMap} from '../../_helpers/data/sample-things';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {DataModule} from './data.module';
|
import {DataModule} from './data.module';
|
||||||
@@ -96,6 +98,61 @@ describe('DataProvider', () => {
|
|||||||
expect(response).toEqual(sampleResponse);
|
expect(response).toEqual(sampleResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should provide backend data items using multi search query', async () => {
|
||||||
|
spyOn(Client.prototype as any, 'multiSearch').and.callFake(() => ({
|
||||||
|
then: (callback: any) => {
|
||||||
|
return callback({
|
||||||
|
a: sampleResponse,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const response = await dataProvider.multiSearch({a: sampleQuery});
|
||||||
|
expect(response).toEqual({a: sampleResponse});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should partition search requests correctly', async () => {
|
||||||
|
const request = {
|
||||||
|
a: 'a',
|
||||||
|
b: 'b',
|
||||||
|
c: 'c',
|
||||||
|
d: 'd',
|
||||||
|
e: 'e',
|
||||||
|
} as SCMultiSearchRequest; // and response...
|
||||||
|
const requestCheck = Object.assign({}, request);
|
||||||
|
const responseShould = {
|
||||||
|
a: 'A',
|
||||||
|
b: 'B',
|
||||||
|
c: 'C',
|
||||||
|
d: 'D',
|
||||||
|
e: 'E',
|
||||||
|
};
|
||||||
|
|
||||||
|
dataProvider.backendQueriesLimit = 2;
|
||||||
|
spyOn(Client.prototype as any, 'multiSearch').and.callFake((req: SCMultiSearchRequest) => ({
|
||||||
|
then: (callback: any) => {
|
||||||
|
let i = 0;
|
||||||
|
for (const key in req) {
|
||||||
|
if (req.hasOwnProperty(key)) {
|
||||||
|
i++;
|
||||||
|
// @ts-ignore
|
||||||
|
expect(requestCheck[key]).not.toBeNull();
|
||||||
|
expect(requestCheck[key]).toEqual(req[key]);
|
||||||
|
// @ts-ignore
|
||||||
|
requestCheck[key] = null;
|
||||||
|
// @ts-ignore
|
||||||
|
req[key] = req[key].toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(i).toBeLessThanOrEqual(dataProvider.backendQueriesLimit);
|
||||||
|
|
||||||
|
return callback(req);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await dataProvider.multiSearch(request);
|
||||||
|
expect(response).toEqual(responseShould);
|
||||||
|
});
|
||||||
|
|
||||||
it('should put an data item into the local database (storage)', async () => {
|
it('should put an data item into the local database (storage)', async () => {
|
||||||
let providedThing: SCSaveableThing<SCThing>;
|
let providedThing: SCSaveableThing<SCThing>;
|
||||||
spyOn(storageProvider, 'put' as any).and.callFake((_id: any, thing: any) => {
|
spyOn(storageProvider, 'put' as any).and.callFake((_id: any, thing: any) => {
|
||||||
|
|||||||
@@ -14,8 +14,16 @@
|
|||||||
*/
|
*/
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {Client} from '@openstapps/api/lib/client';
|
import {Client} from '@openstapps/api/lib/client';
|
||||||
import {SCSearchRequest, SCSearchResponse, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core';
|
import {
|
||||||
|
SCMultiSearchRequest, SCMultiSearchResponse,
|
||||||
|
SCSearchRequest,
|
||||||
|
SCSearchResponse,
|
||||||
|
SCThingOriginType,
|
||||||
|
SCThings,
|
||||||
|
SCThingType,
|
||||||
|
} from '@openstapps/core';
|
||||||
import {SCSaveableThing} from '@openstapps/core';
|
import {SCSaveableThing} from '@openstapps/core';
|
||||||
|
import {chunk, fromPairs, toPairs} from 'lodash-es';
|
||||||
import {environment} from '../../../environments/environment';
|
import {environment} from '../../../environments/environment';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||||
@@ -35,6 +43,20 @@ export enum DataScope {
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class DataProvider {
|
export class DataProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*/
|
||||||
|
get storagePrefix(): string {
|
||||||
|
return this._storagePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*/
|
||||||
|
set storagePrefix(storagePrefix) {
|
||||||
|
this._storagePrefix = storagePrefix;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* TODO
|
* TODO
|
||||||
*/
|
*/
|
||||||
@@ -43,6 +65,10 @@ export class DataProvider {
|
|||||||
* Version of the app (used for the header in communication with the backend)
|
* Version of the app (used for the header in communication with the backend)
|
||||||
*/
|
*/
|
||||||
appVersion = environment.backend_version;
|
appVersion = environment.backend_version;
|
||||||
|
/**
|
||||||
|
* Maximum number of sub-queries in a multi-query allowed by the backend
|
||||||
|
*/
|
||||||
|
backendQueriesLimit = 5;
|
||||||
/**
|
/**
|
||||||
* TODO
|
* TODO
|
||||||
*/
|
*/
|
||||||
@@ -144,6 +170,18 @@ export class DataProvider {
|
|||||||
return this.storageProvider.has(this.getDataKey(uid));
|
return this.storageProvider.has(this.getDataKey(uid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs multiple searches at once and returns their responses
|
||||||
|
*
|
||||||
|
* @param query - query to send to the backend (auto-splits according to the backend limit)
|
||||||
|
*/
|
||||||
|
async multiSearch(query: SCMultiSearchRequest): Promise<SCMultiSearchResponse> {
|
||||||
|
// partition object into chunks, process those requests in parallel, then merge their responses again
|
||||||
|
return Object.assign({}, ...(await Promise.all(chunk(toPairs(query), this.backendQueriesLimit)
|
||||||
|
.map((request) => this.client.multiSearch(fromPairs(request))),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a data item
|
* Save a data item
|
||||||
*
|
*
|
||||||
@@ -174,18 +212,4 @@ export class DataProvider {
|
|||||||
async search(query: SCSearchRequest): Promise<SCSearchResponse> {
|
async search(query: SCSearchRequest): Promise<SCSearchResponse> {
|
||||||
return (this.client.search(query));
|
return (this.client.search(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
get storagePrefix(): string {
|
|
||||||
return this._storagePrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
set storagePrefix(storagePrefix) {
|
|
||||||
this._storagePrefix = storagePrefix;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
|
|
||||||
import moment, {Moment} from 'moment';
|
import moment, {Moment} from 'moment';
|
||||||
|
|
||||||
import {AfterViewInit, ChangeDetectorRef, Component, Input} from '@angular/core';
|
import {AfterViewInit, Component, Input} from '@angular/core';
|
||||||
import {SCDish, SCPlace} from '@openstapps/core';
|
import {SCDish, SCISO8601Date, SCPlace} from '@openstapps/core';
|
||||||
|
import {keys} from 'lodash-es';
|
||||||
import {PlaceMensaService} from './place-mensa-service';
|
import {PlaceMensaService} from './place-mensa-service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,14 +31,14 @@ import {PlaceMensaService} from './place-mensa-service';
|
|||||||
export class PlaceMensaDetailComponent implements AfterViewInit {
|
export class PlaceMensaDetailComponent implements AfterViewInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Map of dishes for each day
|
||||||
*/
|
*/
|
||||||
dishes: Promise<Record<string, SCDish[]>> | null = null;
|
dishes: Promise<Record<SCISO8601Date, SCDish[]>> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* number of days to display mensa menus for
|
* number of days to display mensa menus for
|
||||||
*/
|
*/
|
||||||
displayRange = 5;
|
@Input() displayRange = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* TODO
|
||||||
@@ -45,7 +46,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
|
|||||||
@Input() item: SCPlace;
|
@Input() item: SCPlace;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* The currently selected day
|
||||||
*/
|
*/
|
||||||
selectedDay: string;
|
selectedDay: string;
|
||||||
|
|
||||||
@@ -54,9 +55,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
|
|||||||
*/
|
*/
|
||||||
startingDay: Moment;
|
startingDay: Moment;
|
||||||
|
|
||||||
|
constructor(private mensaService: PlaceMensaService) {
|
||||||
constructor(private mensaService: PlaceMensaService, private changeDetectorRef: ChangeDetectorRef) {
|
|
||||||
// TODO: translation
|
|
||||||
this.startingDay = moment();
|
this.startingDay = moment();
|
||||||
this.startingDay.hour(0);
|
this.startingDay.hour(0);
|
||||||
this.startingDay.minute(0);
|
this.startingDay.minute(0);
|
||||||
@@ -67,55 +66,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
|
|||||||
* TODO
|
* TODO
|
||||||
*/
|
*/
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.dishes = this.mensaService.getAllDishes(this.item)
|
this.dishes = this.mensaService.getAllDishes(this.item, this.displayRange);
|
||||||
.then((dishesResult) => {
|
this.dishes.then((result) => this.selectedDay = keys(result)[0]);
|
||||||
const out = this.splitDishes(dishesResult, this.item, this.startingDay, this.displayRange);
|
|
||||||
for (const key in out) {
|
|
||||||
if (out.hasOwnProperty(key)) {
|
|
||||||
this.selectedDay = key;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.changeDetectorRef.detectChanges();
|
|
||||||
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Splits a list of dishes with availability start and end into multiple lists by day
|
|
||||||
*/
|
|
||||||
// tslint:disable-next-line:prefer-function-over-method
|
|
||||||
splitDishes(dishes: SCDish[], place: SCPlace, startingDay: Moment, displayRange: number): Record<string, SCDish[]> {
|
|
||||||
const out: Record<string, SCDish[]> = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < displayRange; i++) {
|
|
||||||
const nextDay: Moment = moment(startingDay)
|
|
||||||
.add(1, 'days');
|
|
||||||
const selectedDishes: SCDish[] = [];
|
|
||||||
for (const dish of dishes) {
|
|
||||||
// go through all offers
|
|
||||||
if (dish.offers === undefined) { continue; }
|
|
||||||
for (const offer of dish.offers) {
|
|
||||||
if (offer.inPlace === undefined || offer.inPlace.uid !== place.uid) { continue; }
|
|
||||||
// get availabilities
|
|
||||||
const availabilityStarts = offer.availabilityStarts === undefined ? undefined : moment(offer.availabilityStarts);
|
|
||||||
const availabilityEnds = offer.availabilityEnds === undefined ? undefined : moment(offer.availabilityEnds);
|
|
||||||
|
|
||||||
if ((availabilityStarts === undefined || availabilityStarts.isBefore(nextDay))
|
|
||||||
&& (availabilityEnds === undefined || availabilityEnds.isAfter(startingDay))) {
|
|
||||||
selectedDishes.push(dish);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (selectedDishes.length > 0) {
|
|
||||||
out[startingDay.format('L')] = selectedDishes;
|
|
||||||
}
|
|
||||||
// tslint:disable-next-line:no-parameter-reassignment
|
|
||||||
startingDay = nextDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {SCDish, SCPlace, SCThingType} from '@openstapps/core';
|
import {SCDish, SCISO8601Date, SCMultiSearchRequest, SCPlace, SCThingType} from '@openstapps/core';
|
||||||
|
import {keyBy, mapValues, range} from 'lodash-es';
|
||||||
|
import moment from 'moment';
|
||||||
import {DataProvider} from '../../../../data.provider';
|
import {DataProvider} from '../../../../data.provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,40 +26,54 @@ import {DataProvider} from '../../../../data.provider';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class PlaceMensaService {
|
export class PlaceMensaService {
|
||||||
constructor(private dataProvider: DataProvider) { }
|
constructor(private dataProvider: DataProvider) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all dishes for this building
|
* Fetches all dishes for this building
|
||||||
|
*
|
||||||
|
* Splits dishes as such that each list contains all dishes that are available at that day.
|
||||||
*/
|
*/
|
||||||
async getAllDishes(place: SCPlace): Promise<SCDish[]> {
|
async getAllDishes(place: SCPlace, days: number): Promise<Record<SCISO8601Date, SCDish[]>> {
|
||||||
// use filter to get all dishes with the building's uid in one of the offer's inPlace field
|
const request: SCMultiSearchRequest = mapValues(
|
||||||
// TODO: make sure this actually works with ES
|
keyBy(range(days)
|
||||||
const result = await this.dataProvider.search({
|
.map((i) => moment()
|
||||||
filter: {
|
.add(i, 'days')
|
||||||
arguments: {
|
.toISOString()),
|
||||||
filters: [
|
), (date: SCISO8601Date) => ({
|
||||||
{
|
filter: {
|
||||||
arguments: {
|
arguments: {
|
||||||
field: 'offers.inPlace.uid',
|
filters: [
|
||||||
value: place.uid,
|
{
|
||||||
|
arguments: {
|
||||||
|
field: 'offers.inPlace.uid',
|
||||||
|
value: place.uid,
|
||||||
|
},
|
||||||
|
type: 'value',
|
||||||
},
|
},
|
||||||
type: 'value',
|
{
|
||||||
},
|
arguments: {
|
||||||
{
|
field: 'type',
|
||||||
arguments: {
|
value: SCThingType.Dish,
|
||||||
field: 'type',
|
},
|
||||||
value: SCThingType.Dish,
|
type: 'value',
|
||||||
},
|
},
|
||||||
type: 'value',
|
{
|
||||||
},
|
arguments: {
|
||||||
],
|
field: 'offers.availabilityRange',
|
||||||
operation: 'and',
|
scope: 'd',
|
||||||
|
time: date,
|
||||||
|
},
|
||||||
|
type: 'availability',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
operation: 'and',
|
||||||
|
},
|
||||||
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
type: 'boolean',
|
size: 1000,
|
||||||
},
|
}));
|
||||||
size: 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.data as SCDish[];
|
return mapValues(await this.dataProvider.multiSearch(request), 'data') as Record<SCISO8601Date, SCDish[]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<div *ngIf="dishes | async as dishes; else loading">
|
<div *ngIf="dishes | async as dishes; else loading">
|
||||||
<ion-segment [(ngModel)]="selectedDay">
|
<ion-segment [(ngModel)]="selectedDay">
|
||||||
<ion-segment-button *ngFor="let day of dishes | keyvalue" [value]="day.key">
|
<ion-segment-button *ngFor="let date of dishes | keyvalue" [value]="date.key">
|
||||||
<ion-label>{{day.key | amParse:'YYYY-MM-DD' | amDateFormat:'dddd, L'}}</ion-label>
|
<ion-label>{{date.key | amDateFormat:'dddd, L'}}</ion-label>
|
||||||
</ion-segment-button>
|
</ion-segment-button>
|
||||||
</ion-segment>
|
</ion-segment>
|
||||||
<div [ngSwitch]="selectedDay">
|
<div [ngSwitch]="selectedDay">
|
||||||
<div *ngFor="let day of dishes | keyvalue">
|
<div *ngFor="let date of dishes | keyvalue">
|
||||||
<ion-list *ngSwitchCase="day.key">
|
<ion-list *ngSwitchCase="date.key">
|
||||||
<stapps-data-list-item [item]="dish" *ngFor="let dish of day.value"></stapps-data-list-item>
|
<stapps-data-list-item [item]="dish" *ngFor="let dish of date.value"></stapps-data-list-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user