Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions src/palace/manager/api/admin/controller/report.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import uuid
from http import HTTPStatus

import flask
Expand All @@ -13,12 +14,20 @@
InventoryReportCollectionInfo,
InventoryReportInfo,
)
from palace.manager.api.admin.problem_details import ADMIN_NOT_AUTHORIZED
from palace.manager.api.admin.problem_details import (
ADMIN_NOT_AUTHORIZED,
INVALID_REPORT_KEY,
)
from palace.manager.celery.tasks.generate_inventory_and_hold_reports import (
generate_inventory_and_hold_reports,
library_report_integrations,
)
from palace.manager.celery.tasks.reports import (
REPORT_KEY_MAPPING,
generate_report,
)
from palace.manager.core.problem_details import INTERNAL_SERVER_ERROR
from palace.manager.reporting.reports.library_collection import LibraryCollectionReport
from palace.manager.service.integration_registry.license_providers import (
LicenseProvidersRegistry,
)
Expand All @@ -27,6 +36,7 @@
from palace.manager.sqlalchemy.model.library import Library
from palace.manager.util.log import LoggerMixin
from palace.manager.util.problem_detail import ProblemDetail, ProblemDetailException
from palace.manager.util.uuid import uuid_encode


def _authorize_from_request(
Expand All @@ -36,7 +46,7 @@ def _authorize_from_request(

:param request: A Flask Request object.
:return: A 2-tuple of admin and library, if the admin is authorized for the library.
:raise: ProblemDetailException, if no library or if admin not authorized for the library.
:raise: ProblemDetailException, if no library or if admin is not authorized for the library.
"""
library = required_library_from_request(request)
admin = required_admin_from_request(request)
Expand All @@ -46,10 +56,64 @@ def _authorize_from_request(


class ReportController(LoggerMixin):

def __init__(self, db: Session, registry: LicenseProvidersRegistry):
self._db = db
self.registry = registry

@classmethod
def report_for_key(cls, key: str) -> type[LibraryCollectionReport]:
if key not in REPORT_KEY_MAPPING:
detail = INVALID_REPORT_KEY.detail or "Unknown report key."
raise ProblemDetailException(
INVALID_REPORT_KEY.detailed(f"{detail.rstrip('. ')} (key='{key}').")
)
return REPORT_KEY_MAPPING[key]

def generate_report(self, *, report_key: str) -> Response:
"""Generate the report indicated by the report_key."""

admin, library = _authorize_from_request(flask.request)
email_address = admin.email

report = self.report_for_key(report_key)
report_title = report.TITLE

request_id = uuid_encode(uuid.uuid4())

self.log.info(
f"Report '{report_title}' ({report_key}) requested by <{email_address}>. (request ID: {request_id})"
)
try:
task = generate_report.delay(
key=report_key,
request_id=request_id,
library_id=library.id,
email_address=email_address,
)
except Exception as e:
msg = f"Failed to generate report '{report_title}' ({report_key}). (request ID: {request_id})"
self.log.error(msg=msg, exc_info=e)
raise ProblemDetailException(
INTERNAL_SERVER_ERROR.detailed(detail=msg)
) from e

self.log.info(
f"Report task created: '{report_title}' ({report_key}) for <{email_address}>. "
f"(request ID: {request_id}, task ID: {task.id})"
)

response_message = (
f"The '{report_title}' request was received. "
"Report processing may take a few minutes to complete, depending on current server load. "
f"Upon completion, a notification will be sent to {email_address}."
)
return Response(
json.dumps({"message": response_message}),
HTTPStatus.ACCEPTED,
mimetype=MediaTypes.APPLICATION_JSON_MEDIA_TYPE,
)

def inventory_report_info(self) -> Response:
"""InventoryReportInfo response of reportable collections for a library.

Expand Down
7 changes: 7 additions & 0 deletions src/palace/manager/api/admin/problem_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@
_("There was a problem with the edited metadata."),
)

INVALID_REPORT_KEY = pd(
"http://palaceproject.io/terms/problem/invalid-report-key",
400,
_("Invalid report key"),
_("No currently defined report is associated with the specified key."),
)

METADATA_REFRESH_PENDING = pd(
"http://librarysimplified.org/terms/problem/metadata-refresh-pending",
201,
Expand Down
8 changes: 8 additions & 0 deletions src/palace/manager/api/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,14 @@ def generate_inventory_report():
return app.manager.admin_report_controller.generate_inventory_report()


@library_route("/admin/reports/<report_key>", methods=["POST"])
@has_library
@returns_json_or_response_or_problem_detail
@requires_admin
def generate_report(report_key: str):
return app.manager.admin_report_controller.generate_report(report_key=report_key)


@app.route("/admin/sign_in_again")
def admin_sign_in_again():
"""Allows an admin with expired credentials to sign back in
Expand Down
34 changes: 34 additions & 0 deletions src/palace/manager/celery/tasks/reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations

from celery import shared_task
from frozendict import frozendict
from typing_extensions import Unpack

from palace.manager.celery.task import Task
from palace.manager.reporting.reports.library_collection import (
LibraryCollectionReport,
LibraryReportKwargs,
LibraryTitleLevelReport,
)
from palace.manager.service.celery.celery import QueueNames

REPORT_KEY_MAPPING: frozendict[str, type[LibraryCollectionReport]] = frozendict(
{report.KEY: report for report in [LibraryTitleLevelReport]}
)


@shared_task(queue=QueueNames.high, bind=True)
def generate_report(
task: Task, *, key: str, **kwargs: Unpack[LibraryReportKwargs]
) -> bool:
report_class = REPORT_KEY_MAPPING[key]
report = report_class.from_task(task, **kwargs)

with task.session() as session:
success = report.run(session=session)
if not success:
task.log.error(
f"Report task failed: '{report.title}' ({report.key}) for <{report.email_address}>. "
f"(request ID: {report.request_id})"
)
return success
10 changes: 8 additions & 2 deletions src/palace/manager/reporting/reports/library_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from palace.manager.celery.task import Task
from palace.manager.core.exceptions import IntegrationException
from palace.manager.reporting.model import ReportTable, TTabularDataProcessor
from palace.manager.reporting.tables.library_all_title import LibraryAllTitleReportTable
from palace.manager.reporting.util import (
RequestIdLoggerAdapter,
TimestampFormat,
Expand Down Expand Up @@ -368,8 +369,7 @@ def _run_report(self, *, session: Session) -> bool:
# Send success notification.
self.send_success_notification(access_url=location)
self.log.info(
f"Emailed notification for report '{self.title}' ({self.key}) for "
f"library {library.name} ({library.short_name}) to {self.email_address}."
f"Completed report '{self.key}' for {library.name} ({library.short_name})."
)
return True

Expand Down Expand Up @@ -400,3 +400,9 @@ def run(self, *, session: Session) -> bool:
)
self.send_error_notification()
return False


class LibraryTitleLevelReport(LibraryCollectionReport):
KEY = "title-level-report"
TITLE = "Title-Level Report"
TABLE_CLASSES = [LibraryAllTitleReportTable]
Loading