Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Us 2721 performance #3447

Merged
merged 4 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,10 @@ rero-ils = "rero_ils.modules.ext:REROILSAPP"
acq_accounts = "rero_ils.modules.acquisition.acq_accounts.views:api_blueprint"
acq_orders = "rero_ils.modules.acquisition.acq_orders.views:api_blueprint"
acq_receipts = "rero_ils.modules.acquisition.acq_receipts.views:api_blueprint"
api_documents = "rero_ils.modules.documents.views:api_blueprint"
documents = "rero_ils.modules.documents.api_views:api_blueprint"
circ_policies = "rero_ils.modules.circ_policies.views:blueprint"
local_entities = "rero_ils.modules.entities.local_entities.views:api_blueprint"
remote_entities = "rero_ils.modules.entities.remote_entities.views:api_blueprint"
documents = "rero_ils.modules.documents.views:api_blueprint"
holdings = "rero_ils.modules.holdings.api_views:api_blueprint"
imports = "rero_ils.modules.imports.views:api_blueprint"
item_types = "rero_ils.modules.item_types.views:blueprint"
Expand Down
136 changes: 117 additions & 19 deletions rero_ils/modules/documents/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,27 +151,125 @@ def _validate(self, **kwargs):
return json

@classmethod
def is_available(cls, pid, view_code, raise_exception=False):
"""Get availability for document."""
from ..holdings.api import Holding
def get_n_available_holdings(cls, pid, org_pid=None):
"""Get the number of available and electronic holdings.

:param pid: str - the document pid.
:param org_pid: str - the organisation pid.
:returns: int, int - the number of available and electronic holdings.
"""
from rero_ils.modules.holdings.api import HoldingsSearch

# create the holding search query
holding_query = HoldingsSearch().available_query()

# filter by the current document
filters = Q('term', document__pid=pid)

# filter by organisation
if org_pid:
filters &= Q('term', organisation__pid=org_pid)
holding_query = holding_query.filter(filters)

# get the number of electronic holdings
n_electronic_holdings = holding_query\
.filter('term', holdings_type='electronic')

return holding_query.count(), n_electronic_holdings.count()

@classmethod
def get_available_item_pids(cls, pid, org_pid=None):
"""Get the list of the available item pids.

:param pid: str - the document pid.
:param org_pid: str - the organisation pid.
:returns: [str] - the list of the available item pids.
"""
from rero_ils.modules.items.api import ItemsSearch

# create the item query
items_query = ItemsSearch().available_query()

# filter by the current document
filters = Q('term', document__pid=pid)

# filter by organisation
if org_pid:
filters &= Q('term', organisation__pid=org_pid)

return [
hit.pid for hit in items_query.filter(filters).source('pid').scan()
]

@classmethod
def get_item_pids_with_active_loan(cls, pid, org_pid=None):
"""Get the list of items pids that have active loans.

:param pid: str - the document pid.
:param org_pid: str - the organisation pid.
:returns: [str] - the list of the item pids having active loans.
"""
from rero_ils.modules.loans.api import LoansSearch

loan_query = LoansSearch().unavailable_query()

# filter by the current document
filters = Q('term', document_pid=pid)

# filter by organisation
if org_pid:
filters &= Q('term', organisation__pid=org_pid)

loan_query = loan_query.filter(filters)

return [
hit.item_pid.value for hit in loan_query.source('item_pid').scan()
]

@classmethod
def is_available(cls, pid, view_code=None):
"""Get availability for document.

Note: if the logic has to be changed here please check also for items
and holdings availability.

:param pid: str - document pid value.
:param view_code: str - the view code.
"""
# get the organisation pid corresponding to the view code
org_pid = None
if view_code != current_app.config.get(
'RERO_ILS_SEARCH_GLOBAL_VIEW_CODE'):
view_id = Organisation.get_record_by_viewcode(view_code)['pid']
holding_pids = Holding.get_holdings_pid_by_document_pid_by_org(
pid, view_id)
else:
holding_pids = Holding.get_holdings_pid_by_document_pid(pid)
for holding_pid in holding_pids:
if holding := Holding.get_record_by_pid(holding_pid):
if holding.available:
return True
else:
msg = f'No holding: {holding_pid} in DB ' \
f'for document: {pid}'
current_app.logger.error(msg)
if raise_exception:
raise ValueError(msg)
return False
org_pid = Organisation.get_record_by_viewcode(view_code)['pid']

# -------------- Holdings --------------------
# get the number of available and electronic holdings
n_available_holdings, n_electronic_holdings = \
cls.get_n_available_holdings(pid, org_pid)

# available if an electronic holding exists
if n_electronic_holdings:
return True

# unavailable if no holdings exists
if not n_available_holdings:
return False

# -------------- Items --------------------
# get the available item pids
available_item_pids = cls.get_available_item_pids(pid, org_pid)

# unavailable if no items exists
if not available_item_pids:
return False

# --------------- Loans -------------------
# get item pids that have active loans
unavailable_item_pids = \
cls.get_item_pids_with_active_loan(pid, org_pid)

# available if at least one item don't have active loan
return bool(set(available_item_pids) - set(unavailable_item_pids))

