Skip to content

Commit 1835c10

Browse files
committed
Base functionality.
1 parent a44ebce commit 1835c10

File tree

5 files changed

+124
-2
lines changed

5 files changed

+124
-2
lines changed

src/palace/manager/api/admin/controller/report.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import uuid
23
from http import HTTPStatus
34

45
import flask
@@ -13,12 +14,20 @@
1314
InventoryReportCollectionInfo,
1415
InventoryReportInfo,
1516
)
16-
from palace.manager.api.admin.problem_details import ADMIN_NOT_AUTHORIZED
17+
from palace.manager.api.admin.problem_details import (
18+
ADMIN_NOT_AUTHORIZED,
19+
INVALID_REPORT_KEY,
20+
)
1721
from palace.manager.celery.tasks.generate_inventory_and_hold_reports import (
1822
generate_inventory_and_hold_reports,
1923
library_report_integrations,
2024
)
25+
from palace.manager.celery.tasks.reports import (
26+
REPORT_KEY_MAPPING,
27+
generate_report,
28+
)
2129
from palace.manager.core.problem_details import INTERNAL_SERVER_ERROR
30+
from palace.manager.reporting.reports.library_collection import LibraryCollectionReport
2231
from palace.manager.service.integration_registry.license_providers import (
2332
LicenseProvidersRegistry,
2433
)
@@ -27,6 +36,7 @@
2736
from palace.manager.sqlalchemy.model.library import Library
2837
from palace.manager.util.log import LoggerMixin
2938
from palace.manager.util.problem_detail import ProblemDetail, ProblemDetailException
39+
from palace.manager.util.uuid import uuid_encode
3040

3141

