diff --git a/.gitignore b/.gitignore index 8c0960f5d..3e66c5a41 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__/ # Idea software family .idea/ +.vscode/ # C extensions *.so diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index cfdb08c7b..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.pythonPath": "/usr/bin/python3.6" -} \ No newline at end of file diff --git a/invenio_app_ils/config.py b/invenio_app_ils/config.py index 2e1fbd51b..a3dc03a62 100644 --- a/invenio_app_ils/config.py +++ b/invenio_app_ils/config.py @@ -1141,10 +1141,10 @@ def _(x): ), ), filters=dict( - circulation=default_value_when_missing_filter("circulation.state", - "NOT_ON_LOAN"), ), post_filters=dict( + circulation=default_value_when_missing_filter( + "circulation.state", "NOT_ON_LOAN"), status=terms_filter("status"), medium=terms_filter("medium"), restrictions=terms_filter("circulation_restriction"), @@ -1187,19 +1187,17 @@ def _(x): ) ), ), - filters={ + post_filters={ + "state": terms_filter("state"), + "delivery": terms_filter("delivery.method"), + "availability": keyed_range_filter( + "document.circulation.has_items_for_loan", + {"Available for loan": {"gt": 0}}, + ), "returns.end_date": overdue_loans_filter("end_date"), "loans_from_date": date_range_filter("start_date", "gte"), "loans_to_date": date_range_filter("start_date", "lte"), }, - post_filters=dict( - state=terms_filter("state"), - delivery=terms_filter("delivery.method"), - availability=keyed_range_filter( - "document.circulation.has_items_for_loan", - {"Available for loan": {"gt": 0}}, - ), - ), ), acq_orders=dict( # OrderSearch.Meta.index aggs=dict( diff --git a/invenio_app_ils/facets.py b/invenio_app_ils/facets.py index 6566d5854..0ee2c6225 100644 --- a/invenio_app_ils/facets.py +++ b/invenio_app_ils/facets.py @@ -121,5 +121,9 @@ def date_range_filter(field, comparator): :param comparator: Comparison we want with the supplied date. """ def inner(values): - return Range(**{field: {comparator: str(arrow.get(values[0]).date())}}) + try: + input_date = str(arrow.get(values[0]).date()) + except arrow.parser.ParserError as e: + raise ValueError("Input should be a date") + return Range(**{field: {comparator: input_date}}) return inner diff --git a/tests/api/test_facets.py b/tests/api/test_facets.py index e824b1d7b..b9160f05f 100644 --- a/tests/api/test_facets.py +++ b/tests/api/test_facets.py @@ -12,11 +12,13 @@ from datetime import timedelta import arrow +import pytest from elasticsearch_dsl.query import Bool, Q, Range, Terms from flask import current_app -from invenio_app_ils.facets import default_value_when_missing_filter, \ - keyed_range_filter, overdue_loans_filter +from invenio_app_ils.facets import date_range_filter, \ + default_value_when_missing_filter, keyed_range_filter, \ + overdue_loans_filter def test_keyed_range_filter(): @@ -34,7 +36,6 @@ def test_keyed_range_filter(): def test_current_ranged_loans_filter(app): """Test ranged current loans filter.""" - with app.app_context(): rfilter = overdue_loans_filter("field") @@ -57,3 +58,18 @@ def test_default_value_when_missing_filter(app): assert rfilter("test") == Terms(field="test") assert rfilter("missing val") == Bool( **{'must_not': {'exists': {'field': "field"}}}) + + +@pytest.mark.parametrize("input_date", ["", "a string", "2020-02-02"]) +def test_date_range_filter(app, input_date): + """Test date range filter date validation and query.""" + from_filter = date_range_filter("field", "gte") + to_filter = date_range_filter("field", "lte") + + try: + assert from_filter([input_date]) == Range(field={"gte": input_date}) + assert to_filter([input_date]) == Range(field={"lte": input_date}) + except: + with pytest.raises(ValueError) as err: + from_filter([input_date]) + to_filter([input_date]) diff --git a/ui/package-lock.json b/ui/package-lock.json index c6e5154ae..8ebe9d7cb 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -3205,12 +3205,11 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "axios": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", - "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" + "follow-redirects": "1.5.10" } }, "axios-mock-adapter": { @@ -8098,11 +8097,6 @@ "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", "dev": true }, - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" - }, "is-callable": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", diff --git a/ui/package.json b/ui/package.json index fe75c1e19..af42b91b7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,7 +11,7 @@ "format": "prettier --config ./.prettierrc --ignore-path ./.prettierignore --write \"**/*.js\"" }, "dependencies": { - "axios": "^0.19.0", + "axios": "^0.19.2", "extract-text-webpack-plugin": "^3.0.2", "formik": "^2.0.6", "less": "^3.10.3", diff --git a/ui/src/components/SearchControls/__tests__/SearchControls.test.js b/ui/src/components/SearchControls/__tests__/SearchControls.test.js index bd7e208ca..0d8e8734a 100644 --- a/ui/src/components/SearchControls/__tests__/SearchControls.test.js +++ b/ui/src/components/SearchControls/__tests__/SearchControls.test.js @@ -30,8 +30,10 @@ beforeEach(() => { }); const searchApi = new InvenioSearchApi({ - url: documentApi.searchBaseURL, - withCredentials: true, + axios: { + url: documentApi.searchBaseURL, + withCredentials: true, + }, }); describe('SearchControls tests', () => { diff --git a/ui/src/components/SearchControls/__tests__/__snapshots__/SearchControls.test.js.snap b/ui/src/components/SearchControls/__tests__/__snapshots__/SearchControls.test.js.snap index b5c9eb485..66ce4764e 100644 --- a/ui/src/components/SearchControls/__tests__/__snapshots__/SearchControls.test.js.snap +++ b/ui/src/components/SearchControls/__tests__/__snapshots__/SearchControls.test.js.snap @@ -101,16 +101,23 @@ exports[`SearchControls tests should mount SearchControls component 1`] = ` } > { diff --git a/ui/src/pages/backoffice/Acquisition/Vendor/VendorSearch/VendorSearch.js b/ui/src/pages/backoffice/Acquisition/Vendor/VendorSearch/VendorSearch.js index 41a323a95..467d5a451 100644 --- a/ui/src/pages/backoffice/Acquisition/Vendor/VendorSearch/VendorSearch.js +++ b/ui/src/pages/backoffice/Acquisition/Vendor/VendorSearch/VendorSearch.js @@ -29,8 +29,10 @@ import history from '@history'; export class VendorSearch extends Component { searchApi = new InvenioSearchApi({ - url: vendorApi.searchBaseURL, - withCredentials: true, + axios: { + url: vendorApi.searchBaseURL, + withCredentials: true, + }, }); searchConfig = getSearchConfig('vendors'); diff --git a/ui/src/pages/backoffice/Document/DocumentSearch/DocumentSearch.js b/ui/src/pages/backoffice/Document/DocumentSearch/DocumentSearch.js index 38fcbd5ad..e596810bf 100644 --- a/ui/src/pages/backoffice/Document/DocumentSearch/DocumentSearch.js +++ b/ui/src/pages/backoffice/Document/DocumentSearch/DocumentSearch.js @@ -30,8 +30,10 @@ import history from '@history'; export class DocumentSearch extends Component { searchApi = new InvenioSearchApi({ - url: documentApi.searchBaseURL, - withCredentials: true, + axios: { + url: documentApi.searchBaseURL, + withCredentials: true, + }, }); searchConfig = getSearchConfig('documents'); diff --git a/ui/src/pages/backoffice/DocumentRequest/DocumentRequestSearch/DocumentRequestSearch.js b/ui/src/pages/backoffice/DocumentRequest/DocumentRequestSearch/DocumentRequestSearch.js index 24d81d843..5723fc77f 100644 --- a/ui/src/pages/backoffice/DocumentRequest/DocumentRequestSearch/DocumentRequestSearch.js +++ b/ui/src/pages/backoffice/DocumentRequest/DocumentRequestSearch/DocumentRequestSearch.js @@ -29,8 +29,10 @@ import { responseRejectInterceptor } from '@api/base'; export class DocumentRequestSearch extends Component { searchApi = new InvenioSearchApi({ - url: documentRequestApi.searchBaseURL, - withCredentials: true, + axios: { + url: documentRequestApi.searchBaseURL, + withCredentials: true, + }, interceptors: { response: { reject: responseRejectInterceptor }, }, diff --git a/ui/src/pages/backoffice/EItem/EItemSearch/EItemSearch.js b/ui/src/pages/backoffice/EItem/EItemSearch/EItemSearch.js index 26eeed52c..479412636 100644 --- a/ui/src/pages/backoffice/EItem/EItemSearch/EItemSearch.js +++ b/ui/src/pages/backoffice/EItem/EItemSearch/EItemSearch.js @@ -28,8 +28,10 @@ import history from '@history'; export class EItemSearch extends Component { searchApi = new InvenioSearchApi({ - url: eitemApi.searchBaseURL, - withCredentials: true, + axios: { + url: eitemApi.searchBaseURL, + withCredentials: true, + }, interceptors: { response: { reject: responseRejectInterceptor }, }, diff --git a/ui/src/pages/backoffice/ILL/BorrowingRequestSearch/BorrowingRequestSearch.js b/ui/src/pages/backoffice/ILL/BorrowingRequestSearch/BorrowingRequestSearch.js index 35ace6fab..c5c295a55 100644 --- a/ui/src/pages/backoffice/ILL/BorrowingRequestSearch/BorrowingRequestSearch.js +++ b/ui/src/pages/backoffice/ILL/BorrowingRequestSearch/BorrowingRequestSearch.js @@ -29,8 +29,10 @@ import { BorrowingRequestListEntry } from './components'; export class BorrowingRequestSearch extends Component { searchApi = new InvenioSearchApi({ - url: borrowingRequestApi.searchBaseURL, - withCredentials: true, + axios: { + url: borrowingRequestApi.searchBaseURL, + withCredentials: true, + }, }); renderSearchBar = (_, queryString, onInputChange, executeSearch) => { diff --git a/ui/src/pages/backoffice/ILL/LibrarySearch/LibrarySearch.js b/ui/src/pages/backoffice/ILL/LibrarySearch/LibrarySearch.js index d3acb0bd4..e5db5887d 100644 --- a/ui/src/pages/backoffice/ILL/LibrarySearch/LibrarySearch.js +++ b/ui/src/pages/backoffice/ILL/LibrarySearch/LibrarySearch.js @@ -27,8 +27,10 @@ import { LibraryListEntry } from './components/LibraryListEntry'; export class LibrarySearch extends Component { searchApi = new InvenioSearchApi({ - url: libraryApi.searchBaseURL, - withCredentials: true, + axios: { + url: libraryApi.searchBaseURL, + withCredentials: true, + }, }); searchConfig = getSearchConfig('libraries'); diff --git a/ui/src/pages/backoffice/Item/ItemSearch/ItemSearch.js b/ui/src/pages/backoffice/Item/ItemSearch/ItemSearch.js index 688b8f9f5..18200b3a2 100644 --- a/ui/src/pages/backoffice/Item/ItemSearch/ItemSearch.js +++ b/ui/src/pages/backoffice/Item/ItemSearch/ItemSearch.js @@ -25,8 +25,10 @@ import { export class ItemSearch extends Component { searchApi = new InvenioSearchApi({ - url: itemApi.searchBaseURL, - withCredentials: true, + axios: { + url: itemApi.searchBaseURL, + withCredentials: true, + }, interceptors: { response: { reject: responseRejectInterceptor }, }, diff --git a/ui/src/pages/backoffice/Loan/LoanSearch/SearchDateRange.js b/ui/src/pages/backoffice/Loan/LoanSearch/SearchDateRange.js index 7ba7b5ead..a9b3b97fc 100644 --- a/ui/src/pages/backoffice/Loan/LoanSearch/SearchDateRange.js +++ b/ui/src/pages/backoffice/Loan/LoanSearch/SearchDateRange.js @@ -1,10 +1,11 @@ import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { Card } from 'semantic-ui-react'; import { DatePicker } from '@components'; import { withState } from 'react-searchkit'; import _isEmpty from 'lodash/isEmpty'; -class _SearchDateRange extends Component { +export class _SearchDateRange extends Component { getCurrentDates() { const { filters } = this.props.currentQueryState; let fromDate = ''; @@ -17,17 +18,18 @@ class _SearchDateRange extends Component { return [fromDate, toDate]; } - /** react-searchkit allows having the same filter multiple times. - * For the range dates filters we want each filter one time only so we have - * to remove any pre-existing filters with the same name + /** react-searchkit allows having the same filter multiple times with + * different values. For this range dates filters, we want each filter to + * appear only one time with one value (e.g. loan_start_date = ``) */ onDateChange = newFilter => { + const { currentQueryState, updateQueryState } = this.props; const [name, value] = newFilter; - let filters = newFilter; - // If value is empty we simply remove the filter otherwise if we have - // value we remove the filter and add the new one. - if (!_isEmpty(value)) filters = [[name, ''], newFilter]; - return this.props.updateQueryState({ filters: filters }); + const filters = currentQueryState.filters.filter( + filter => filter[0] === name + ); + if (!_isEmpty(value)) filters.push(newFilter); + return updateQueryState({ filters: filters }); }; render() { @@ -67,3 +69,8 @@ class _SearchDateRange extends Component { } export const SearchDateRange = withState(_SearchDateRange); + +_SearchDateRange.propTypes = { + currentQueryState: PropTypes.object.isRequired, + updateQueryState: PropTypes.func.isRequired, +}; diff --git a/ui/src/pages/backoffice/Loan/LoanSearch/__tests__/SearchDateRange.test.js b/ui/src/pages/backoffice/Loan/LoanSearch/__tests__/SearchDateRange.test.js new file mode 100644 index 000000000..9bb580e08 --- /dev/null +++ b/ui/src/pages/backoffice/Loan/LoanSearch/__tests__/SearchDateRange.test.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { _SearchDateRange as SearchDateRange } from '../SearchDateRange'; + +/** _SearchDateRange is the wrapper before we wrap with HOC `withState` */ + +jest.mock('@components/DatePicker', () => { + return { + DatePicker: () => null, + }; +}); + +const newFilter = ['loans_from_date', '2020-02-02']; +const mockCurrentQueryState = { filters: [] }; +const mockUpdateQueryState = jest.fn(); + +let wrapper; + +beforeEach(() => { + mockUpdateQueryState.mockClear(); +}); + +describe('SearchDateRange tests', () => { + it('should render the SearchDateRange', () => { + wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('should call the update query with initial filter value', () => { + wrapper = shallow( + + ); + + wrapper.instance().onDateChange(newFilter); + const expected = { filters: [['loans_from_date', '2020-02-02']] }; + const mockFn = wrapper.instance().props.updateQueryState; + expect(mockFn).toBeCalledWith(expected); + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + it('should replace the existing value in loans_from_date', () => { + mockCurrentQueryState.filters = [['loans_from_date', '2020-02-01']]; + + wrapper = shallow( + + ); + wrapper.instance().onDateChange(newFilter); + const expected = { + filters: [ + ['loans_from_date', '2020-02-01'], + ['loans_from_date', '2020-02-02'], + ], + }; + const mockFn = wrapper.instance().props.updateQueryState; + expect(mockFn).toBeCalledWith(expected); + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + it('should leave the previous filters unaffected and add the new one', () => { + mockCurrentQueryState.filters = [['loans_to_date', '2020-03-03']]; + wrapper = shallow( + + ); + wrapper.instance().onDateChange(newFilter); + const expected = { filters: [newFilter] }; + const mockFn = wrapper.instance().props.updateQueryState; + expect(mockFn).toBeCalledWith(expected); + expect(mockFn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/src/pages/backoffice/Loan/LoanSearch/__tests__/__snapshots__/SearchDateRange.test.js.snap b/ui/src/pages/backoffice/Loan/LoanSearch/__tests__/__snapshots__/SearchDateRange.test.js.snap new file mode 100644 index 000000000..14e304dbf --- /dev/null +++ b/ui/src/pages/backoffice/Loan/LoanSearch/__tests__/__snapshots__/SearchDateRange.test.js.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchDateRange tests should render the SearchDateRange 1`] = ` + + + + Date + + + + *Loan start date + + + + + + + + + + +`; diff --git a/ui/src/pages/backoffice/Patron/PatronSearch/PatronSearch.js b/ui/src/pages/backoffice/Patron/PatronSearch/PatronSearch.js index cd58b5789..1f49281cf 100644 --- a/ui/src/pages/backoffice/Patron/PatronSearch/PatronSearch.js +++ b/ui/src/pages/backoffice/Patron/PatronSearch/PatronSearch.js @@ -29,8 +29,10 @@ import { ExportReactSearchKitResults } from '../../components'; export class PatronSearch extends Component { searchApi = new InvenioSearchApi({ - url: patronApi.searchBaseURL, - withCredentials: true, + axios: { + url: patronApi.searchBaseURL, + withCredentials: true, + }, interceptors: { response: { reject: responseRejectInterceptor }, }, diff --git a/ui/src/pages/backoffice/Series/SeriesSearch/SeriesSearch.js b/ui/src/pages/backoffice/Series/SeriesSearch/SeriesSearch.js index 8a4c48f5b..b79e926c1 100644 --- a/ui/src/pages/backoffice/Series/SeriesSearch/SeriesSearch.js +++ b/ui/src/pages/backoffice/Series/SeriesSearch/SeriesSearch.js @@ -31,8 +31,10 @@ import history from '@history'; export class SeriesSearch extends Component { searchApi = new InvenioSearchApi({ - url: seriesApi.searchBaseURL, - withCredentials: true, + axios: { + url: seriesApi.searchBaseURL, + withCredentials: true, + }, interceptors: { response: { reject: responseRejectInterceptor }, }, diff --git a/ui/src/pages/frontsite/Documents/DocumentsSearch/DocumentsSearch.js b/ui/src/pages/frontsite/Documents/DocumentsSearch/DocumentsSearch.js index b21b6d5e1..29060ac94 100644 --- a/ui/src/pages/frontsite/Documents/DocumentsSearch/DocumentsSearch.js +++ b/ui/src/pages/frontsite/Documents/DocumentsSearch/DocumentsSearch.js @@ -35,8 +35,10 @@ import history from '@history'; export class DocumentsSearch extends Component { searchApi = new InvenioSearchApi({ - url: literatureApi.searchBaseURL, - withCredentials: true, + axios: { + url: literatureApi.searchBaseURL, + withCredentials: true, + }, interceptors: { response: { reject: responseRejectInterceptor }, }, diff --git a/ui/src/pages/frontsite/Documents/DocumentsSearch/DocumentsSearchMobile.js b/ui/src/pages/frontsite/Documents/DocumentsSearch/DocumentsSearchMobile.js index 59fcae909..e7acc1b86 100644 --- a/ui/src/pages/frontsite/Documents/DocumentsSearch/DocumentsSearchMobile.js +++ b/ui/src/pages/frontsite/Documents/DocumentsSearch/DocumentsSearchMobile.js @@ -14,8 +14,10 @@ import { DocumentSearchResultsGrid } from './DocumentSearchResultsGrid'; export class DocumentsSearchMobile extends Component { searchApi = new InvenioSearchApi({ - url: documentApi.searchBaseURL, - withCredentials: true, + axios: { + url: documentApi.searchBaseURL, + withCredentials: true, + }, interceptors: { response: { reject: responseRejectInterceptor }, }, diff --git a/ui/src/pages/frontsite/Documents/DocumentsSearch/__tests__/__snapshots__/DocumentsSearch.test.js.snap b/ui/src/pages/frontsite/Documents/DocumentsSearch/__tests__/__snapshots__/DocumentsSearch.test.js.snap index 9ca0d9a16..a41c3c430 100644 --- a/ui/src/pages/frontsite/Documents/DocumentsSearch/__tests__/__snapshots__/DocumentsSearch.test.js.snap +++ b/ui/src/pages/frontsite/Documents/DocumentsSearch/__tests__/__snapshots__/DocumentsSearch.test.js.snap @@ -3,7 +3,9 @@ exports[`DocumentsSearch tests should load the DocumentsSearch component 1`] = ` diff --git a/ui/src/pages/frontsite/components/Series/SeriesLiteratureSearch/__tests__/__snapshots__/SeriesLiteratureSearch.test.js.snap b/ui/src/pages/frontsite/components/Series/SeriesLiteratureSearch/__tests__/__snapshots__/SeriesLiteratureSearch.test.js.snap index 6432e3d00..6f7766485 100644 --- a/ui/src/pages/frontsite/components/Series/SeriesLiteratureSearch/__tests__/__snapshots__/SeriesLiteratureSearch.test.js.snap +++ b/ui/src/pages/frontsite/components/Series/SeriesLiteratureSearch/__tests__/__snapshots__/SeriesLiteratureSearch.test.js.snap @@ -10,7 +10,9 @@ exports[`SeriesLiteratureSearch tests should load the SeriesLiteratureSearch com )