Skip to content

Commit

Permalink
Add test for generate inventory report
Browse files Browse the repository at this point in the history
  • Loading branch information
dbernstein committed Mar 14, 2024
1 parent 8943083 commit ab0ca0a
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 104 deletions.
1 change: 1 addition & 0 deletions bin/generate_inventory_reports
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import sys
bin_dir = os.path.split(__file__)[0]
package_dir = os.path.join(bin_dir, "..")
sys.path.append(os.path.abspath(package_dir))

from core.scripts import GenerateInventoryReports

GenerateInventoryReports().run()
175 changes: 71 additions & 104 deletions core/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from core.lane import Lane
from core.metadata_layer import TimestampData
from core.model import (
Admin,
BaseCoverageRecord,
Collection,
Contributor,
Expand Down Expand Up @@ -2770,31 +2769,6 @@ def suppress_work(self, library: Library, identifier: Identifier) -> None:
class GenerateInventoryReports(Script):
"""Generate inventory reports from queued report tasks"""

HEADER = [
"title",
"author",
"isbn",
"other_identifier",
"language",
"genre",
"publisher",
"audience",
"format",
"library",
"collection",
"license_duration_in_days",
"license_expiration_date",
"initial_loan_count",
"consumed_loans",
"remaining_loans",
"allowed_concurrent_users",
"max_loan_duration_in_days",
"active_holds_for_library",
"active_loans_for_library",
"active_holds_for_collection",
"active_loans_for_collection",
]

DATA_SOURCES = [
"Palace Marketplace",
"BiblioBoard",
Expand All @@ -2817,12 +2791,6 @@ def parse_command_line(
parser = cls.arg_parser(_db)
return parser.parse_known_args(cmd_args)[0]

def load_admin(self, admin_id: int) -> Admin:
admin = self._db.query(Admin).filter(Admin.id == admin_id).first()
if not admin:
raise ValueError(f"Unknown Admin: id = {id}")
return admin

def do_run(self, cmd_args: list[str] | None = None) -> None:
parsed = self.parse_command_line(self._db, cmd_args=cmd_args)

Expand All @@ -2835,16 +2803,15 @@ def do_run(self, cmd_args: list[str] | None = None) -> None:

def process_task(self, task: AsyncTask):
data = InventoryReportTaskData(**task.data)

admin = self.load_admin(data.admin_id)
files = []
try:
current_time = datetime.datetime.now()
date_str = current_time.strftime("%Y-%m-%d_%H:%M:%s")
attachments = {}

for data_source_name in self.DATA_SOURCES:
prefix = f"palace-inventory-report-{data_source_name}-{date_str}"
formatted_ds_name = data_source_name.lower().replace(" ", "_")
prefix = f"palace-inventory-report-{formatted_ds_name}-{date_str}"
suffix = ".csv"
with tempfile.NamedTemporaryFile(
"w",
Expand Down Expand Up @@ -2888,75 +2855,75 @@ def generate_report(self, data_source_name: str, library_id: int, output_file):
writer.writerows(rows)

def inventory_report_query(self) -> str:
return """select lp.id as license_pool_id,
e.title,
e.author,
i.identifier,
e.language,
e.publisher,
e.medium as format,
ic.name collection_name,
DATE_PART('day', l.expires::date) - DATE_PART('day',lp.availability_time::date) as license_duration_days,
l.expires license_expiration_date,
l.checkouts_available initial_loan_count,
(l.checkouts_available-l.checkouts_left) consumed_loans,
l.checkouts_left remaining_loans,
l.terms_concurrency allowed_concurrent_users,
coalesce(lib_holds.active_hold_count, 0) library_active_hold_count,
coalesce(lib_loans.active_loan_count, 0) library_active_loan_count,
CASE WHEN collection_sharing.is_shared_collection THEN lp.patrons_in_hold_queue
ELSE -1
END shared_hold_queue,
CASE WHEN collection_sharing.is_shared_collection THEN lp.licenses_reserved
ELSE -1
END shared_hold_queue
from datasources d,
collections c,
integration_configurations ic,
integration_library_configurations il,
libraries lib,
editions e,
identifiers i,
(select ic.parent_id,
count(ic.parent_id) > 1 is_shared_collection
from integration_library_configurations ic,
integration_configurations i,
collections c
where c.integration_configuration_id = i.id and
i.id = ic.parent_id group by ic.parent_id) collection_sharing,
licensepools lp left outer join licenses l on lp.id = l.license_pool_id
left outer join (select h.license_pool_id,
p.library_id,
count(h.id) active_hold_count
from holds h,
patrons p,
libraries l
where p.id = h.patron_id and
p.library_id = l.id and
l.id = :library_id
group by p.library_id, h.license_pool_id) lib_holds on lp.id = lib_holds.license_pool_id
left outer join (select ln.license_pool_id,
p.library_id,
count(ln.id) active_loan_count
from loans ln,
patrons p,
libraries l
where p.id = ln.patron_id and
p.library_id = l.id and
l.id = :library_id
group by p.library_id, ln.license_pool_id) lib_loans on lp.id = lib_holds.license_pool_id
where lp.identifier_id = i.id and
e.primary_identifier_id = i.id and
d.id = e.data_source_id and
c.id = lp.collection_id and
c.integration_configuration_id = ic.id and
ic.id = il.parent_id and
ic.id = collection_sharing.parent_id and
il.library_id = lib.id and
d.name = :data_source_name and
lib.id = :library_id
order by title, author
return """
select
e.title,
e.author,
i.identifier,
e.language,
e.publisher,
e.medium as format,
ic.name collection_name,
DATE_PART('day', l.expires::date) - DATE_PART('day',lp.availability_time::date) as license_duration_days,
l.expires license_expiration_date,
l.checkouts_available initial_loan_count,
(l.checkouts_available-l.checkouts_left) consumed_loans,
l.checkouts_left remaining_loans,
l.terms_concurrency allowed_concurrent_users,
coalesce(lib_holds.active_hold_count, 0) library_active_hold_count,
coalesce(lib_loans.active_loan_count, 0) library_active_loan_count,
CASE WHEN collection_sharing.is_shared_collection THEN lp.patrons_in_hold_queue
ELSE -1
END shared_active_hold_count,
CASE WHEN collection_sharing.is_shared_collection THEN lp.licenses_reserved
ELSE -1
END shared_active_loan_count
from datasources d,
collections c,
integration_configurations ic,
integration_library_configurations il,
libraries lib,
editions e,
identifiers i,
(select ic.parent_id,
count(ic.parent_id) > 1 is_shared_collection
from integration_library_configurations ic,
integration_configurations i,
collections c
where c.integration_configuration_id = i.id and
i.id = ic.parent_id group by ic.parent_id) collection_sharing,
licensepools lp left outer join licenses l on lp.id = l.license_pool_id
left outer join (select h.license_pool_id,
p.library_id,
count(h.id) active_hold_count
from holds h,
patrons p,
libraries l
where p.id = h.patron_id and
p.library_id = l.id and
l.id = :library_id
group by p.library_id, h.license_pool_id) lib_holds on lp.id = lib_holds.license_pool_id
left outer join (select ln.license_pool_id,
p.library_id,
count(ln.id) active_loan_count
from loans ln,
patrons p,
libraries l
where p.id = ln.patron_id and
p.library_id = l.id and
l.id = :library_id
group by p.library_id, ln.license_pool_id) lib_loans on lp.id = lib_holds.license_pool_id
where lp.identifier_id = i.id and
e.primary_identifier_id = i.id and
d.id = e.data_source_id and
c.id = lp.collection_id and
c.integration_configuration_id = ic.id and
ic.id = il.parent_id and
ic.id = collection_sharing.parent_id and
il.library_id = lib.id and
d.name = :data_source_name and
lib.id = :library_id
order by title, author
"""


Expand Down
138 changes: 138 additions & 0 deletions tests/core/test_scripts.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import csv
import datetime
import json
import random
from dataclasses import asdict
from io import StringIO
from unittest.mock import MagicMock, call, create_autospec, patch

Expand Down Expand Up @@ -35,6 +37,12 @@
get_one,
get_one_or_create,
)
from core.model.asynctask import (
AsyncTaskStatus,
AsyncTaskType,
InventoryReportTaskData,
queue_task,
)
from core.model.classification import Classification, Subject
from core.model.customlist import CustomList
from core.model.devicetokens import DeviceToken, DeviceTokenTypes
Expand All @@ -52,6 +60,7 @@
CustomListUpdateEntriesScript,
DeleteInvisibleLanesScript,
Explain,
GenerateInventoryReports,
IdentifierInputScript,
LaneSweeperScript,
LibraryInputScript,
Expand Down Expand Up @@ -2554,6 +2563,135 @@ def test_suppress_work(self, db: DatabaseTransactionFixture):
assert work.suppressed_for == [test_library]


class TestGenerateInventoryReports:
def test_do_run(self, db: DatabaseTransactionFixture):
# create some test data
library = db.library(short_name="test")
collection = db.collection(
name="BiblioBoard Test collection", data_source_name="BiblioBoard"
)
collection.libraries = [library]
ds = collection.data_source
title = "Leaves of Grass"
author = "Walt Whitman"
email = "[email protected]"
checkouts_left = 10
checkouts_available = 11
terms_concurrency = 5
edition = db.edition(data_source_name=ds.name)
edition.title = title
edition.author = author
work = db.work(
language="eng",
fiction=True,
with_license_pool=False,
data_source_name=ds.name,
presentation_edition=edition,
collection=collection,
)
licensepool = db.licensepool(
edition=edition,
open_access=False,
data_source_name=ds.name,
set_edition_as_presentation=True,
collection=collection,
)

db.license(
pool=licensepool,
checkouts_available=checkouts_available,
checkouts_left=checkouts_left,
terms_concurrency=terms_concurrency,
)

data = InventoryReportTaskData(
admin_id=1, library_id=library.id, admin_email=email
)
task, is_new = queue_task(
db.session, task_type=AsyncTaskType.INVENTORY_REPORT, data=asdict(data)
)

assert task.status == AsyncTaskStatus.READY

script = GenerateInventoryReports(db.session)
send_email_mock = create_autospec(script.services.email.container.send_email)
script.services.email.container.send_email = send_email_mock
script.do_run()
send_email_mock.assert_called_once()
args, kwargs = send_email_mock.call_args
assert task.status == AsyncTaskStatus.SUCCESS
assert kwargs["receivers"] == [email]
assert kwargs["subject"].__contains__("Inventory Report")
attachments: dict = kwargs["attachments"]
csv_titles_strings = [
"biblioboard",
"palace_marketplace",
"unlimited_listens",
"palace_bookshelf",
]

def at_least_one_contains_the_other(list1: [str], list2: [str]) -> bool:
for s1 in list1:
for s2 in list2:
if s1.__contains__(s2):
return True
return False

assert at_least_one_contains_the_other(attachments.keys(), csv_titles_strings)

for key in attachments.keys():
value = attachments[key]
assert len(value) > 0
if str(key).__contains__("biblioboard"):
csv_file = StringIO(value)
reader = csv.reader(csv_file, delimiter=",")
first_row = None
row_count = 0

for row in reader:
row_count += 1
if not first_row:
first_row = row
row_headers = [
"title",
"author",
"identifier",
"language",
"publisher",
"format",
"collection_name",
"license_duration_days",
"license_expiration_date",
"initial_loan_count",
"consumed_loans",
"remaining_loans",
"allowed_concurrent_users",
"library_active_hold_count",
"library_active_loan_count",
"shared_active_hold_count",
"shared_active_loan_count",
]
for h in row_headers:
assert h in row
continue

assert row[first_row.index("title")] == title
assert row[first_row.index("author")] == author
assert row[first_row.index("shared_active_hold_count")] == "-1"
assert row[first_row.index("shared_active_loan_count")] == "-1"
assert row[first_row.index("initial_loan_count")] == str(
checkouts_available
)
assert row[first_row.index("consumed_loans")] == str(
checkouts_available - checkouts_left
)
assert row[first_row.index("allowed_concurrent_users")] == str(
terms_concurrency
)

assert row_count == 2


class TestWorkConsolidationScript:
"""TODO"""

Expand Down

0 comments on commit ab0ca0a

Please sign in to comment.