diff --git a/invenio_circulation/api.py b/invenio_circulation/api.py index 3055c50..85b2e87 100644 --- a/invenio_circulation/api.py +++ b/invenio_circulation/api.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2016 CERN. +# Copyright (C) 2017 CERN. # # Invenio is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public License as @@ -40,7 +40,7 @@ from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy_continuum import version_class -from invenio_circulation.models import ItemStatus +from .models import ItemStatus def check_status(method=None, statuses=None): diff --git a/invenio_circulation/config.py b/invenio_circulation/config.py index 140ad94..eb0ab39 100644 --- a/invenio_circulation/config.py +++ b/invenio_circulation/config.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2016 CERN. +# Copyright (C) 2017 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -51,6 +51,27 @@ 'item_route': '/circulation/items/', 'default_media_type': 'application/json', 'max_result_window': 10000, + }, + 'crcitmrev': { + 'pid_type': 'crcitm', + 'pid_minter': 'circulation_item', + 'pid_fetcher': 'circulation_item', + 'record_class': 'invenio_circulation.api:Item', + 'record_serializers': { + 'application/json': ('invenio_records_rest.serializers' + ':json_v1_response'), + }, + 'search_class': 'invenio_circulation.search:ItemRevisionSearch', + 'search_index': None, + 'search_type': None, + 'search_serializers': { + 'application/json': ('invenio_circulation.serializers' + ':revision_serializer'), + }, + 'list_route': '/circulation/item_revisions/', + 'item_route': '/circulation/item_revisions/', + 'default_media_type': 'application/json', + 'max_result_window': 10000, } } """Basic REST circulation configuration.""" diff --git a/invenio_circulation/search.py b/invenio_circulation/search.py index a550d0a..7d8822f 100644 --- a/invenio_circulation/search.py +++ b/invenio_circulation/search.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2016 CERN. +# Copyright (C) 2017 CERN. # # Invenio is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public License as @@ -23,9 +23,12 @@ # as an Intergovernmental Organization or submit itself to any jurisdiction. """Configuration for circulation search.""" +from elasticsearch_dsl.query import Bool, MultiMatch, Range from invenio_search import RecordsSearch +from .api import Item + class ItemSearch(RecordsSearch): """Default search class.""" @@ -35,3 +38,79 @@ class Meta: index = 'circulation-item' doc_types = None + + +class ItemRevisionSearch(object): + """Search class utilizing `Item.find_by_holding`. + + Since this function doesn't utilize elasticsearch, ItemRevisionSearch + has to mimick certain aspects of `elasticsearch_dsl.Search`. + """ + + class Meta: + """Configuration for circulation search.""" + + index = 'circulation-item' + doc_types = None + + class Results(object): + """Substitution of `elasticsearch_dsl.result.Result.""" + + class Hits(object): + """Wrapper class for the search hits.""" + + def __init__(self, results): + """Constructor to wrap the search results.""" + self.hits = self.Hits() + self.hits.total = len(results) + self.results = results + + def to_dict(self): + """Convert results into a dictionary.""" + return { + 'hits': { + 'hits': self.results, + 'total': self.hits.total + } + } + + def __init__(self): + """Constructor for `elasticsearch_dsl.result.Result substituion. + + Adds dummy `_index` value. + """ + self._index = [''] + self._query = {} + + def query(self, q, *args, **kwargs): + """Set the desired query.""" + if type(q) is not Bool: + q = Bool(must=[q]) + for must in q.must: + if type(must) == MultiMatch: + for field in must.fields: + self._query[field] = must.query + elif type(must) == Range: + for key, value in must._params.items(): + self._query[key] = [value['gte'], value['lte']] + return self + + def __getitem__(self, *args, **kwargs): + """Support slicing of the search results. Currently not implemented.""" + return self + + def params(self, *args, **kwargs): + """Specify query params to be used. Currently not implemented.""" + return self + + def execute(self): + """Execute the search. + + :returns: ItemRevisionSearch.Results + """ + res = [] + for uuid, revision in Item.find_by_holding(**self._query): + item = Item.get_record(uuid) + res.append(item.revisions[revision-2]) + + return self.Results(res) diff --git a/invenio_circulation/serializers.py b/invenio_circulation/serializers.py new file mode 100644 index 0000000..ad9bf7a --- /dev/null +++ b/invenio_circulation/serializers.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2017 CERN. +# +# Invenio 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; either version 2 of the +# License, or (at your option) any later version. +# +# Invenio 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 Invenio; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + +"""Serializers for circulation search.""" +import json + +from invenio_records_rest.serializers import JSONSerializer +from invenio_records_rest.serializers.response import search_responsify +from invenio_records_rest.serializers.schemas.json import RecordSchemaJSONV1 + + +class RevisionSerializer(JSONSerializer): + """JSON serializer for items found by `Item.find_by_holding`.""" + + def serialize_search(self, pid_fetcher, search_result, links=None, + item_links_factory=None): + """Serialize a search result. + + :param search_result: Elasticsearch search result. + :param links: Dictionary of links to add to response. + """ + return json.dumps({ + 'hits': { + 'hits': [hit for hit in search_result['hits']['hits']], + 'total': search_result['hits']['total'], + }, + 'links': links or {}, + 'aggregations': search_result.get('aggregations', {}), + }, **self._format_args()) + + +revision_serializer = search_responsify(RevisionSerializer(RecordSchemaJSONV1), + 'application/json') diff --git a/tests/test_examples_app.py b/tests/test_examples_app.py index b53d7b7..f0c2b45 100644 --- a/tests/test_examples_app.py +++ b/tests/test_examples_app.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2016 CERN. +# Copyright (C) 2017 CERN. # # Invenio is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public License as @@ -83,3 +83,10 @@ def test_example_app(example_app): subprocess.check_output(cmd, shell=True).decode('utf-8') ) assert len(output['hits']['hits']) > 0 + + # item revision search API + cmd = 'curl http://localhost:5000/api/circulation/item_revisions/' + output = json.loads( + subprocess.check_output(cmd, shell=True).decode('utf-8') + ) + assert output and 'hits' in output and 'hits' in output['hits'] diff --git a/tests/test_search_revision.py b/tests/test_search_revision.py new file mode 100644 index 0000000..83cb35f --- /dev/null +++ b/tests/test_search_revision.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2017 CERN. +# +# Invenio 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; either version 2 of the +# License, or (at your option) any later version. +# +# Invenio 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 Invenio; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + + +"""Revision search tests.""" + +import datetime +import json + +import pytest +from elasticsearch_dsl.query import Bool, MultiMatch, Range + +from invenio_circulation.api import Item, ItemStatus, Location +from invenio_circulation.search import ItemRevisionSearch +from invenio_circulation.validators import LoanItemSchema + + +def test_item_revision_search_result(): + search_result = ['result1', 'result2'] + result = ItemRevisionSearch.Results(search_result) + + assert result.hits.total == len(search_result) + assert result.results == search_result + assert result.to_dict() == { + 'hits': { + 'hits': search_result, + 'total': len(search_result) + } + } + + +def test_item_revision_search_query(): + # Testing the initial setup + item_revision_search = ItemRevisionSearch() + + assert item_revision_search._index == [''] + assert item_revision_search._query == {} + + # Testing a MultiMatch query + item_revision_search = ItemRevisionSearch() + mm = MultiMatch(fields=['foo'], query='bar') + + item_revision_search.query(mm) + + assert item_revision_search._query == {'foo': 'bar'} + + # Testing a range query + item_revision_search = ItemRevisionSearch() + r = Range(foo={'gte': 'bar', 'lte': u'baz'}) + + item_revision_search.query(r) + + assert item_revision_search._query == {'foo': ['bar', 'baz']} + + +def test_item_revision_search_execute(app, db): + # Prepare the item + item = Item.create({'foo': 'bar'}) + db.session.commit() + + # Create loan data + la = LoanItemSchema() + la.context['item'] = item + + # Prepare the loan data + tmp = la.load({'user_id': 1}).data + data = la.dump(tmp).data + + # Loan item + item.loan_item(**data) + item.commit() + db.session.commit() + + # Return item + item.return_item() + item.commit() + db.session.commit() + + # Prepare ItemRevisionSearch + item_revision_search = ItemRevisionSearch() + mm = MultiMatch(fields=['user_id'], query=1) + + result = item_revision_search.query(mm).execute() + + assert result.hits.total == 1 + assert result.to_dict() == { + 'hits': { + 'hits': [item], + 'total': 1 + } + } + + +def test_item_revision_search_dummies(): + """Tests necessary dummy implementations.""" + item_revision_search = ItemRevisionSearch() + + assert item_revision_search == item_revision_search[0] + assert item_revision_search == item_revision_search.params() diff --git a/tests/test_serializer_revision.py b/tests/test_serializer_revision.py new file mode 100644 index 0000000..eb75599 --- /dev/null +++ b/tests/test_serializer_revision.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2017 CERN. +# +# Invenio 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; either version 2 of the +# License, or (at your option) any later version. +# +# Invenio 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 Invenio; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + + +"""Revision serializer tests.""" + +import json +import pytest + +from invenio_circulation.serializers import RevisionSerializer + + +def test_serialize_search(): + + val1 = {'_source': {'title': 'test1', 'pid': '1'}, '_id': 'a', + '_version': 1} + val2 = {'_source': {'title': 'test2', 'pid': '2'}, '_id': 'b', + '_version': 1} + + data = json.loads(RevisionSerializer(None).serialize_search( + None, + { + 'hits': { + 'hits': [val1, val2], + 'total': 2, + }, + 'aggregations': {}, + } + )) + + assert data['aggregations'] == {} + assert 'links' in data + assert data['hits'] == { + 'hits': [val1, val2], + 'total': 2, + }