3242
def _authorize_from_request(
@@ -36,7 +46,7 @@ def _authorize_from_request(
3646
3747
:param request: A Flask Request object.
3848
:return: A 2-tuple of admin and library, if the admin is authorized for the library.
39-
:raise: ProblemDetailException, if no library or if admin not authorized for the library.
49+
:raise: ProblemDetailException, if no library or if admin is not authorized for the library.
4050
"""
4151
library = required_library_from_request(request)
4252
admin = required_admin_from_request(request)
@@ -46,10 +56,65 @@ def _authorize_from_request(
4656

4757

4858
class ReportController(LoggerMixin):
59+
4960
def __init__(self, db: Session, registry: LicenseProvidersRegistry):
5061
self._db = db
5162
self.registry = registry
5263

64+
@classmethod
65+
def report_for_key(cls, key: str) -> type[LibraryCollectionReport]:
66+
if key not in REPORT_KEY_MAPPING:
67+
detail = INVALID_REPORT_KEY.detail or "Unknown report key."
68+
raise ProblemDetailException(
69+
INVALID_REPORT_KEY.detailed(f"{detail.rstrip('. ')} (key='{key}').")
70+
)
71+
return REPORT_KEY_MAPPING[key]
72+
73+
def generate_report(self, *, report_key: str) -> Response:
74+
"""Generate the report indicated by the report_key."""
75+
76+
admin, library = _authorize_from_request(flask.request)
77+
email_address = admin.email
78+
79+
report = self.report_for_key(report_key)
80+
report_title = report.TITLE
81+
82+
request_id = uuid_encode(uuid.uuid4())
83+
84+
self.log.info(
85+
f"Report '{report_title}' ({report_key}) requested by <{email_address}>. (request ID: {request_id})"
86+
)
87+
try:
88+
task = generate_report.delay(
89+
key=report_key,
90+
request_id=request_id,
91+
library_id=library.id,
92+
email_address=email_address,
93+
)
94+
except Exception as e:
95+
msg = f"Failed to generate report '{report_title}' ({report_key}). (request ID: {request_id})"
96+
self.log.error(msg=msg, exc_info=e)
97+
self._db.rollback()
98+
raise ProblemDetailException(
99+
INTERNAL_SERVER_ERROR.detailed(detail=msg)
100+
) from e
101+
102+
self.log.info(
103+
f"Report task created: '{report_title}' ({report_key}) for <{email_address}>. "
104+
f"(request ID: {request_id}, task ID: {task.id})"
105+
)
106+
107+
response_message = (
108+
f"The '{report_title}' request was received. "
109+
"Report processing may take a few minutes to complete, depending on current server load. "
110+
f"Upon completion, a notification will be sent to {email_address}."
111+
)
112+
return Response(
113+
json.dumps({"message": response_message}),
114+
HTTPStatus.ACCEPTED,
115+
mimetype=MediaTypes.APPLICATION_JSON_MEDIA_TYPE,
116+
)
117+
53118
def inventory_report_info(self) -> Response:
54119
"""InventoryReportInfo response of reportable collections for a library.
55120

src/palace/manager/api/admin/problem_details.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@
4646
_("There was a problem with the edited metadata."),
4747
)
4848

49+
INVALID_REPORT_KEY = pd(
50+
"http://palaceproject.io/terms/problem/invalid-report-key",
51+
400,
52+
_("Invalid report key"),
53+
_("No currently defined report is associated with the specified key."),
54+
)
55+
4956
METADATA_REFRESH_PENDING = pd(
5057
"http://librarysimplified.org/terms/problem/metadata-refresh-pending",
5158
201,

src/palace/manager/api/admin/routes.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,14 @@ def generate_inventory_report():
689689
return app.manager.admin_report_controller.generate_inventory_report()
690690

691691

692+
@library_route("/admin/reports/<report_key>", methods=["POST"])
693+
@has_library
694+
@returns_json_or_response_or_problem_detail
695+
@requires_admin
696+
def generate_report(report_key: str):
697+
return app.manager.admin_report_controller.generate_report(report_key=report_key)
698+
699+
692700
@app.route("/admin/sign_in_again")
693701
def admin_sign_in_again():
694702
"""Allows an admin with expired credentials to sign back in
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
from celery import shared_task
4+
from typing_extensions import Unpack
5+
6+
from palace.manager.celery.task import Task
7+
from palace.manager.reporting.reports.library_collection import (
8+
LibraryCollectionReport,
9+
LibraryReportKwargs,
10+
LibraryTitleLevelReport,
11+
)
12+
from palace.manager.service.celery.celery import QueueNames
13+
14+
REPORT_KEY_MAPPING: dict[str, type[LibraryCollectionReport]] = {
15+
report.KEY: report
16+
for report in [
17+
LibraryTitleLevelReport,
18+
]
19+
}
20+
21+
22+
@shared_task(queue=QueueNames.high, bind=True)
23+
def generate_report(
24+
task: Task, *, key: str, **kwargs: Unpack[LibraryReportKwargs]
25+
) -> bool:
26+
with task.session() as session:
27+
report_class = REPORT_KEY_MAPPING[key]
28+
report = report_class.from_task(task, **kwargs)
29+
success = report.run(session=session)
30+
if not success:
31+
task.log.error(
32+
f"Report task failed: '{report.title}' ({report.key}) for <{report.email_address}>. "
33+
f"(request ID: {report.request_id}, task ID: {task.id})"
34+
)
35+
return success

src/palace/manager/reporting/reports/library_collection.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from palace.manager.celery.task import Task
1717
from palace.manager.core.exceptions import IntegrationException
1818
from palace.manager.reporting.model import ReportTable, TTabularDataProcessor
19+
from palace.manager.reporting.tables.library_all_title import LibraryAllTitleReportTable
1920
from palace.manager.reporting.util import (
2021
RequestIdLoggerAdapter,
2122
TimestampFormat,
@@ -402,3 +403,9 @@ def run(self, *, session: Session) -> bool:
402403
)
403404
self.send_error_notification()
404405
return False
406+
407+
408+
class LibraryTitleLevelReport(LibraryCollectionReport):
409+
KEY = "title-level-report"
410+
TITLE = "Title-Level Report"
411+
TABLE_CLASSES = [LibraryAllTitleReportTable]

0 commit comments

Comments
 (0)