mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-09 11:12:52 +00:00
122
src/app/modules/data/data-facets.provider.spec.ts
Normal file
122
src/app/modules/data/data-facets.provider.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {SCFacet, SCThing} from '@openstapps/core';
|
||||
import {sampleAggregations} from '../../_helpers/data/sampleConfiguration';
|
||||
import {sampleThingsMap} from '../../_helpers/data/sampleThings';
|
||||
import {DataFacetsProvider} from './data-facets.provider';
|
||||
import {DataModule} from './data.module';
|
||||
import {DataProvider} from './data.provider';
|
||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||
|
||||
describe('DataProvider', () => {
|
||||
let dataFacetsProvider: DataFacetsProvider;
|
||||
const sampleFacets: SCFacet[] = [
|
||||
{
|
||||
buckets: [{education: 4}, {learn: 3}, {computer: 3}],
|
||||
field: 'categories',
|
||||
},
|
||||
{
|
||||
buckets: [{'Major One': 1}, {'Major Two': 2}, {'Major Three': 1}],
|
||||
field: 'majors',
|
||||
},
|
||||
{
|
||||
buckets: [{building: 3}, {room: 7}],
|
||||
field: 'type',
|
||||
},
|
||||
];
|
||||
|
||||
const sampleFacetsMap: {[key: string]: {[key: string]: number}} = {
|
||||
categories: {education: 4, learn: 3, computer: 3},
|
||||
majors: {'Major One': 1, 'Major Two': 2, 'Major Three': 1},
|
||||
type: {building: 3, room: 7},
|
||||
};
|
||||
|
||||
const sampleItems: SCThing[] = [
|
||||
...sampleThingsMap.building,
|
||||
...sampleThingsMap.person,
|
||||
...sampleThingsMap.room,
|
||||
...sampleThingsMap['academic event'],
|
||||
];
|
||||
|
||||
const sampleBuckets: Array<{[key: string]: number}> = [{foo: 1}, {bar: 2}, {'foo bar': 3}];
|
||||
const sampleBucketsMap: {[key: string]: number} = {foo: 1, bar: 2, 'foo bar': 3};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DataModule],
|
||||
providers: [
|
||||
DataProvider,
|
||||
StAppsWebHttpClient,
|
||||
],
|
||||
});
|
||||
dataFacetsProvider = TestBed.get(DataFacetsProvider);
|
||||
});
|
||||
|
||||
it('should add buckets properly', () => {
|
||||
let bucketsMap: {[key: string]: number} = {};
|
||||
bucketsMap = dataFacetsProvider.addBuckets(bucketsMap, ['foo']);
|
||||
expect(bucketsMap).toEqual({foo: 1});
|
||||
|
||||
bucketsMap = dataFacetsProvider.addBuckets(bucketsMap, ['foo']);
|
||||
expect(bucketsMap).toEqual({foo: 2});
|
||||
|
||||
bucketsMap = dataFacetsProvider.addBuckets(bucketsMap, ['bar']);
|
||||
expect(bucketsMap).toEqual({foo: 2, bar: 1});
|
||||
});
|
||||
|
||||
it('should convert buckets to buckets map', () => {
|
||||
expect(dataFacetsProvider.bucketsToMap(sampleBuckets)).toEqual(sampleBucketsMap);
|
||||
});
|
||||
|
||||
it('should convert buckets map into buckets', () => {
|
||||
expect(dataFacetsProvider.mapToBuckets(sampleBucketsMap)).toEqual(sampleBuckets);
|
||||
});
|
||||
|
||||
it('should convert facets into a facets map', () => {
|
||||
expect(dataFacetsProvider.facetsToMap(sampleFacets)).toEqual(sampleFacetsMap);
|
||||
});
|
||||
|
||||
it('should convert facets map into facets', () => {
|
||||
expect(dataFacetsProvider.mapToFacets(sampleFacetsMap)).toEqual(sampleFacets);
|
||||
});
|
||||
|
||||
it('should extract facets (and append them if needed) from the data', () => {
|
||||
const sampleCombinedFacets: SCFacet[] = [
|
||||
{
|
||||
buckets: [{computer: 3}, {course: 1}, {education: 5}, {learn: 3}, {library: 1}, {practicum: 1}],
|
||||
field: 'categories',
|
||||
},
|
||||
{
|
||||
buckets: [{'Major One': 2}, {'Major Two': 4}, {'Major Three': 2}],
|
||||
field: 'majors',
|
||||
},
|
||||
{
|
||||
buckets: [{building: 4}, {'academic event': 2}, {person: 2}, {room: 8}],
|
||||
field: 'type',
|
||||
},
|
||||
];
|
||||
const checkEqual = (expected: SCFacet[], actual: SCFacet[]) => {
|
||||
const expectedMap = dataFacetsProvider.facetsToMap(expected);
|
||||
const actualMap = dataFacetsProvider.facetsToMap(actual);
|
||||
Object.keys(actualMap).forEach((key) => {
|
||||
Object.keys(actualMap[key]).forEach((subKey) => {
|
||||
expect(actualMap[key][subKey]).toBe(expectedMap[key][subKey]);
|
||||
});
|
||||
});
|
||||
};
|
||||
checkEqual(dataFacetsProvider.extractFacets(sampleItems, sampleAggregations, sampleFacets), sampleCombinedFacets);
|
||||
});
|
||||
});
|
||||
152
src/app/modules/data/data-facets.provider.ts
Normal file
152
src/app/modules/data/data-facets.provider.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
import {SCBackendAggregationConfiguration, SCFacet, SCThing} from '@openstapps/core';
|
||||
|
||||
@Injectable()
|
||||
export class DataFacetsProvider {
|
||||
// tslint:disable-next-line:no-empty
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds buckets to a map of buckets (e.g. if a buckets array is [{foo: 1}, {bar: 3}],
|
||||
* its bucketsMap is {foo: 1, bar: 3}), if a field 'bar' is added to it it becomes:
|
||||
* {foo: 1, bar: 4}
|
||||
*
|
||||
* @param bucketsMap Buckets array transformed into a map
|
||||
* @param fields A field that should be added to buckets (its map)
|
||||
*/
|
||||
addBuckets(bucketsMap: {[key: string]: number}, fields: string[]): {[key: string]: number} {
|
||||
fields.forEach((field) => {
|
||||
if (typeof bucketsMap[field] !== 'undefined') {
|
||||
bucketsMap[field] = bucketsMap[field] + 1;
|
||||
} else {
|
||||
bucketsMap[field] = 1;
|
||||
}
|
||||
});
|
||||
return bucketsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a buckets array to a map
|
||||
*
|
||||
* @param buckets Buckets from a facet
|
||||
*/
|
||||
bucketsToMap(buckets: Array<{[key: string]: number}>): {[key: string]: number} {
|
||||
const bucketsMap: {[key: string]: number} = {};
|
||||
buckets.forEach((bucket) => {
|
||||
for (const key in bucket) {
|
||||
if (bucket.hasOwnProperty(key)) {
|
||||
bucketsMap[key] = bucket[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return bucketsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a buckets map into buckets array (as it is inside of a facet)
|
||||
*
|
||||
* @param bucketsMap A map from a buckets array
|
||||
*/
|
||||
mapToBuckets(bucketsMap: {[key: string]: number}): Array<{[key: string]: number}> {
|
||||
const buckets: Array<{[key: string]: number}> = [];
|
||||
for (const key in bucketsMap) {
|
||||
if (bucketsMap.hasOwnProperty(key)) {
|
||||
const bucket: {[key: string]: number} = {};
|
||||
bucket[key] = bucketsMap[key];
|
||||
buckets.push(bucket);
|
||||
}
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts facets array into a map (for quicker operations with facets)
|
||||
*
|
||||
* @param facets Array of facets
|
||||
*/
|
||||
facetsToMap(facets: SCFacet[]): {[key: string]: {[key: string]: number}} {
|
||||
const facetsMap: {[key: string]: {[key: string]: number}} = {};
|
||||
facets.forEach((facet) => {
|
||||
facetsMap[facet.field] = this.bucketsToMap(facet.buckets);
|
||||
});
|
||||
return facetsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts facets map into an array of facets (as they are provided by backend)
|
||||
*
|
||||
* @param facetsMap A map from facets array
|
||||
*/
|
||||
mapToFacets(facetsMap: {[key: string]: {[key: string]: number}}): SCFacet[] {
|
||||
const facets: SCFacet[] = [];
|
||||
for (const key in facetsMap) {
|
||||
if (facetsMap.hasOwnProperty(key)) {
|
||||
const facet: SCFacet = {buckets: [], field: ''};
|
||||
facet.field = key;
|
||||
facet.buckets = this.mapToBuckets(facetsMap[key]);
|
||||
facets.push(facet);
|
||||
}
|
||||
}
|
||||
return facets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract facets from data items, optionally combine them with a list of existing facets
|
||||
*
|
||||
* @param items Items to extract facets from
|
||||
* @param aggregations Aggregations configuration(s) from backend
|
||||
* @param facets Existing facets to be combined with the facets from the items
|
||||
*/
|
||||
extractFacets(
|
||||
items: SCThing[],
|
||||
aggregations: SCBackendAggregationConfiguration[],
|
||||
facets: SCFacet[] = []): SCFacet[] {
|
||||
if (items.length === 0) {
|
||||
if (facets.length === 0) {
|
||||
return [];
|
||||
} else {
|
||||
return facets;
|
||||
}
|
||||
}
|
||||
const combinedFacets: SCFacet[] = facets;
|
||||
const combinedFacetsMap: {[key: string]: {[key: string]: number}} = this.facetsToMap(combinedFacets);
|
||||
(items as any[]).forEach((item) => {
|
||||
aggregations.forEach((aggregation) => {
|
||||
let fieldValues: string | string[] = item[aggregation.fieldName];
|
||||
if (typeof fieldValues === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (typeof fieldValues === 'string') {
|
||||
fieldValues = [fieldValues];
|
||||
}
|
||||
if (typeof aggregation.onlyOnTypes === 'undefined') {
|
||||
combinedFacetsMap[aggregation.fieldName] = this.addBuckets(
|
||||
combinedFacetsMap[aggregation.fieldName] || {},
|
||||
fieldValues,
|
||||
);
|
||||
} else if (aggregation.onlyOnTypes.indexOf(item.type) !== -1) {
|
||||
combinedFacetsMap[aggregation.fieldName] = this.addBuckets(
|
||||
combinedFacetsMap[aggregation.fieldName] || {},
|
||||
fieldValues,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return this.mapToFacets(combinedFacetsMap);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {FormsModule} from '@angular/forms';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {StorageModule} from '../storage/storage.module';
|
||||
import {DataFacetsProvider} from './data-facets.provider';
|
||||
import {DataRoutingModule} from './data-routing.module';
|
||||
import {DataProvider} from './data.provider';
|
||||
import {DataDetailComponent} from './detail/data-detail.component';
|
||||
@@ -55,6 +56,7 @@ import {EventListItemComponent} from './types/event/event-list-item.component';
|
||||
],
|
||||
providers: [
|
||||
DataProvider,
|
||||
DataFacetsProvider,
|
||||
StAppsWebHttpClient,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {Client} from '@openstapps/api/lib/client';
|
||||
import {SCMessage, SCSaveableThing, SCSearchQuery, SCSearchResponse,
|
||||
SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core';
|
||||
import {SCDish, SCMessage, SCSaveableThing, SCSearchQuery,
|
||||
SCSearchResponse, SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core';
|
||||
import {sampleThingsMap} from '../../_helpers/data/sampleThings';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {DataModule} from './data.module';
|
||||
import {DataProvider, DataScope} from './data.provider';
|
||||
@@ -24,31 +25,9 @@ import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||
describe('DataProvider', () => {
|
||||
let dataProvider: DataProvider;
|
||||
let storageProvider: StorageProvider;
|
||||
const sampleThing: SCMessage = sampleThingsMap.message[0] as SCMessage;
|
||||
const sampleResponse: SCSearchResponse = {
|
||||
data: [
|
||||
{
|
||||
categories: ['main dish'],
|
||||
name: 'foo dish',
|
||||
origin: {
|
||||
indexed: '12345',
|
||||
name: 'bar',
|
||||
type: SCThingOriginType.Remote,
|
||||
},
|
||||
type: SCThingType.Dish,
|
||||
uid: '123',
|
||||
},
|
||||
{
|
||||
categories: ['dessert'],
|
||||
name: 'foo dessert',
|
||||
origin: {
|
||||
indexed: '12345',
|
||||
name: 'bar',
|
||||
type: SCThingOriginType.Remote,
|
||||
},
|
||||
type: SCThingType.Dish,
|
||||
uid: '123',
|
||||
},
|
||||
],
|
||||
data: sampleThingsMap.dish as SCDish[],
|
||||
facets: [
|
||||
{
|
||||
buckets: [],
|
||||
@@ -75,18 +54,6 @@ describe('DataProvider', () => {
|
||||
filter: sampleFilter,
|
||||
};
|
||||
|
||||
const sampleThing: SCMessage = {
|
||||
audiences: ['students'],
|
||||
message: 'Foo Message',
|
||||
name: 'foo',
|
||||
origin: {
|
||||
indexed: 'SOME-DATE',
|
||||
name: 'some name',
|
||||
type: SCThingOriginType.Remote,
|
||||
},
|
||||
type: SCThingType.Message,
|
||||
uid: '123',
|
||||
};
|
||||
const sampleSavable: SCSaveableThing<SCThings> = {
|
||||
data: sampleThing,
|
||||
name: sampleThing.name,
|
||||
@@ -97,7 +64,7 @@ describe('DataProvider', () => {
|
||||
type: SCThingType.Message,
|
||||
uid: sampleThing.uid,
|
||||
};
|
||||
const otherSampleThing: SCMessage = {...sampleThing, uid: '456', name: 'bar'};
|
||||
const otherSampleThing: SCMessage = {...sampleThing, uid: 'message-456', name: 'bar'};
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
@@ -16,12 +16,12 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {ActivatedRoute, RouterModule} from '@angular/router';
|
||||
import {IonRefresher} from '@ionic/angular';
|
||||
import {SCMessage, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||
import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||
import {sampleThingsMap} from '../../../_helpers/data/sampleThings';
|
||||
import {DataRoutingModule} from '../data-routing.module';
|
||||
import {DataModule} from '../data.module';
|
||||
import {DataProvider} from '../data.provider';
|
||||
import {DataDetailComponent} from './data-detail.component';
|
||||
import {TranslateModule, TranslateLoader, TranslateFakeLoader} from '@ngx-translate/core';
|
||||
|
||||
describe('DataDetailComponent', () => {
|
||||
let comp: DataDetailComponent;
|
||||
@@ -29,25 +29,13 @@ describe('DataDetailComponent', () => {
|
||||
let detailPage: HTMLElement;
|
||||
let dataProvider: DataProvider;
|
||||
let refresher: IonRefresher;
|
||||
const sampleThing = sampleThingsMap.message[0];
|
||||
|
||||
// @Component({ selector: 'stapps-data-list-item', template: '' })
|
||||
// class DataListItemComponent {
|
||||
// @Input() item;
|
||||
// }
|
||||
|
||||
const sampleThing: SCMessage = {
|
||||
audiences: ['students'],
|
||||
message: 'Foo Message',
|
||||
name: 'foo',
|
||||
origin: {
|
||||
indexed: 'SOME-DATE',
|
||||
name: 'some name',
|
||||
type: SCThingOriginType.Remote,
|
||||
},
|
||||
type: SCThingType.Message,
|
||||
uid: '123',
|
||||
};
|
||||
|
||||
const fakeActivatedRoute = {
|
||||
snapshot: {
|
||||
paramMap: {
|
||||
|
||||
@@ -36,8 +36,11 @@ export class DataListComponent {
|
||||
|
||||
loading: HTMLIonLoadingElement;
|
||||
|
||||
// tslint:disable-next-line:max-line-length
|
||||
constructor(private loadingController: LoadingController, private alertController: AlertController, dataProvider: DataProvider) {
|
||||
constructor(
|
||||
private loadingController: LoadingController,
|
||||
private alertController: AlertController,
|
||||
dataProvider: DataProvider,
|
||||
) {
|
||||
this.dataProvider = dataProvider;
|
||||
this.queryChanged
|
||||
.pipe(
|
||||
@@ -84,15 +87,13 @@ export class DataListComponent {
|
||||
});
|
||||
}
|
||||
|
||||
async loadMore(event: any): Promise<any> {
|
||||
async loadMore(event: any): Promise<void> {
|
||||
this.from += this.size;
|
||||
await this.fetchItems();
|
||||
event.target.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
search(query: string) {
|
||||
this.queryChanged.next(query);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user