Skip to content

Commit 7cef13e

Browse files
Merge pull request #137 from NatLibFi/EKIR-289-explain-licenses-in-licensepool
Ekir 289 explain licenses in licensepool
2 parents 1e02525 + 5352ec5 commit 7cef13e

File tree

4 files changed

+197
-0
lines changed

4 files changed

+197
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ __pycache__/
1818
env/
1919
env2/
2020
venv/
21+
.venv/
2122
build/
2223
develop-eggs/
2324
dist/

bin/informational/license_report

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
6+
bin_dir = os.path.split(__file__)[0]
7+
package_dir = os.path.join(bin_dir, "..", "..")
8+
sys.path.append(os.path.abspath(package_dir))
9+
10+
from scripts import LicenseReportScript # noqa: E402
11+
12+
LicenseReportScript().run()

core/scripts.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2391,6 +2391,18 @@ def explain_license_pool(self, pool):
23912391
pool.licenses_reserved,
23922392
)
23932393
)
2394+
self.write("Licenses: %s" % (len(pool.licenses)))
2395+
for license in pool.licenses:
2396+
self.write("License ID: %s:" % (license.identifier))
2397+
self.write(
2398+
" Checkouts left: %s, Checkouts available: %s, Concurrency: %s , Expires: %s"
2399+
% (
2400+
license.checkouts_left,
2401+
license.checkouts_available,
2402+
license.terms_concurrency,
2403+
license.expires,
2404+
)
2405+
)
23942406

23952407
def explain_work(self, work):
23962408
self.write("Work info:")

scripts.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,178 @@ def explain(self, licensepool):
887887
print("\t".join([str(x) for x in data]))
888888

889889

