Skip to content

Commit

Permalink
facets: expand facet items by link
Browse files Browse the repository at this point in the history
* Adds a link to see more or less results on the facet.
* Closes #87

Co-Authored-by: Bertrand Zuchuat <[email protected]>
Co-Authored-by: Johnny Mariéthoz <[email protected]>
  • Loading branch information
Garfield-fr and jma committed Jul 11, 2019
1 parent af90732 commit b85e5b5
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 90 deletions.
70 changes: 59 additions & 11 deletions rero_ils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@

from rero_ils.modules.api import IlsRecordIndexer
from rero_ils.modules.loans.api import Loan
from rero_ils.utils import get_agg_config

from .modules.circ_policies.api import CircPolicy
from .modules.documents.api import Document
Expand Down Expand Up @@ -703,33 +704,80 @@ def _(x):
'status',
],
'expand': ['document_type'],
'initialBucketSize': 5
},
'ptrn': {
'order': [
'roles'
],
'expand': ['roles']
'expand': ['roles'],
'initialBucketSize': 5
},
'pers': {
'order': [
'sources'
],
'expand': ['sources']
'expand': ['sources'],
'initialBucketSize': 5
},
}

# Default number of results in facet
RERO_ILS_DEFAULT_AGGREGATION_SIZE = 50

# Number of aggregation by index name
RERO_ILS_AGGREGATION_SIZE = {
'documents': 50
}

