|
| 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) |
0 commit comments