diff --git a/src/app/modules/data/data-facets.provider.spec.ts b/src/app/modules/data/data-facets.provider.spec.ts
new file mode 100644
index 00000000..1fffaab2
--- /dev/null
+++ b/src/app/modules/data/data-facets.provider.spec.ts
@@ -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 .
+ */
+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);
+ });
+});
diff --git a/src/app/modules/data/data-facets.provider.ts b/src/app/modules/data/data-facets.provider.ts
new file mode 100644
index 00000000..452e6ef6
--- /dev/null
+++ b/src/app/modules/data/data-facets.provider.ts
@@ -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 .
+ */
+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);
+ }
+}
diff --git a/src/app/modules/data/data.module.ts b/src/app/modules/data/data.module.ts
index 44b17a5a..3501f798 100644
--- a/src/app/modules/data/data.module.ts
+++ b/src/app/modules/data/data.module.ts
@@ -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,
],
})
diff --git a/src/app/modules/data/data.provider.spec.ts b/src/app/modules/data/data.provider.spec.ts
index b842763a..ca2dbc1d 100644
--- a/src/app/modules/data/data.provider.spec.ts
+++ b/src/app/modules/data/data.provider.spec.ts
@@ -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 = {
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({
diff --git a/src/app/modules/data/detail/data-detail.component.spec.ts b/src/app/modules/data/detail/data-detail.component.spec.ts
index 6d4e3a44..b2c4c242 100644
--- a/src/app/modules/data/detail/data-detail.component.spec.ts
+++ b/src/app/modules/data/detail/data-detail.component.spec.ts
@@ -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: {
diff --git a/src/app/modules/data/list/data-list.component.ts b/src/app/modules/data/list/data-list.component.ts
index c26bf020..6da72370 100644
--- a/src/app/modules/data/list/data-list.component.ts
+++ b/src/app/modules/data/list/data-list.component.ts
@@ -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 {
+ async loadMore(event: any): Promise {
this.from += this.size;
await this.fetchItems();
event.target.complete();
- return;
}
search(query: string) {
this.queryChanged.next(query);
}
-
}