RECORDS_REST_FACETS = {
'documents': dict(
aggs=dict(
document_type=dict(terms=dict(field='type')),
library=dict(terms=dict(field='items.library_pid')),
author__en=dict(terms=dict(field='facet_authors_en')),
author__fr=dict(terms=dict(field='facet_authors_fr')),
author__de=dict(terms=dict(field='facet_authors_de')),
author__it=dict(terms=dict(field='facet_authors_it')),
language=dict(terms=dict(field='languages.language')),
subject=dict(terms=dict(field='facet_subjects')),
status=dict(terms=dict(field='items.status'))
document_type=partial(
get_agg_config,
index_name='documents',
field='type'
),
library=partial(
get_agg_config,
index_name='documents',
field='items.library_pid'
),
author__en=partial(
get_agg_config,
index_name='documents',
field='facet_authors_en'
),
author__fr=partial(
get_agg_config,
index_name='documents',
field='facet_authors_fr'
),
author__de=partial(
get_agg_config,
index_name='documents',
field='facet_authors_de'
),
author__it=partial(
get_agg_config,
index_name='documents',
field='facet_authors_it'
),
language=partial(
get_agg_config,
index_name='documents',
field='languages.language'
),
subject=partial(
get_agg_config,
index_name='documents',
field='facet_subjects'
),
status=partial(
get_agg_config,
index_name='items.status',
field='items.status'
)
),
filters={
_('document_type'): terms_filter('type'),
Expand Down
22 changes: 22 additions & 0 deletions rero_ils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,25 @@ def send_mail(subject, recipients, template, language, **context):
def unique_list(data):
"""Unicity of list."""
return list(dict.fromkeys(data))


def get_agg_config(index_name, field):
"""Get Elasticsearch aggregation term configuration.
This function allows to configure aggregation size per index name using
environnement variables.
:param index_name: name of Elasticsearch index
:param field: field name for the aggregation
:return: dict of Elasticsearch DSL aggregation configuration
"""
from flask import current_app
return dict(terms=dict(
field=field,
size=current_app.config.get(
'RERO_ILS_AGGREGATION_SIZE', {}
).get(
index_name,
current_app.config.get('RERO_ILS_DEFAULT_AGGREGATION_SIZE')
)
))
4 changes: 3 additions & 1 deletion ui/src/app/records/records.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { PersonsSearchComponent } from './search/public-search/persons-search.co
import { RefAuthorityComponent } from './editor/ref-authority/ref-authority.component';
import { TypeaheadModule } from 'ngx-bootstrap/typeahead';
import { RolesCheckboxesComponent } from './editor/roles-checkboxes/roles-checkboxes.component';
import { AggregationComponent } from './search/aggregation/aggregation.component';

@NgModule({
declarations: [
Expand Down Expand Up @@ -79,7 +80,8 @@ import { RolesCheckboxesComponent } from './editor/roles-checkboxes/roles-checkb
DocumentsSearchComponent,
PersonsSearchComponent,
RefAuthorityComponent,
RolesCheckboxesComponent
RolesCheckboxesComponent,
AggregationComponent
],
imports: [
CommonModule,
Expand Down
34 changes: 34 additions & 0 deletions ui/src/app/records/search/aggregation/aggregation.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<section class="mb-2" *ngIf="aggregation.buckets.length">
<a class="text-muted" [ngClass]="{'collapsed': !isOpen(aggregation.title)}"
data-toggle="collapse" href="#{{'agg_'+aggregation.title }}"
aria-expanded="false" aria-controls="libraryData">
<h6 class="mb-0 d-inline border-bottom pb-1 font-weight-bold"><i class="fa fa-caret-down" aria-hidden="true"></i> {{ aggregation.name | translate }}</h6>
</a>
<div class="collapse" [ngClass]="{'show': isOpen(aggregation.title)}" id="{{'agg_'+aggregation.title }}">
<ul class="list-unstyled mb-0">
<li class="form-check" *ngFor="let bucket of aggregation.buckets|slice:0:sizeOfBucket()">
<input class="form-check-input" type="checkbox" [checked]="isFiltered(aggregation.title, bucket.key)" (click)="aggFilter(aggregation.title, bucket.key) ">
<label class="form-check-label">
<span *ngIf="bucket.name">{{ bucket.name | translate }}</span>
<span *ngIf="!bucket.name">{{ bucket.key | translate }}</span> ({{ bucket.doc_count }})
</label>
</li>
</ul>
<div *ngIf="displayMoreAndLessLink()">
<a
*ngIf="moreMode"
href="#"
data-toggle="collapse"
(click)="setMoreMode(false)"
translate
>more…</a>
<a
*ngIf="!moreMode"
href="#"
data-toggle="collapse"
(click)="setMoreMode(true)"
translate
>less…</a>
</div>
</div>
</section>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { AggregationComponent } from './aggregation.component';

describe('AggregationComponent', () => {
let component: AggregationComponent;
let fixture: ComponentFixture<AggregationComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AggregationComponent ]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(AggregationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
61 changes: 61 additions & 0 deletions ui/src/app/records/search/aggregation/aggregation.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'app-aggregation',
templateUrl: './aggregation.component.html',
styleUrls: ['./aggregation.component.scss']
})
export class AggregationComponent {

@Input() aggFilters = null;
@Input() aggsSettings = null;
@Input() aggregation = null;

@Output() addAggFilter = new EventEmitter<{term: string, value: string}>();
@Output() removeAggFilter = new EventEmitter<{term: string, value: string}>();

private moreMode = true;

isFiltered(term: any, value?: any) {
if (value) {
const filterValue = `${term}=${value}`;
return this.aggFilters.some((val: any) => filterValue === val);
} else {
return this.aggFilters.some((val: any) => term === val.split('=')[0]);
}
}

aggFilter(term: string, value: string) {
if (this.isFiltered(term, value)) {
this.removeAggFilter.emit({term: term, value: value});
} else {
this.addAggFilter.emit({term: term, value: value});
}
}

isOpen(title: string) {
if (this.isFiltered(title)) {
return true;
}
if (this.aggsSettings.expand.some((value: any) => value === title)) {
return true;
}
return false;
}

sizeOfBucket() {
if (this.moreMode) {
return this.aggsSettings.initialBucketSize;
} else {
return this.aggregation.buckets.length;
}
}

displayMoreAndLessLink() {
return this.aggregation.buckets.length > this.aggsSettings.initialBucketSize;
}

setMoreMode(state: boolean) {
this.moreMode = state;
}
}
23 changes: 7 additions & 16 deletions ui/src/app/records/search/search.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,13 @@ <h6>

<aside *ngIf="aggregations && aggregations.length" class="col-md-4 col-lg-3 order-12 order-md-0">
<div *ngFor="let item of aggregations">
<section class="mb-2" *ngIf="item.buckets.length" >
<a class="text-muted" [ngClass]="{'collapsed': !startOpen(item.title)}"
data-toggle="collapse" href="#{{'agg_'+item.title }}"
aria-expanded="false" aria-controls="libraryData">
<h6 class="mb-0 d-inline border-bottom pb-1 font-weight-bold"><i class="fa fa-caret-down" aria-hidden="true"></i> {{ item.name | translate }}</h6>
</a>
<ul class="list-unstyled collapse" [ngClass]="{'show': startOpen(item.title)}" id="{{'agg_'+item.title }}">
<li class="form-check" *ngFor="let bucket of item.buckets">
<input class="form-check-input" type="checkbox" [checked]="isFiltered(item.title, bucket.key)" (click)="aggFilter(item.title, bucket.key) ">
<label class="form-check-label">
<span *ngIf="bucket.name">{{ bucket.name | translate }}</span>
<span *ngIf="!bucket.name">{{ bucket.key | translate }}</span> ({{ bucket.doc_count }})
</label>
</li>
</ul>
</section>
<app-aggregation
[aggFilters]="aggFilters"
[aggsSettings]="aggsSettings"
[aggregation]="item"
(addAggFilter)="addAggFilter($event)"
(removeAggFilter)="removeAggFilter($event)"
></app-aggregation>
</div>
</aside>
<section [ngClass]="aggregations && aggregations.length ? 'col-md-8 col-lg-9' : 'col-12'">
Expand Down
46 changes: 17 additions & 29 deletions ui/src/app/records/search/search.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export class SearchComponent implements OnInit {
public language = null;
public permissions = null;
onInitDone = false;

constructor(
protected recordsService: RecordsService,
protected route: ActivatedRoute,
Expand Down Expand Up @@ -251,35 +252,6 @@ export class SearchComponent implements OnInit {
});
}

aggFilter(term, value) {
const filterValue = `${term}=${value}`;
if (this.isFiltered(term, value)) {
this.aggFilters = this.aggFilters.filter(val => val !== filterValue);
} else {
this.aggFilters.push(filterValue);
}
this.updateRoute();
}

isFiltered(term, value?) {
if (value) {
const filterValue = `${term}=${value}`;
return this.aggFilters.some(val => filterValue === val);
} else {
return this.aggFilters.some(val => term === val.split('=')[0]);
}
}

startOpen(title: string) {
if (this.isFiltered(title)) {
return true;
}
if (this.aggsSettings.expand.some(value => value === title)) {
return true;
}
return false;
}

hasPermissionToCreate() {
if (this.permissions
&& this.permissions.cannot_create
Expand All @@ -288,4 +260,20 @@ export class SearchComponent implements OnInit {
}
return true;
}

removeAggFilter(event: any) {
this.aggFilters = this.aggFilters.filter(
val => val !== this.formatFilterValue(event)
);
this.updateRoute();
}

addAggFilter(event: any) {
this.aggFilters.push(this.formatFilterValue(event));
this.updateRoute();
}

formatFilterValue(object: {term: string, value: string}) {
return `${object.term}=${object.value}`;
}
}
6 changes: 4 additions & 2 deletions ui/src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,5 +243,7 @@
"Documents": "Documents",
"Persons": "Persons",
"results": "Resultate",
"No result found.": "Keine Resultate gefunden"
}
"No result found.": "Keine Resultate gefunden",
"more…": "Weiteres…",
"less…": "Weniger…"
}
Loading

0 comments on commit b85e5b5

Please sign in to comment.