diff --git a/rero_ils/modules/stats/api.py b/rero_ils/modules/stats/api.py index f73e41a4c2..a8dfd251f6 100644 --- a/rero_ils/modules/stats/api.py +++ b/rero_ils/modules/stats/api.py @@ -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 @@ -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( @@ -340,40 +340,26 @@ 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({ @@ -381,34 +367,40 @@ def collect(self, **kwargs): '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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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.""" diff --git a/rero_ils/modules/stats/cli.py b/rero_ils/modules/stats/cli.py index 3f1bdca445..896fd9dc59 100644 --- a/rero_ils/modules/stats/cli.py +++ b/rero_ils/modules/stats/cli.py @@ -83,12 +83,14 @@ 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. """ @@ -96,7 +98,12 @@ def collect_year(year, timespan, force): 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') @@ -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, @@ -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, diff --git a/rero_ils/modules/stats/permissions.py b/rero_ils/modules/stats/permissions.py index cbd527cef3..25a602c691 100644 --- a/rero_ils/modules/stats/permissions.py +++ b/rero_ils/modules/stats/permissions.py @@ -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() \ @@ -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'])) diff --git a/rero_ils/modules/stats/serializers.py b/rero_ils/modules/stats/serializers.py index 1c1e4b0003..f2e73b3fe2 100644 --- a/rero_ils/modules/stats/serializers.py +++ b/rero_ils/modules/stats/serializers.py @@ -22,44 +22,104 @@ from invenio_records_rest.serializers.csv import CSVSerializer, Line from invenio_records_rest.serializers.response import add_link_header -# from invenio_records_rest.serializers.response import record_responsify +from ..patrons.api import Patron class StatCSVSerializer(CSVSerializer): - """Mixin serializing records as JSON.""" + """Process data to write in csv file.""" + + ordered_keys = [ + 'library id', + 'library name', + 'checkouts_for_transaction_library', + 'checkouts_for_owning_library', + 'renewals', + 'validated_requests', + 'active_patrons_by_postal_code', + 'new_active_patrons_by_postal_code', + 'items_by_document_type_and_subtype', + 'new_items', + 'new_items_by_location', + 'new_documents', + 'loans_of_transaction_library_by_item_location' + ] def _format_csv(self, records): """Return the list of records as a CSV string.""" # build a unique list of all records keys as CSV headers assert len(records) == 1 record = records[0] - headers = set(('library name', 'library id')) - for value in record['metadata']['values']: - headers.update([v for v in value.keys() if v != 'library']) - - # write the CSV output in memory - line = Line() - writer = csv.DictWriter(line, fieldnames=sorted(headers)) - writer.writeheader() - yield line.read() - # sort by library name - values = sorted( - record['metadata']['values'], - key=lambda v: v['library']['name'] - ) - for value in values: - library = value['library'] - value['library name'] = library['name'] - value['library id'] = library['pid'] - del value['library'] - for v in value: - if isinstance(value[v], dict): - dict_to_text = '' - for k, m in value[v].items(): - dict_to_text += f'{k} :{m}\r\n' - value[v] = dict_to_text - writer.writerow(value) + + if record['metadata'].get('type') == Patron.ROLE_LIBRARIAN: + # statistics of type librarian + headers = [key.capitalize().replace('_', ' ') + for key in self.ordered_keys] + line = Line() + writer = csv.writer(line) + writer.writerow(headers) + yield line.read() + values = sorted( + record['metadata']['values'], + key=lambda v: v['library']['name'] + ) + + for value in values: + library = value['library'] + value['library name'] = library['name'] + value['library id'] = library['pid'] + del value['library'] + for v in value: + if isinstance(value[v], dict): + dict_to_text = '' + for k, m in value[v].items(): + dict_to_text += f'{k} :{m}\r\n' + value[v] = dict_to_text + value = StatCSVSerializer.sort_dict_by_key(value)[1] + writer.writerow(value) + yield line.read() + else: + # statistics of type billing + headers = set(('library name', 'library id')) + for value in record['metadata']['values']: + headers.update([v for v in value.keys() if v != 'library']) + + # write the CSV output in memory + line = Line() + writer = csv.DictWriter(line, fieldnames=sorted(headers)) + writer.writeheader() yield line.read() + # sort by library name + values = sorted( + record['metadata']['values'], + key=lambda v: v['library']['name'] + ) + + for value in values: + library = value['library'] + value['library name'] = library['name'] + value['library id'] = library['pid'] + del value['library'] + for v in value: + if isinstance(value[v], dict): + dict_to_text = '' + for k, m in value[v].items(): + dict_to_text += f'{k} :{m}\r\n' + value[v] = dict_to_text + writer.writerow(value) + yield line.read() + + @classmethod + def sort_dict_by_key(cls, dictionary): + """Sort dict by dict of keys. + + :param dictionary: dict - an input dictionary + :param ordered_keys: list - the ordered list keys + :returns: a list of tuples + :rtype: list + """ + tuple_list = sorted(dictionary.items(), + key=lambda x: cls.ordered_keys.index(x[0])) + return list(zip(*tuple_list)) def process_dict(self, dictionary): """Transform record dict with nested keys to a flat dict. diff --git a/rero_ils/modules/stats/templates/rero_ils/detailed_view_stats.html b/rero_ils/modules/stats/templates/rero_ils/detailed_view_stats.html index 22ab1a3297..c4ac850115 100644 --- a/rero_ils/modules/stats/templates/rero_ils/detailed_view_stats.html +++ b/rero_ils/modules/stats/templates/rero_ils/detailed_view_stats.html @@ -19,25 +19,73 @@ {%- extends 'rero_ils/page.html' %} {%- block page_body %} {%- block record_body %} -
- {%- if record.pid %} - - {%- endif %} - {%- if 'date_range' in record and 'from' in record.date_range %} - {% set stat_name = record.date_range.to|yearmonthfilter+ - ' (from '+record.date_range.from.split('T')[0]+' to '+record.date_range.to.split('T')[0]+')'%} -

{{_(stat_name)}}

- {% else %} -

{{record.created | string | format_date}}

- {% endif %} -
- - - - - - {%- for head in record['values'][0].keys() %} - {%- if head != 'library'%} + {%- if record.type == 'librarian' %} + {% set values = record['values']|sort_dict_by_library %} +
+ {%- if record.pid %} + + {%- endif %} + {%- if 'date_range' in record and 'from' in record.date_range %} + {% set stat_name = record.date_range.to|yearmonthfilter+ + ' (from '+record.date_range.from.split('T')[0]+' to '+record.date_range.to.split('T')[0]+')'%} +

{{_(stat_name)}}

+ {% else %} +

{{record.created | string | format_date}}

+ {% endif %} +
+
{{_('Library id')}}{{_('Library name')}}
+ + + {% set value = values[0] %} + {% set value = value|process_data %} + {% set value = value|sort_dict_by_key %} + {%- for head in value[0] %} + {% set formatted_head = head[0]|upper+head[1:].replace('_', ' ') %} + + {%- endfor %} + + + + {%- for value in values %} + {% set value = value|process_data %} + {% set value = value|sort_dict_by_key %} + + {%- for val in value[1] %} + {% if val is mapping %} + + {%- else %} + + {%- endif %} + {%- endfor%} + + {%- endfor %} + +
{{_(formatted_head)}}
+ {%- for k in val %} +
  • {{k}}: {{val[k]}}
  • + {%- endfor%} +
    {{val}}
    +
    +
    + {%- else %} +
    + {%- if record.pid %} + + {%- endif %} + {%- if 'date_range' in record and 'from' in record.date_range %} + {% set stat_name = record.date_range.to|yearmonthfilter+ + ' (from '+record.date_range.from.split('T')[0]+' to '+record.date_range.to.split('T')[0]+')'%} +

    {{_(stat_name)}}

    + {% else %} +

    {{record.created | string | format_date}}

    + {% endif %} + {% set values = record['values'] %} +
    + + + + {% set val = values[0]|process_data %} + {%- for head in val.keys() %} {%- if head == 'number_of_patrons_by_postal_code' %} {%- elif head == 'number_of_new_items_by_location' %} @@ -48,17 +96,14 @@

    {{record.created | string | format_date}}

    {% set other = head[0]|upper+head[1:].replace('_', ' ') %} {%- endif %} - {%- endif %} - {%- endfor %} - - - - {%- for val in record['values'] %} - - - - {%- for head in record['values'][0].keys() %} - {%- if head != 'library'%} + {%- endfor %} + + + + {%- for val in values %} + {% set val = val|process_data %} + + {%- for head in val.keys() %} {% if val[head] is mapping %} {%- endif %} - {%- endif %} - {%- endfor%} - - {%- endfor %} - -
    {{_('Postal code: number of patrons')}}{{_(other)}}
    {{val.library.pid}}{{val.library.name}}
    {%- for k, v in val[head].items() %} @@ -68,13 +113,13 @@

    {{record.created | string | format_date}}

    {%- else %}
    {{val[head]}}
    + {%- endfor%} + + {%- endfor %} + + +
    - + {%- endif%} {%- endblock record_body %} {%- endblock page_body %} diff --git a/rero_ils/modules/stats/templates/rero_ils/stats_list.html b/rero_ils/modules/stats/templates/rero_ils/stats_list.html index 2396a38441..0f74012762 100644 --- a/rero_ils/modules/stats/templates/rero_ils/stats_list.html +++ b/rero_ils/modules/stats/templates/rero_ils/stats_list.html @@ -26,25 +26,63 @@

    {{_('Statistics - billing')}}

    {{_('Statistics')}}

    {% endif %} -
    - {%- for rec in records %} - - - {%- if 'date_range' in rec._source and 'from' in rec._source.date_range %} - {%- if (rec._source.date_range.to|stringtodatetime).month - (rec._source.date_range.from|stringtodatetime).month == 11 %} - {% set stat_name = (rec._source.date_range.from|stringtodatetime).year|string+ - ' (from '+rec._source.date_range.from.split('T')[0]+' to '+rec._source.date_range.to.split('T')[0]+')'%} - {%- else %} - {% set stat_name = rec._source.date_range.from|yearmonthfilter+ - ' (from '+rec._source.date_range.from.split('T')[0]+' to '+rec._source.date_range.to.split('T')[0]+')'%} - {%- endif %} - {{_(stat_name)}} - {% else %} - {{rec._source._created | format_date}} - {% endif %} - - - - {%- endfor %} -
    +{% if type=="billing" %} +
    + {%- for rec in records %} + + + {%- if 'date_range' in rec._source and 'from' in rec._source.date_range %} + {%- if (rec._source.date_range.to|stringtodatetime).month - (rec._source.date_range.from|stringtodatetime).month == 11 %} + {% set stat_name = (rec._source.date_range.from|stringtodatetime).year|string+ + ' (from '+rec._source.date_range.from.split('T')[0]+' to '+rec._source.date_range.to.split('T')[0]+')'%} + {%- else %} + {% set stat_name = rec._source.date_range.from|yearmonthfilter+ + ' (from '+rec._source.date_range.from.split('T')[0]+' to '+rec._source.date_range.to.split('T')[0]+')'%} + {%- endif %} + {{_(stat_name)}} + {% else %} + {{rec._source._created | format_date}} + {% endif %} + + + + {%- endfor %} +
    +{% elif type=="librarian" %} + + + {%- for rec in records %} + + + + + + + + + + {%- endfor %} + +
    + {%- if 'date_range' in rec._source and 'from' in rec._source.date_range %} + {%- if (rec._source.date_range.to|stringtodatetime).month - (rec._source.date_range.from|stringtodatetime).month == 11 %} + {% set stat_name = (rec._source.date_range.from|stringtodatetime).year|string+ + ' (from '+rec._source.date_range.from.split('T')[0]+' to '+rec._source.date_range.to.split('T')[0]+')'%} + {%- else %} + {% set stat_name = rec._source.date_range.from|yearmonthfilter+ + ' (from '+rec._source.date_range.from.split('T')[0]+' to '+rec._source.date_range.to.split('T')[0]+')'%} + {%- endif %} + {{_(stat_name)}} + {% else %} + {{rec._source._created | format_date}} + {% endif %} +
    + {{_('View statistics')}} + +
    {{_('Loans of the transaction library by item location')}} + +
    +{% endif %} {%- endblock body %} diff --git a/rero_ils/modules/stats/views.py b/rero_ils/modules/stats/views.py index 989d8619f9..bb5897555e 100644 --- a/rero_ils/modules/stats/views.py +++ b/rero_ils/modules/stats/views.py @@ -19,18 +19,21 @@ from __future__ import absolute_import, print_function +import csv import datetime +from io import StringIO import arrow import jinja2 import pytz from elasticsearch_dsl import Q -from flask import Blueprint, render_template +from flask import Blueprint, abort, make_response, render_template, request +from flask_login import current_user -from .api import StatsForPricing, StatsSearch -from .permissions import check_logged_as_admin, check_logged_as_librarian - -# from pytz import timezone +from .api import Stat, StatsForPricing, StatsSearch +from .permissions import StatPermission, check_logged_as_admin, \ + check_logged_as_librarian +from .serializers import StatCSVSerializer blueprint = Blueprint( 'stats', @@ -80,6 +83,60 @@ def stats_librarian(): type='librarian') +@blueprint.route('/librarian//csv') +@check_logged_as_librarian +def stats_librarian_queries(record_pid): + """Download specific statistic query into csv file. + + :param record_pid: statistics pid + :return: response object, the csv file + """ + queries = ['loans_of_transaction_library_by_item_location'] + query_id = request.args.get('query_id', None) + if query_id not in queries: + abort(404) + + record = Stat.get_record_by_pid(record_pid) + if not record: + abort(404) + StatPermission.read(current_user, record) + + date_range = '{}_{}'.format(record['date_range']['from'].split('T')[0], + record['date_range']['to'].split('T')[0]) + filename = f'{query_id}_{date_range}.csv' + + data = StringIO() + w = csv.writer(data) + + if query_id == 'loans_of_transaction_library_by_item_location': + fieldnames = ['Transaction library', 'Item library', + 'Item location', 'Checkins', 'Checkouts'] + w.writerow(fieldnames) + for result in record['values']: + transaction_library = '{}: {}'\ + .format(result['library']['pid'], + result['library']['name']) + if not result[query_id]: + w.writerow((transaction_library, '-', '-', 0, 0)) + else: + for location in result[query_id]: + result_loc = result[query_id][location] + location_name = result_loc['location_name'] + item_library =\ + location.replace(f' - {location_name}', '') + w.writerow(( + transaction_library, + item_library, + location_name, + result_loc['checkin'], + result_loc['checkout'])) + + output = make_response(data.getvalue()) + output.headers["Content-Disposition"] = f'attachment; filename={filename}' + output.headers["Content-type"] = "text/csv" + return output + + @jinja2.contextfilter @blueprint.app_template_filter() def yearmonthfilter(context, value, format="%Y-%m-%dT%H:%M:%S"): @@ -107,3 +164,45 @@ def stringtodatetime(context, value, format="%Y-%m-%dT%H:%M:%S"): """ datetime_object = datetime.datetime.strptime(value, format) return datetime_object + + +@jinja2.contextfilter +@blueprint.app_template_filter() +def sort_dict_by_key(context, dictionary): + """Sort dict by dict of keys. + + dictionary: dict to sort + returns: list of tuples + :rtype: list + """ + return StatCSVSerializer.sort_dict_by_key(dictionary) + + +@jinja2.contextfilter +@blueprint.app_template_filter() +def sort_dict_by_library(context, dictionary): + """Sort dict by library name. + + dictionary: dict to sort + returns: sorted dict + :rtype: dict + """ + return sorted(dictionary, key=lambda v: v['library']['name']) + + +@jinja2.contextfilter +@blueprint.app_template_filter() +def process_data(context, value): + """Process data. + + Create key library name and library id, delete key library. + value: dict to process + returns: processed dict + :rtype: dict + """ + if 'library' in value: + updated_dict = {'library id': value['library']['pid'], + 'library name': value['library']['name']} + updated_dict.update(value) + updated_dict.pop('library') + return updated_dict diff --git a/tests/api/stats/conftest.py b/tests/api/stats/conftest.py index 46c26cc887..381d99e9db 100644 --- a/tests/api/stats/conftest.py +++ b/tests/api/stats/conftest.py @@ -20,7 +20,7 @@ import arrow import pytest -from rero_ils.modules.stats.api import Stat, StatsForPricing +from rero_ils.modules.stats.api import Stat, StatsForLibrarian, StatsForPricing @pytest.fixture(scope='module') @@ -29,3 +29,16 @@ def stats(item_lib_martigny, item_lib_fully, item_lib_sion): stats = StatsForPricing(to_date=arrow.utcnow()) yield Stat.create( dict(values=stats.collect()), dbcommit=True, reindex=True) + + +@pytest.fixture(scope='module') +def stats_librarian(item_lib_martigny, item_lib_fully, item_lib_sion): + """Stats fixture.""" + stats_librarian = StatsForLibrarian() + date_range = {'from': stats_librarian.date_range['gte'], + 'to': stats_librarian.date_range['lte']} + stats_values = stats_librarian.collect() + yield Stat.create( + dict(type='librarian', date_range=date_range, + values=stats_values), + dbcommit=True, reindex=True) diff --git a/tests/api/stats/test_stats_rest.py b/tests/api/stats/test_stats_rest.py index 1486d066a8..08d19ca708 100644 --- a/tests/api/stats/test_stats_rest.py +++ b/tests/api/stats/test_stats_rest.py @@ -28,7 +28,6 @@ def test_stats_permissions(client, stats): """Test record retrieval.""" item_url = url_for('invenio_records_rest.stat_item', pid_value=stats.pid) - res = client.get(item_url) assert res.status_code == 401 @@ -126,3 +125,101 @@ def test_stats_get_post_put_delete(client, librarian_martigny): # Delete record/DELETE res = client.delete(item_url) assert res.status_code == 403 + + +def test_stats_librarian_permissions(client, stats_librarian): + """Test record retrieval for statistics librarian.""" + item_url = url_for('invenio_records_rest.stat_item', + pid_value=stats_librarian.pid) + res = client.get(item_url) + assert res.status_code == 401 + + res, _ = postdata( + client, + 'invenio_records_rest.stat_list', + {} + ) + assert res.status_code == 401 + + res = client.put( + url_for('invenio_records_rest.stat_item', + pid_value=stats_librarian.pid), + data={} + ) + assert res.status_code == 401 + + res = client.delete(item_url) + assert res.status_code == 401 + + +def test_stats_librarian_get_post_put_delete(client, stats_librarian, + librarian_martigny, + librarian_sion, + patron_martigny): + """Test CRUD operations for statistics librarian.""" + # patron: role librarian + login_user_via_session(client, librarian_martigny.user) + item_url = url_for('invenio_records_rest.stat_item', + pid_value=stats_librarian.pid) + + # GET is allowed for a librarian or system librarian + res = client.get(item_url) + assert res.status_code == 200 + + # POST / Create record + res, data = postdata( + client, + 'invenio_records_rest.stat_list', + {} + ) + assert res.status_code == 403 + + # PUT / Update record + res = client.put( + item_url, + data={} + ) + assert res.status_code == 403 + + # DELETE / Delete record + res = client.delete(item_url) + assert res.status_code == 403 + + # patron: role patron + login_user_via_session(client, patron_martigny.user) + + # GET is not allowed for a patron + res = client.get(item_url) + assert res.status_code == 403 + + # POST / Create record + res, data = postdata( + client, + 'invenio_records_rest.stat_list', + {} + ) + assert res.status_code == 403 + + # PUT / Update record + res = client.put( + item_url, + data={} + ) + assert res.status_code == 403 + + # DELETE / Delete record + res = client.delete(item_url) + assert res.status_code == 403 + + +def test_stats_librarian_data(client, stats_librarian, librarian_martigny): + """Check data include date_range and type librarian.""" + login_user_via_session(client, librarian_martigny.user) + + item_url = url_for('invenio_records_rest.stat_item', + pid_value=stats_librarian.pid) + res = client.get(item_url) + data = res.get_json() + + assert data['metadata']['date_range'] + assert data['metadata']['type'] == 'librarian'