@@ -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+
8901062class NYTBestSellerListsScript (TimestampScript ):
8911063 name = "Update New York Times best-seller lists"
8921064
0 commit comments