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

statistics: add tests, query and csv download button for librarian statistics #2661

Merged
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
158 changes: 92 additions & 66 deletions rero_ils/modules/stats/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import arrow
from dateutil.relativedelta import relativedelta
from flask import current_app
from flask_login import current_user
from invenio_search.api import RecordsSearch

from .models import StatIdentifier, StatMetadata
Expand All @@ -36,8 +35,9 @@
from ..loans.logs.api import LoanOperationLog
from ..locations.api import LocationsSearch
from ..minters import id_minter
from ..patrons.api import PatronsSearch
from ..patrons.api import Patron, PatronsSearch, current_librarian
from ..providers import Provider
from ..utils import extracted_data_from_ref

# provider
StatProvider = type(
Expand Down Expand Up @@ -340,75 +340,67 @@ def get_librarian_library_pids(cls):

Note: for system_librarian includes libraries of organisation.
"""
patron_search = PatronsSearch()\
.filter('term', user_id=current_user.id)\
.source(['libraries', 'roles', 'organisation']).scan()
patron_data = next(patron_search)

library_pids = set()
if 'librarian' in patron_data.roles:
for library_pid in patron_data.libraries:
library_pids.add(library_pid.pid)

if Patron.ROLE_LIBRARIAN in current_librarian["roles"]:
library_pids = set([extracted_data_from_ref(lib)
for lib in current_librarian
.get('libraries', [])])
# case system_librarian: add libraries of organisation
if 'system_librarian' in patron_data.roles:
organisation_libraries_pid = LibrariesSearch()\
.filter('term',
organisation__pid=patron_data.organisation.pid)\
if Patron.ROLE_SYSTEM_LIBRARIAN in current_librarian["roles"]:
patron_organisation = current_librarian.get_organisation()
libraries_search = LibrariesSearch()\
.filter('term', organisation__pid=patron_organisation.pid)\
.source(['pid']).scan()
for s in organisation_libraries_pid:
library_pids.add(s.pid)
library_pids = library_pids.union(
set([s.pid for s in libraries_search])
)
return list(library_pids)

@classmethod
def get_librarian_libraries(cls):
"""Get libraries of librarian."""
library_pids = cls.get_librarian_library_pids()
return LibrariesSearch().filter('terms', pid=library_pids)\
.source(['pid', 'name', 'organisation']).scan()

def collect(self, **kwargs):
def collect(self):
"""Compute statistics for librarian."""
stats = []
if 'libraries' in kwargs:
libraries = kwargs.get("libraries")
else:
libraries = self.get_all_libraries()
libraries = self.get_all_libraries()
libraries_map = {lib.pid: lib.name for lib in libraries}

for lib in libraries:
stats.append({
'library': {
'pid': lib.pid,
'name': lib.name
},
'number_of_loans_by_transaction_library':
self.number_of_loans_by_transaction_library(lib.pid,
['checkout']),
'number_of_loans_for_items_in_library':
self.number_of_loans_for_items_in_library(lib.pid,
['checkout']),
'number_of_patrons_by_postal_code':
self.number_of_patrons_by_postal_code(lib.pid,
['request',
'checkin',
'checkout']),
'number_of_new_patrons_by_postal_code':
self.number_of_new_patrons_by_postal_code(lib.pid,
['request',
'checkin',
'checkout']),
'number_of_new_documents':
self.number_of_new_documents(lib.pid),
'number_of_new_items':
'checkouts_for_transaction_library':
self.checkouts_for_transaction_library(lib.pid,
['checkout']),
'checkouts_for_owning_library':
self.checkouts_for_owning_library(lib.pid,
['checkout']),
'active_patrons_by_postal_code':
self.active_patrons_by_postal_code(lib.pid,
['request',
'checkin',
'checkout']),
'new_active_patrons_by_postal_code':
self.new_active_patrons_by_postal_code(lib.pid,
['request',
'checkin',
'checkout']),
'new_documents':
self.new_documents(lib.pid),
'new_items':
self.number_of_new_items(lib.pid),
'number_of_extended_items':
self.number_of_extended_items(lib.pid, ['extend']),
'number_of_validated_requests':
self.number_of_validated_requests(lib.pid, ['validate']),
'number_of_items_by_document_type_and_subtype':
self.number_of_items_by_document_type_subtype(lib.pid),
'number_of_new_items_by_location':
self.number_of_new_items_by_location(lib.pid)
'renewals':
self.renewals(lib.pid, ['extend']),
'validated_requests':
self.validated_requests(lib.pid, ['validate']),
'items_by_document_type_and_subtype':
self.items_by_document_type_and_subtype(lib.pid),
'new_items_by_location':
self.new_items_by_location(lib.pid),
'loans_of_transaction_library_by_item_location':
self.loans_of_transaction_library_by_item_location(
libraries_map,
lib.pid,
['checkin',
'checkout'])
})
return stats

Expand Down Expand Up @@ -436,7 +428,7 @@ def _get_location_code_name(self, location_pid):
location = next(location_search)
return f'{location.code} - {location.name}'

def number_of_loans_by_transaction_library(self, library_pid, trigger):
def checkouts_for_transaction_library(self, library_pid, trigger):
"""Number of circulation operation during the specified timeframe.

Number of loans of items when transaction location is equal to
Expand All @@ -454,7 +446,7 @@ def number_of_loans_by_transaction_library(self, library_pid, trigger):
.filter('terms', loan__transaction_location__pid=location_pids)\
.count()

def number_of_loans_for_items_in_library(self, library_pid, trigger):
def checkouts_for_owning_library(self, library_pid, trigger):
"""Number of circulation operation during the specified timeframe.

Number of loans of items per library when the item is owned by
Expand All @@ -470,7 +462,7 @@ def number_of_loans_for_items_in_library(self, library_pid, trigger):
.filter('term', loan__item__library_pid=library_pid)\
.count()

def number_of_patrons_by_postal_code(self, library_pid, trigger):
def active_patrons_by_postal_code(self, library_pid, trigger):
"""Number of circulation operation during the specified timeframe.

Number of patrons per library and CAP when transaction location
Expand Down Expand Up @@ -507,7 +499,7 @@ def number_of_patrons_by_postal_code(self, library_pid, trigger):
patron_pids.add(patron_pid)
return stats

def number_of_new_patrons_by_postal_code(self, library_pid, trigger):
def new_active_patrons_by_postal_code(self, library_pid, trigger):
"""Number of circulation operation during the specified timeframe.

Number of new patrons per library and CAP when transaction location
Expand Down Expand Up @@ -555,7 +547,7 @@ def number_of_new_patrons_by_postal_code(self, library_pid, trigger):

return stats

def number_of_new_documents(self, library_pid):
def new_documents(self, library_pid):
"""Number of new documents per library for given time interval.

:param library_pid: string - the library to filter with
Expand All @@ -569,7 +561,7 @@ def number_of_new_documents(self, library_pid):
.filter('term', library__value=library_pid)\
.count()

def number_of_extended_items(self, library_pid, trigger):
def renewals(self, library_pid, trigger):
"""Number of items with loan extended.

Number of items with loan extended per library for given time interval
Expand All @@ -584,7 +576,7 @@ def number_of_extended_items(self, library_pid, trigger):
.filter('term', loan__item__library_pid=library_pid)\
.count()

def number_of_validated_requests(self, library_pid, trigger):
def validated_requests(self, library_pid, trigger):
"""Number of validated requests.

Number of validated requests per library for given time interval
Expand All @@ -601,7 +593,7 @@ def number_of_validated_requests(self, library_pid, trigger):
.filter('term', library__value=library_pid)\
.count()

def number_of_new_items_by_location(self, library_pid):
def new_items_by_location(self, library_pid):
"""Number of new items per library by location.

Note: items created and then deleted during the time interval
Expand All @@ -623,7 +615,7 @@ def number_of_new_items_by_location(self, library_pid):
stats[location_code_name] = bucket.doc_count
return stats

def number_of_items_by_document_type_subtype(self, library_pid):
def items_by_document_type_and_subtype(self, library_pid):
"""Number of items per library by document type and sub-type.

Note: if item has more than one doc type/subtype the item is counted
Expand All @@ -650,6 +642,40 @@ def number_of_items_by_document_type_subtype(self, library_pid):
stats[bucket.key] = bucket.doc_count
return stats

def loans_of_transaction_library_by_item_location(self,
libraries_map,
library_pid,
trigger):
"""Number of circulation operation during the specified timeframe.

Number of loans of items by location when transaction location
is equal to any of the library locations
:param libraries_map: dict - map of library pid and name
:param library_pid: string - the library to filter with
:param trigger: string - action name (checkin, checkout)
:return: the number of matched circulation operation
:rtype: dict
"""
location_pids = self._get_locations_pid(library_pid)
search = RecordsSearch(index=LoanOperationLog.index_name)\
.filter('range', date=self.date_range)\
.filter('terms', loan__trigger=trigger)\
.filter('terms', loan__transaction_location__pid=location_pids)\
.source('loan').scan()

stats = {}
for s in search:
item_library_pid = s.loan.item.library_pid
item_library_name = libraries_map[item_library_pid]
location_name = s.loan.item.holding.location_name

key = f'{item_library_pid}: {item_library_name} - {location_name}'
stats.setdefault(key, {'location_name': location_name,
'checkin': 0, 'checkout': 0})
stats[key][s.loan.trigger] += 1

return stats


class Stat(IlsRecord):
"""ItemType class."""
Expand Down
15 changes: 11 additions & 4 deletions rero_ils/modules/stats/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,27 @@ def collect(type):
@stats.command('collect_year')
@click.argument('year', type=int)
@click.argument('timespan', default='yearly')
@click.option('--n_months', default=12)
@click.option('-f', '--force', is_flag=True, default=False)
@with_appcontext
def collect_year(year, timespan, force):
def collect_year(year, timespan, n_months, force):
"""Extract the stats librarian for one year and store them in db.

:param year: year of statistics
:param n_months: month up to which the statistics are calculated
:param timespan: time interval, can be 'montly' or 'yearly'
:param force: force update of stat.
"""
stat_pid = None
type = 'librarian'
if year:
if timespan == 'montly':
for month in range(1, 13):
if n_months not in range(1, 13):
click.secho(f'ERROR: not a valid month', fg='red')
raise click.Abort()
n_months += 1

for month in range(1, n_months):
first_day = f'{year}-{month:02d}-01T23:59:59'\
.format(fmt='YYYY-MM-DDT23:59:59')
first_day = arrow.get(first_day, 'YYYY-MM-DDTHH:mm:ss')
Expand All @@ -115,7 +122,7 @@ def collect_year(year, timespan, force):
click.secho(
f'ERROR: statistics of type {type}\
for time interval {_from} - {_to}\
already exists. Pid: {stat_pid}', fg='red')
already exist. Pid: {stat_pid}', fg='red')
return

stat_data = dict(type=type, date_range=date_range,
Expand Down Expand Up @@ -154,7 +161,7 @@ def collect_year(year, timespan, force):
click.secho(
f'ERROR: statistics of type {type}\
for time interval {_from} - {_to}\
already exists. Pid: {stat_pid}', fg='red')
already exist. Pid: {stat_pid}', fg='red')
return

stat_data = dict(type=type, date_range=date_range,
Expand Down
10 changes: 7 additions & 3 deletions rero_ils/modules/stats/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def read(cls, user, record):
if librarian_permission.require().can():
if 'type' not in record or record['type'] != 'librarian':
return False
record = filter_stat_by_librarian(current_user, record)
record = filter_stat_by_librarian(record)

return admin_permission.require().can() \
or monitoring_permission.require().can() \
Expand Down Expand Up @@ -105,8 +105,12 @@ def stats_ui_permission_factory(record, *args, **kwargs):
action='read', record=record, cls=StatPermission)


def filter_stat_by_librarian(current_user, record):
"""Filter data for libraries of specific user."""
def filter_stat_by_librarian(record):
"""Filter data for libraries of specific librarian/system_librarian.

:param record: statistics to check.
:return: statistics filtered by libraries.
"""
library_pids = StatsForLibrarian.get_librarian_library_pids()
record['values'] = list(filter(lambda l: l['library']['pid'] in
library_pids, record['values']))
Expand Down
Loading