mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-10 19:52:53 +00:00
refactor: use range query for canteen module
This commit is contained in:
@@ -14,8 +14,10 @@
|
||||
*/
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {Client} from '@openstapps/api/lib/client';
|
||||
import {SCDish, SCMessage, SCSaveableThing, SCSearchQuery,
|
||||
SCSearchResponse, SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core';
|
||||
import {
|
||||
SCDish, SCMessage, SCMultiSearchRequest, SCSaveableThing, SCSearchQuery,
|
||||
SCSearchResponse, SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType,
|
||||
} from '@openstapps/core';
|
||||
import {sampleThingsMap} from '../../_helpers/data/sample-things';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {DataModule} from './data.module';
|
||||
@@ -96,6 +98,61 @@ describe('DataProvider', () => {
|
||||
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 () => {
|
||||
let providedThing: SCSaveableThing<SCThing>;
|
||||
spyOn(storageProvider, 'put' as any).and.callFake((_id: any, thing: any) => {
|
||||
|
||||
@@ -14,8 +14,16 @@
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
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 {chunk, fromPairs, toPairs} from 'lodash-es';
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||
@@ -35,6 +43,20 @@ export enum DataScope {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DataProvider {
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
get storagePrefix(): string {
|
||||
return this._storagePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
set storagePrefix(storagePrefix) {
|
||||
this._storagePrefix = storagePrefix;
|
||||
}
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@@ -43,6 +65,10 @@ export class DataProvider {
|
||||
* Version of the app (used for the header in communication with the backend)
|
||||
*/
|
||||
appVersion = environment.backend_version;
|
||||
/**
|
||||
* Maximum number of sub-queries in a multi-query allowed by the backend
|
||||
*/
|
||||
backendQueriesLimit = 5;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@@ -144,6 +170,18 @@ export class DataProvider {
|
||||
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
|
||||
*
|
||||
@@ -174,18 +212,4 @@ export class DataProvider {
|
||||
async search(query: SCSearchRequest): Promise<SCSearchResponse> {
|
||||
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 {AfterViewInit, ChangeDetectorRef, Component, Input} from '@angular/core';
|
||||
import {SCDish, SCPlace} from '@openstapps/core';
|
||||
import {AfterViewInit, Component, Input} from '@angular/core';
|
||||
import {SCDish, SCISO8601Date, SCPlace} from '@openstapps/core';
|
||||
import {keys} from 'lodash-es';
|
||||
import {PlaceMensaService} from './place-mensa-service';
|
||||
|
||||
/**
|
||||
@@ -30,14 +31,14 @@ import {PlaceMensaService} from './place-mensa-service';
|
||||
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
|
||||
*/
|
||||
displayRange = 5;
|
||||
@Input() displayRange = 5;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
@@ -45,7 +46,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
|
||||
@Input() item: SCPlace;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* The currently selected day
|
||||
*/
|
||||
selectedDay: string;
|
||||
|
||||
@@ -54,9 +55,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
|
||||
*/
|
||||
startingDay: Moment;
|
||||
|
||||
|
||||
constructor(private mensaService: PlaceMensaService, private changeDetectorRef: ChangeDetectorRef) {
|
||||
// TODO: translation
|
||||
constructor(private mensaService: PlaceMensaService) {
|
||||
this.startingDay = moment();
|
||||
this.startingDay.hour(0);
|
||||
this.startingDay.minute(0);
|
||||
@@ -67,55 +66,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
|
||||
* TODO
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
this.dishes = this.mensaService.getAllDishes(this.item)
|
||||
.then((dishesResult) => {
|
||||
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;
|
||||
this.dishes = this.mensaService.getAllDishes(this.item, this.displayRange);
|
||||
this.dishes.then((result) => this.selectedDay = keys(result)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -24,40 +26,54 @@ import {DataProvider} from '../../../../data.provider';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PlaceMensaService {
|
||||
constructor(private dataProvider: DataProvider) { }
|
||||
constructor(private dataProvider: DataProvider) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[]> {
|
||||
// use filter to get all dishes with the building's uid in one of the offer's inPlace field
|
||||
// TODO: make sure this actually works with ES
|
||||
const result = await this.dataProvider.search({
|
||||
filter: {
|
||||
arguments: {
|
||||
filters: [
|
||||
{
|
||||
arguments: {
|
||||
field: 'offers.inPlace.uid',
|
||||
value: place.uid,
|
||||
async getAllDishes(place: SCPlace, days: number): Promise<Record<SCISO8601Date, SCDish[]>> {
|
||||
const request: SCMultiSearchRequest = mapValues(
|
||||
keyBy(range(days)
|
||||
.map((i) => moment()
|
||||
.add(i, 'days')
|
||||
.toISOString()),
|
||||
), (date: SCISO8601Date) => ({
|
||||
filter: {
|
||||
arguments: {
|
||||
filters: [
|
||||
{
|
||||
arguments: {
|
||||
field: 'offers.inPlace.uid',
|
||||
value: place.uid,
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.Dish,
|
||||
{
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.Dish,
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
operation: 'and',
|
||||
{
|
||||
arguments: {
|
||||
field: 'offers.availabilityRange',
|
||||
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>
|
||||
<div *ngIf="dishes | async as dishes; else loading">
|
||||
<ion-segment [(ngModel)]="selectedDay">
|
||||
<ion-segment-button *ngFor="let day of dishes | keyvalue" [value]="day.key">
|
||||
<ion-label>{{day.key | amParse:'YYYY-MM-DD' | amDateFormat:'dddd, L'}}</ion-label>
|
||||
<ion-segment-button *ngFor="let date of dishes | keyvalue" [value]="date.key">
|
||||
<ion-label>{{date.key | amDateFormat:'dddd, L'}}</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<div [ngSwitch]="selectedDay">
|
||||
<div *ngFor="let day of dishes | keyvalue">
|
||||
<ion-list *ngSwitchCase="day.key">
|
||||
<stapps-data-list-item [item]="dish" *ngFor="let dish of day.value"></stapps-data-list-item>
|
||||
<div *ngFor="let date of dishes | keyvalue">
|
||||
<ion-list *ngSwitchCase="date.key">
|
||||
<stapps-data-list-item [item]="dish" *ngFor="let dish of date.value"></stapps-data-list-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user