@property
def harvested(self):
Expand Down
52 changes: 52 additions & 0 deletions rero_ils/modules/documents/api_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
#
# RERO ILS
# Copyright (C) 2019-2023 RERO
# Copyright (C) 2019-2023 UCLouvain
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Blueprint for document api."""

from flask import Blueprint, abort, jsonify
from flask import request as flask_request

from .api import Document
from .utils import get_remote_cover
from ..utils import cached

api_blueprint = Blueprint(
'api_documents',
__name__,
url_prefix='/document'
)


@api_blueprint.route('/cover/<isbn>')
@cached(timeout=300, query_string=True)
def cover(isbn):
"""Document cover service."""
return jsonify(get_remote_cover(isbn))


@api_blueprint.route('/<pid>/availability', methods=['GET'])
def document_availability(pid):
"""HTTP GET request for document availability."""
if not Document.record_pid_exists(pid):
abort(404)
view_code = flask_request.args.get('view_code')
if not view_code:
view_code = 'global'
return jsonify({
'available': Document.is_available(pid, view_code)
})
16 changes: 13 additions & 3 deletions rero_ils/modules/documents/serializers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from flask import current_app, json, request, stream_with_context
from werkzeug.local import LocalProxy

from rero_ils.modules.documents.api import Document
from rero_ils.modules.documents.utils import process_i18n_literal_fields
from rero_ils.modules.documents.views import create_title_alternate_graphic, \
create_title_responsibilites, create_title_variants
Expand All @@ -31,6 +30,7 @@
from rero_ils.modules.serializers import JSONSerializer

from ..dumpers import document_replace_refs_dumper
from ..dumpers.indexer import IndexerDumper
from ..extensions import TitleExtension

GLOBAL_VIEW_CODE = LocalProxy(lambda: current_app.config.get(
Expand All @@ -54,6 +54,7 @@ def preprocess_record(self, pid, record, links_factory=None, **kwargs):
"""Prepare a record and persistent identifier for serialization."""
rec = record

# TODO: uses dumpers
# build responsibility data for display purpose
responsibility_statement = rec.get('responsibilityStatement', [])
if responsibilities := create_title_responsibilites(
Expand All @@ -71,16 +72,25 @@ def preprocess_record(self, pid, record, links_factory=None, **kwargs):

if variant_titles := create_title_variants(titles):
rec['ui_title_variants'] = variant_titles
return super().preprocess_record(

data = super().preprocess_record(
pid=pid, record=rec, links_factory=links_factory, kwargs=kwargs)
metadata = data['metadata']
resolve = request.args.get(
'resolve',
default=False,
type=lambda v: v.lower() in ['true', '1']
)
if request and resolve:
IndexerDumper()._process_host_document(None, metadata)
return data

def _postprocess_search_hit(self, hit: dict) -> None:
"""Post-process each hit of a search result."""
view_id, view_code = DocumentJSONSerializer._get_view_information()
metadata = hit.get('metadata', {})
pid = metadata.get('pid')

metadata['available'] = Document.is_available(pid, view_code)
titles = metadata.get('title', [])
if text_title := TitleExtension.format_text(
titles, with_subtitle=False
Expand Down
45 changes: 12 additions & 33 deletions rero_ils/modules/documents/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@

import click
from elasticsearch_dsl.query import Q
from flask import Blueprint, abort, current_app, jsonify, render_template
from flask import request as flask_request
from flask import url_for
from flask import Blueprint, current_app, render_template, url_for
from flask_babelex import gettext as _
from flask_login import current_user
from invenio_records_ui.signals import record_viewed
Expand All @@ -41,14 +39,24 @@
from rero_ils.modules.locations.api import Location
from rero_ils.modules.organisations.api import Organisation
from rero_ils.modules.patrons.api import current_patrons
from rero_ils.modules.utils import cached, extracted_data_from_ref
from rero_ils.modules.utils import extracted_data_from_ref

from .api import Document, DocumentsSearch
from .extensions import EditionStatementExtension, \
ProvisionActivitiesExtension, SeriesStatementExtension, TitleExtension
from .utils import display_alternate_graphic_first, get_remote_cover, \
title_format_text, title_format_text_alternate_graphic, \
title_variant_format_text
from ..collections.api import CollectionsSearch
from ..entities.api import Entity
from ..entities.models import EntityType
from ..holdings.models import HoldingNoteTypes
from ..items.models import ItemCirculationAction
from ..libraries.api import Library
from ..locations.api import Location
from ..organisations.api import Organisation
from ..patrons.api import current_patrons
from ..utils import extracted_data_from_ref


def doc_item_view_method(pid, record, template=None, **kwargs):
Expand All @@ -69,8 +77,6 @@ def doc_item_view_method(pid, record, template=None, **kwargs):
if viewcode != current_app.config.get('RERO_ILS_SEARCH_GLOBAL_VIEW_CODE'):
organisation = Organisation.get_record_by_viewcode(viewcode)

record['available'] = Document.is_available(record.pid, viewcode)

# build provision activity
ProvisionActivitiesExtension().post_dump(record={}, data=record)

Expand Down Expand Up @@ -104,19 +110,6 @@ def doc_item_view_method(pid, record, template=None, **kwargs):
)


api_blueprint = Blueprint(
'api_documents',
__name__
)


@api_blueprint.route('/cover/<isbn>')
@cached(timeout=300, query_string=True)
def cover(isbn):
"""Document cover service."""
return jsonify(get_remote_cover(isbn))


blueprint = Blueprint(
'documents',
__name__,
Expand Down Expand Up @@ -486,20 +479,6 @@ def work_access_point(work_access_point):
return wap


@api_blueprint.route('/availabilty/<document_pid>', methods=['GET'])
def document_availability(document_pid):
"""HTTP GET request for document availability."""
view_code = flask_request.args.get('view_code')
if not view_code:
view_code = 'global'
document = Document.get_record_by_pid(document_pid)
if not document:
abort(404)
return jsonify({
'availability': Document.is_available(document_pid, view_code)
})


@blueprint.app_template_filter()
def create_publication_statement(provision_activity):
"""Create publication statement from place, agent and date values."""
Expand Down
Loading