Skip to content

Commit 1466205

Browse files
authored
Reports: use task queue if available, translate (fixed #351) (#358)
* Reports: use queue, localize * Include locale in apispec
1 parent 17119cb commit 1466205

File tree

8 files changed

+534
-157
lines changed

8 files changed

+534
-157
lines changed

gramps_webapi/api/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@
6060
from .resources.people import PeopleResource, PersonResource
6161
from .resources.places import PlaceResource, PlacesResource
6262
from .resources.relations import RelationResource, RelationsResource
63-
from .resources.reports import ReportFileResource, ReportResource, ReportsResource
63+
from .resources.reports import (
64+
ReportFileResource,
65+
ReportFileResultResource,
66+
ReportResource,
67+
ReportsResource,
68+
)
6469
from .resources.repositories import RepositoriesResource, RepositoryResource
6570
from .resources.search import SearchIndexResource, SearchResource
6671
from .resources.sources import SourceResource, SourcesResource
@@ -202,13 +207,17 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
202207
register_endpt(ReportFileResource, "/reports/<string:report_id>/file", "report-file")
203208
register_endpt(ReportResource, "/reports/<string:report_id>", "report")
204209
register_endpt(ReportsResource, "/reports/", "reports")
210+
register_endpt(
211+
ReportFileResultResource,
212+
"/reports/<string:report_id>/file/processed/<string:filename>",
213+
"report-file-result",
214+
)
205215
# Facts
206216
register_endpt(FactsResource, "/facts/", "facts")
207217
# Exporters
208218
register_endpt(
209219
ExporterFileResource, "/exporters/<string:extension>/file", "exporter-file"
210220
)
211-
# Exporters
212221
register_endpt(
213222
ExporterFileResultResource,
214223
"/exporters/<string:extension>/file/processed/<string:filename>",

gramps_webapi/api/export.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
import mimetypes
2525
import os
26-
import tempfile
2726
import uuid
2827
from pathlib import Path
2928
from typing import Dict

gramps_webapi/api/report.py

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
#
2+
# Gramps Web API - A RESTful API for the Gramps genealogy program
3+
#
4+
# Copyright (C) 2020 Christopher Horn
5+
# Copyright (C) 2023 David Straub
6+
#
7+
# This program is free software; you can redistribute it and/or modify
8+
# it under the terms of the GNU Affero General Public License as published by
9+
# the Free Software Foundation; either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Affero General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Affero General Public License
18+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
#
20+
21+
"""Functions for running Gramps reports."""
22+
23+
import os
24+
import uuid
25+
from pathlib import Path
26+
from typing import Dict, Optional
27+
28+
from flask import abort, current_app
29+
from gramps.cli.plug import CommandLineReport
30+
from gramps.cli.user import User
31+
from gramps.gen.db.base import DbReadBase
32+
from gramps.gen.filters import reload_custom_filters
33+
from gramps.gen.plug import BasePluginManager
34+
from gramps.gen.plug.docgen import PaperStyle
35+
from gramps.gen.plug.report import (
36+
CATEGORY_BOOK,
37+
CATEGORY_DRAW,
38+
CATEGORY_GRAPHVIZ,
39+
CATEGORY_TEXT,
40+
CATEGORY_TREE,
41+
)
42+
from gramps.gen.utils.grampslocale import GrampsLocale
43+
from gramps.gen.utils.resourcepath import ResourcePath
44+
45+
from ..const import MIME_TYPES, REPORT_DEFAULTS, REPORT_FILTERS
46+
47+
_EXTENSION_MAP = {".gvpdf": ".pdf", ".gspdf": ".pdf"}
48+
49+
50+
def get_report_profile(
51+
db_handle: DbReadBase, plugin_manager: BasePluginManager, report_data
52+
):
53+
"""Extract and return report attributes and options."""
54+
module = plugin_manager.load_plugin(report_data)
55+
option_class = getattr(module, report_data.optionclass)
56+
report = CommandLineReport(
57+
db_handle, report_data.name, report_data.category, option_class, {}
58+
)
59+
options_help = report.options_help
60+
if REPORT_FILTERS:
61+
for report_type in REPORT_FILTERS:
62+
for item in options_help["off"][2]:
63+
if item[: len(report_type)] == report_type:
64+
del options_help["off"][2][options_help["off"][2].index(item)]
65+
break
66+
return {
67+
"authors": report_data.authors,
68+
"authors_email": report_data.authors_email,
69+
"category": report_data.category,
70+
"description": report_data.description,
71+
"id": report_data.id,
72+
"name": report_data.name,
73+
"options_dict": report.options_dict,
74+
"options_help": options_help,
75+
"report_modes": report_data.report_modes,
76+
"version": report_data.version,
77+
}
78+
79+
80+
def get_reports(db_handle: DbReadBase, report_id: str = None):
81+
"""Extract and return report attributes and options."""
82+
reload_custom_filters()
83+
plugin_manager = BasePluginManager.get_instance()
84+
reports = []
85+
for report_data in plugin_manager.get_reg_reports(gui=False):
86+
if report_id is not None and report_data.id != report_id:
87+
continue
88+
if report_data.category not in REPORT_DEFAULTS:
89+
continue
90+
report = get_report_profile(db_handle, plugin_manager, report_data)
91+
reports.append(report)
92+
return reports
93+
94+
95+
def check_report_id_exists(report_id: str) -> bool:
96+
"""Check if a report ID exists."""
97+
reload_custom_filters()
98+
plugin_manager = BasePluginManager.get_instance()
99+
for report_data in plugin_manager.get_reg_reports(gui=False):
100+
if report_data.id == report_id:
101+
return True
102+
return False
103+
104+
105+
def cl_report_new(
106+
database,
107+
name,
108+
category,
109+
report_class,
110+
options_class,
111+
options_str_dict,
112+
language: Optional[str] = None,
113+
):
114+
"""Run the selected report.
115+
116+
Derived from gramps.cli.plug.cl_report.
117+
"""
118+
clr = CommandLineReport(database, name, category, options_class, options_str_dict)
119+
if category in [CATEGORY_TEXT, CATEGORY_DRAW, CATEGORY_BOOK]:
120+
if clr.doc_options:
121+
clr.option_class.handler.doc = clr.format(
122+
clr.selected_style,
123+
PaperStyle(
124+
clr.paper,
125+
clr.orien,
126+
clr.marginl,
127+
clr.marginr,
128+
clr.margint,
129+
clr.marginb,
130+
),
131+
clr.doc_options,
132+
)
133+
else:
134+
clr.option_class.handler.doc = clr.format(
135+
clr.selected_style,
136+
PaperStyle(
137+
clr.paper,
138+
clr.orien,
139+
clr.marginl,
140+
clr.marginr,
141+
clr.margint,
142+
clr.marginb,
143+
),
144+
)
145+
elif category in [CATEGORY_GRAPHVIZ, CATEGORY_TREE]:
146+
clr.option_class.handler.doc = clr.format(
147+
clr.option_class,
148+
PaperStyle(
149+
clr.paper,
150+
clr.orien,
151+
clr.marginl,
152+
clr.marginr,
153+
clr.margint,
154+
clr.marginb,
155+
),
156+
)
157+
if clr.css_filename is not None and hasattr(
158+
clr.option_class.handler.doc, "set_css_filename"
159+
):
160+
clr.option_class.handler.doc.set_css_filename(clr.css_filename)
161+
my_report = report_class(database, clr.option_class, User())
162+
my_report.set_locale(language or GrampsLocale.DEFAULT_TRANSLATION_STR)
163+
my_report.doc.init()
164+
my_report.begin_report()
165+
my_report.write_report()
166+
my_report.end_report()
167+
return clr
168+
169+
170+
def run_report(
171+
db_handle: DbReadBase,
172+
report_id: str,
173+
report_options: Dict,
174+
allow_file: bool = False,
175+
language: Optional[str] = None,
176+
):
177+
"""Generate the report."""
178+
if "off" in report_options and report_options["off"] in REPORT_FILTERS:
179+
abort(422)
180+
_resources = ResourcePath()
181+
os.environ["GRAMPS_RESOURCES"] = str(Path(_resources.data_dir).parent)
182+
reload_custom_filters()
183+
plugin_manager = BasePluginManager.get_instance()
184+
for report_data in plugin_manager.get_reg_reports(gui=False):
185+
if report_data.id == report_id:
186+
if report_data.category not in REPORT_DEFAULTS:
187+
abort(404)
188+
if "off" not in report_options:
189+
report_options["off"] = REPORT_DEFAULTS[report_data.category]
190+
file_type = "." + report_options["off"]
191+
file_type = _EXTENSION_MAP.get(file_type) or file_type
192+
if file_type not in MIME_TYPES:
193+
current_app.logger.error(f"Cannot find {file_type} in MIME_TYPES")
194+
abort(500)
195+
report_path = current_app.config.get("REPORT_DIR")
196+
os.makedirs(report_path, exist_ok=True)
197+
file_name = f"{uuid.uuid4()}{file_type}"
198+
report_options["of"] = os.path.join(report_path, file_name)
199+
report_profile = get_report_profile(db_handle, plugin_manager, report_data)
200+
validate_options(report_profile, report_options, allow_file=allow_file)
201+
module = plugin_manager.load_plugin(report_data)
202+
option_class = getattr(module, report_data.optionclass)
203+
report_class = getattr(module, report_data.reportclass)
204+
cl_report_new(
205+
db_handle,
206+
report_data.name,
207+
report_data.category,
208+
report_class,
209+
option_class,
210+
report_options,
211+
language=language,
212+
)
213+
if (
214+
file_type == ".dot"
215+
and not os.path.isfile(report_options["of"])
216+
and os.path.isfile(report_options["of"] + ".gv")
217+
):
218+
file_type = ".gv"
219+
file_name = f"{file_name}.gv"
220+
return file_name, file_type
221+
abort(404)
222+
223+
224+
def validate_options(report: Dict, report_options: Dict, allow_file: bool = False):
225+
"""Check validity of provided report options."""
226+
if report["id"] == "familylines_graph":
227+
if "gidlist" not in report_options or not report_options["gidlist"]:
228+
abort(422)
229+
for option in report_options:
230+
if option not in report["options_dict"]:
231+
abort(422)
232+
if isinstance(report["options_help"][option][2], type([])):
233+
option_list = []
234+
for item in report["options_help"][option][2]:
235+
# Some option specs include a comment part after a tab, e.g. to give
236+
# the name of a family associated with a family ID. It's the part
237+
# before the tab that's a valid value.
238+
# Some tab-separated specs also have a colon before the tab.
239+
option_list.append(item.split("\t")[0].rstrip(":"))
240+
if report_options[option] not in option_list:
241+
abort(422)
242+
continue
243+
if not isinstance(report_options[option], str):
244+
abort(422)
245+
if "A number" in report["options_help"][option][2]:
246+
try:
247+
float(report_options[option])
248+
except ValueError:
249+
abort(422)
250+
if "Size in cm" in report["options_help"][option][2]:
251+
try:
252+
float(report_options[option])
253+
except ValueError:
254+
abort(422)

gramps_webapi/api/resources/exporters.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,13 @@ def get(self, filename: str) -> Response:
200200

201201
file_type = match.group(2)
202202
file_path = os.path.join(export_path, filename)
203+
if not os.path.isfile(file_path):
204+
abort(404)
205+
date_lastmod = time.localtime(os.path.getmtime(file_path))
203206
buffer = get_buffer_for_file(file_path, delete=True)
204207
mime_type = "application/octet-stream"
205208
if file_type != ".pl" and file_type in mimetypes.types_map:
206209
mime_type = mimetypes.types_map[file_type]
207-
date_lastmod = time.localtime(os.path.getmtime(file_path))
208210
date_str = time.strftime("%Y%m%d%H%M%S", date_lastmod)
209211
download_name = f"gramps-web-export-{date_str}{file_type}"
210212
return send_file(buffer, mimetype=mime_type, download_name=download_name)

0 commit comments

Comments
 (0)