890+
class LicenseReportScript(Script):
891+
892+
"""Print a CSV-format report of all our licensepools and their licenses."""
893+
894+
def do_run(self):
895+
qu = (
896+
self._db.query(LicensePool)
897+
.filter(LicensePool.open_access == False)
898+
.order_by(LicensePool.availability_time.desc())
899+
)
900+
first_row = [
901+
"Work/Licensepool Identifier",
902+
"Title",
903+
"Author",
904+
"First seen",
905+
"Last seen (best guess)",
906+
"Current licenses owned",
907+
"Current licenses available",
908+
"Changes in number of licenses",
909+
"Changes in title availability",
910+
"License identifier",
911+
"License status",
912+
"License checkouts left",
913+
"License checkouts available",
914+
"License concurrency",
915+
"Active loans",
916+
"License expiration",
917+
]
918+
print(",".join(first_row))
919+
920+
for pool in qu:
921+
self.explain(pool)
922+
923+
def investigate(self, licensepool):
924+
"""Show events about all our licensepools and information
925+
about each license in the pool.
926+
927+
:param licensepool: A LicensePool.
928+
929+
:return: a 3-tuple (last_seen, title_removal_events,
930+
license_removal_events).
931+
932+
`last_seen` is the latest point at which we knew the book was
933+
circulating. If we never knew the book to be circulating, this
934+
is the first time we ever saw the LicensePool.
935+
936+
`title_removal_events` is a query that returns CirculationEvents
937+
in which this LicensePool was removed from the remote collection.
938+
939+
`license_removal_events` is a query that returns
940+
CirculationEvents in which LicensePool.licenses_owned went
941+
from having a positive number to being zero or a negative
942+
number.
943+
"""
944+
first_activity = None
945+
most_recent_activity = None
946+
947+
# If we have absolutely no information about the book ever
948+
# circulating, we act like we lost track of the book
949+
# immediately after seeing it for the first time.
950+
last_seen = licensepool.availability_time
951+
952+
# If there's a recorded loan or hold on the book, that can
953+
# push up the last time the book was known to be circulating.
954+
for l in (licensepool.loans, licensepool.holds):
955+
for item in l:
956+
if not last_seen or item.start > last_seen:
957+
last_seen = item.start
958+
959+
# Now we look for relevant circulation events. First, an event
960+
# where the title was explicitly removed is pretty clearly
961+
# a 'last seen'.
962+
base_query = (
963+
self._db.query(CirculationEvent)
964+
.filter(CirculationEvent.license_pool == licensepool)
965+
.order_by(CirculationEvent.start.desc())
966+
)
967+
title_removal_events = base_query.filter(
968+
CirculationEvent.type == CirculationEvent.DISTRIBUTOR_TITLE_REMOVE
969+
)
970+
if title_removal_events.count():
971+
candidate = title_removal_events[-1].start
972+
if not last_seen or candidate > last_seen:
973+
last_seen = candidate
974+
975+
# Also look for an event where the title went from a nonzero
976+
# number of licenses to a zero number of licenses. That's a
977+
# good 'last seen'.
978+
license_removal_events = (
979+
base_query.filter(
980+
CirculationEvent.type == CirculationEvent.DISTRIBUTOR_LICENSE_REMOVE,
981+
)
982+
.filter(CirculationEvent.old_value > 0)
983+
.filter(CirculationEvent.new_value <= 0)
984+
)
985+
if license_removal_events.count():
986+
candidate = license_removal_events[-1].start
987+
if not last_seen or candidate > last_seen:
988+
last_seen = candidate
989+
990+
return last_seen, title_removal_events, license_removal_events
991+
992+
format = "%Y-%m-%d"
993+
994+
def explain(self, licensepool):
995+
edition = licensepool.presentation_edition
996+
identifier = licensepool.identifier
997+
last_seen, title_removal_events, license_removal_events = self.investigate(
998+
licensepool
999+
)
1000+
1001+
data = [f"{identifier.type} {identifier.identifier}"]
1002+
if edition:
1003+
data.extend([f'"{edition.title}"', f'"{edition.author}"'])
1004+
if licensepool.availability_time:
1005+
first_seen = licensepool.availability_time.strftime(self.format)
1006+
else:
1007+
first_seen = ""
1008+
data.append(first_seen)
1009+
if last_seen:
1010+
last_seen = last_seen.strftime(self.format)
1011+
else:
1012+
last_seen = ""
1013+
data.append(last_seen)
1014+
data.append(licensepool.licenses_owned)
1015+
data.append(licensepool.licenses_available)
1016+
1017+
license_removals = []
1018+
for event in license_removal_events:
1019+
description = "{}: {}→{}".format(
1020+
event.start.strftime(self.format),
1021+
event.old_value,
1022+
event.new_value,
1023+
)
1024+
license_removals.append(description)
1025+
data.append(", ".join(license_removals))
1026+
1027+
title_removals = [
1028+
event.start.strftime(self.format) for event in title_removal_events
1029+
]
1030+
data.append(", ".join(title_removals))
1031+
# Print the main license pool information
1032+
print(",".join(str(item) for item in data)) # Convert all items to strings
1033+
1034+
# Then fetch each license in the pool
1035+
for license in licensepool.licenses:
1036+
# Append each field's data as a separate entry in the data list
1037+
expire_date = (
1038+
license.expires.strftime(self.format) if license.expires else ""
1039+
)
1040+
license_data = [
1041+
"",
1042+
"",
1043+
"",
1044+
"",
1045+
"",
1046+
"",
1047+
"",
1048+
"",
1049+
"", # Fill the first 9 columns with empty strings
1050+
license.identifier,
1051+
license.status,
1052+
license.checkouts_left,
1053+
license.checkouts_available,
1054+
license.terms_concurrency,
1055+
len(license.loans),
1056+
expire_date,
1057+
]
1058+
# And print each license on a new line
1059+
print(",".join(str(item) for item in license_data))
1060+
1061+
8901062
class NYTBestSellerListsScript(TimestampScript):
8911063
name = "Update New York Times best-seller lists"
8921064

0 commit comments

Comments
 (0)