"""
Routes for printing a document
"""
import json
import os
import shutil
import tempfile
from dataclasses import field
from pathlib import Path
from flask import current_app
from flask import g
from flask import make_response
from flask import request
from flask import send_file, Response
from timApp.auth import sessioninfo
from timApp.auth.accesshelper import (
verify_view_access,
verify_edit_access,
has_edit_access,
)
from timApp.document.docentry import DocEntry
from timApp.document.docinfo import DocInfo
from timApp.document.docparagraph import DocParagraph
from timApp.document.usercontext import UserContext
from timApp.document.viewcontext import default_view_ctx
from timApp.document.yamlblock import YamlBlock
from timApp.markdown.autocounters import (
REMOTE_REFS_KEY,
AUTOCNTS_KEY,
AUTOCNTS_PREFIX,
COUNTERS_SETTINGS_KEY,
)
from timApp.printing.documentprinter import DocumentPrinter, PrintingError, LaTeXError
from timApp.printing.printeddoc import PrintedDoc
from timApp.printing.printsettings import PrintFormat
from timApp.timdb.sqa import db
from timApp.upload.upload import add_csp_if_not_pdf
from timApp.util.flask.requesthelper import (
RouteException,
NotExist,
)
from timApp.util.flask.responsehelper import (
json_response,
add_no_cache_headers,
add_csp_header,
ok_response,
)
from timApp.util.flask.typedblueprint import TypedBlueprint
TEXPRINTTEMPLATE_KEY = "texprinttemplate"
DEFAULT_PRINT_TEMPLATE_NAME = "templates/printing/runko"
EMPTY_PRINT_TEMPLATE_NAME = "templates/printing/empty"
TEMP_DIR_PATH = tempfile.gettempdir()
DOWNLOADED_IMAGES_ROOT = os.path.join(TEMP_DIR_PATH, "tim-img-dls")
print_blueprint = TypedBlueprint("print", __name__, url_prefix="/print")
[docs]@print_blueprint.before_request
def do_before_requests() -> None:
g.user = sessioninfo.get_current_user_object()
[docs]@print_blueprint.url_value_preprocessor
def pull_doc_path(endpoint: str | None, values: dict[str, str] | None) -> None:
if not endpoint or not values:
return
if current_app.url_map.is_endpoint_expecting(endpoint, "doc_path"):
doc_path = values["doc_path"]
if doc_path is None:
raise RouteException()
g.doc_path = doc_path
g.doc_entry = DocEntry.find_by_path(doc_path)
if not g.doc_entry:
raise NotExist("Document not found")
verify_view_access(g.doc_entry)
[docs]def template_by_name(
template_name: str, isdef: bool = False
) -> tuple[DocInfo | None, int, str | None, bool]:
template_doc = DocEntry.find_by_path(template_name)
if template_doc is None:
return None, 0, f"Template not found: {template_name}", False
return template_doc, template_doc.id, None, isdef
[docs]def get_doc_template_name(doc: DocInfo) -> str | None:
texmacros = (
doc.document.get_settings().get_texmacroinfo(default_view_ctx).get_macros()
)
if not texmacros:
return None
template_name = texmacros.get(TEXPRINTTEMPLATE_KEY)
if template_name is None:
return None
return str(template_name)
[docs]def get_template_doc(
doc: DocInfo, template_doc_id: int
) -> tuple[DocInfo | None, int, str | None, bool]:
template_name = get_doc_template_name(doc)
if template_name:
return template_by_name(template_name, True)
if template_doc_id == -1:
return template_by_name(DEFAULT_PRINT_TEMPLATE_NAME, True)
if template_doc_id == 0:
return template_by_name(EMPTY_PRINT_TEMPLATE_NAME)
template_doc = DocEntry.find_by_id(template_doc_id)
if template_doc is None:
return None, 0, f"There is no template with id {str(template_doc_id)}", False
def_template = DocEntry.find_by_path(DEFAULT_PRINT_TEMPLATE_NAME)
isdef = def_template is not None and def_template.id == template_doc_id
return template_doc, template_doc_id, None, isdef
[docs]@print_blueprint.post("/<path:doc_path>")
def print_document(
doc_path: str,
file_type: str = field(metadata={"data_key": "fileType"}),
template_doc_id: int = field(metadata={"data_key": "templateDocId"}),
plugins_user_print: bool = field(metadata={"data_key": "printPluginsUserCode"}),
remove_old_images: bool = field(
metadata={"data_key": "removeOldImages"}, default=False
),
force: bool = False,
) -> Response:
if not file_type:
file_type = "pdf"
if file_type.lower() not in [f.value for f in PrintFormat]:
raise RouteException("The supplied parameter 'fileType' is invalid.")
doc: DocInfo = g.doc_entry
template_doc, template_doc_id, template_error, template_doc_def = get_template_doc(
doc, template_doc_id
)
if template_error:
raise RouteException(template_error)
print_type = PrintFormat[file_type.upper()]
if remove_old_images:
remove_images(doc.id)
existing_doc = check_print_cache(
doc_entry=doc,
template=template_doc,
file_type=print_type,
plugins_user_print=plugins_user_print,
)
# print_access_url = f'{request.url}?file_type={str(print_type.value).lower()}&template_doc_id={template_doc_id}&plugins_user_code={plugins_user_print}'
print_access_url = f"{request.url}" # create url for printed page
sep = "?"
if str(print_type.value).lower() != "pdf":
print_access_url += f"{sep}file_type={str(print_type.value).lower()}"
sep = "&"
if not template_doc_def:
print_access_url += f"{sep}template_doc_id={template_doc_id}"
sep = "&"
if plugins_user_print:
print_access_url += f"{sep}plugins_user_code={plugins_user_print}"
if force:
existing_doc = None
if existing_doc is not None and not plugins_user_print: # never cache user print
return json_response(
{"success": True, "url": print_access_url}, status_code=200
)
if template_doc is None:
raise RouteException("The template doc was not found.")
try:
create_printed_doc(
doc_entry=doc,
template_doc=template_doc,
file_type=print_type,
temp=True,
user_ctx=UserContext.from_one_user(g.user),
plugins_user_print=plugins_user_print,
urlroot="http://localhost:5000/print/",
) # request.url_root + 'print/')
except LaTeXError as err:
try:
print("Error occurred: " + str(err))
e = err.value
latex_access_url = f"{request.url}?file_type=latex&template_doc_id={template_doc_id}&plugins_user_code={plugins_user_print}"
line = e.get("line", "")
return json_response(
{
"success": True,
"url": print_access_url,
"errormsg": "<pre>" + e.get("error", "") + "</pre>",
"latex": latex_access_url,
"latexline": latex_access_url + "&line=" + line + "#L" + line,
},
status_code=201,
)
except Exception as err:
print("General error occurred: " + str(err))
raise RouteException(str(err)) # TODO: maybe there's a better error code?
# raise RouteException(str(err))
except PrintingError as err:
print("Printing occurred: " + str(err))
raise RouteException(str(err)) # TODO: maybe there's a better error code?
except Exception as err:
print("General error occurred: " + str(err))
raise RouteException(str(err)) # TODO: maybe there's a better error code?
# print_access_url = f'{request.url}?file_type={str(print_type.value).lower()}&template_doc_id={template_doc_id}&plugins_user_code={plugins_user_print}'
db.session.commit()
return json_response({"success": True, "url": print_access_url}, status_code=201)
[docs]@print_blueprint.get("/<path:doc_path>")
def get_printed_document(
doc_path: str,
file_type: str | None = None,
plugins_user_code: bool = False,
template_doc_id: int = -1,
force: bool = False,
showerror: bool = False,
) -> Response:
doc = g.doc_entry
def_file_type = "pdf"
if doc.document.get_settings().is_textplain():
def_file_type = "plain"
file_type = file_type or def_file_type
# if doc_path != doc.name and doc_path.rfind('.') >= 0: # name have been changed because . in name
# file_type = 'plain'
line = request.args.get("line")
if file_type.lower() not in (f.value for f in PrintFormat):
raise RouteException("The supplied query parameter 'file_type' was invalid.")
print_type = PrintFormat(file_type)
template_doc = None
orginal_print_type = print_type
eol_type = "native"
if print_type == PrintFormat.ICS:
print_type = PrintFormat.PLAIN
eol_type = "crlf"
if (
print_type != PrintFormat.PLAIN
and print_type != PrintFormat.RST
and print_type != PrintFormat.ICS
):
template_doc, template_doc_id, template_error, _ = get_template_doc(
doc, template_doc_id
)
if template_error:
raise RouteException(template_error)
cached = check_print_cache(
doc_entry=doc,
template=template_doc,
file_type=print_type,
plugins_user_print=plugins_user_code,
)
if force or showerror:
cached = None
pdferror = None
if cached is None:
try:
create_printed_doc(
doc_entry=doc,
template_doc=template_doc,
file_type=print_type,
temp=True,
user_ctx=UserContext.from_one_user(g.user),
plugins_user_print=plugins_user_code,
urlroot="http://localhost:5000/print/",
eol_type=eol_type,
) # request.url_root+'print/')
except PrintingError as err:
raise RouteException(str(err))
except LaTeXError as err:
pdferror = err.value
except Exception as err:
raise RouteException(str(err))
cached = check_print_cache(
doc_entry=doc,
template=template_doc,
file_type=print_type,
plugins_user_print=plugins_user_code,
)
if (pdferror and showerror) or not cached:
if not pdferror:
pdferror = {
"error": "Unknown error (LaTeX did not return an error but still no PDF was generated.)"
}
rurl = request.url
i = rurl.find("?")
rurl = rurl[:i]
latex_access_url = f"{rurl}?file_type=latex&template_doc_id={template_doc_id}&plugins_user_code={plugins_user_code}"
pdf_access_url = f"{rurl}?file_type=pdf&template_doc_id={template_doc_id}&plugins_user_code={plugins_user_code}"
line = pdferror.get("line", "")
result = (
"<!DOCTYPE html>\n"
+ "<html>"
+ "<head>\n"
+ "</head>\n"
+ "<body>\n"
+ '<div class="error">\n'
)
result += (
"LaTeX error: <pre>"
+ pdferror.get("error", "")
+ "</pre>"
+ '<p><a href="'
+ latex_access_url
+ "&line="
+ line
+ "#L"
+ line
+ '" target="_blank">Erronous LaTeX file</a></p>'
+ '<p><a href="'
+ latex_access_url
+ '" target="_blank">Created LaTeX file</a></p>'
+ '<p><a href="'
+ pdf_access_url
+ '" target="_blank">Possibly broken PDF file</a></p>'
+ '<p><a href="'
+ pdf_access_url
+ '&showerror=true">Recreate PDF</a></p>'
)
result += "\n</div>\n</body>\n</html>"
response = make_response(result)
add_no_cache_headers(response)
add_csp_header(response)
return response
mime = get_mimetype_for_format(orginal_print_type)
if not line:
response = make_response(send_file(path_or_file=cached, mimetype=mime))
add_csp_if_not_pdf(response, mime, "sandbox allow-scripts")
else: # show LaTeX with line numbers
styles = "p.red { color: red; }\n"
styles += (
".program {font-family: monospace; line-height: 1.0; }\n"
+ ".program p { -webkit-margin-before: 0em; -webkit-margin-after: 0.2em;}\n"
)
result = (
"<!DOCTYPE html>\n"
+ "<html>"
+ "<head>\n"
+ "<style>\n"
+ styles
+ "</style>\n"
+ "</head>\n"
+ "<body>\n"
+ '<div class="program">\n'
)
n = 1
with open(cached, encoding="utf8") as f:
for rivi in f:
cl = ""
if str(n) == line:
cl = ' class="red" '
result += (
"<p"
+ cl
+ ">"
+ '<a name="L'
+ str(n)
+ '" >'
+ format(n, "04d")
+ "</a> "
+ rivi.strip()
+ "</p>\n"
)
n += 1
result += "\n</div>\n</body>\n</html>"
response = make_response(result)
add_csp_header(response)
add_no_cache_headers(response)
db.session.commit()
return response
[docs]def get_setting_and_counters_par(
doc_info: DocInfo,
) -> tuple[DocParagraph | None, DocParagraph | None]:
# TODO: Make this more efective!
settings_par: DocParagraph | None = None
counters_par: DocParagraph | None = None
for par in doc_info.document: # type: DocParagraph
s = par.get_attr("settings", None)
if s is not None:
if s == "":
settings_par = par
continue
if s == COUNTERS_SETTINGS_KEY:
counters_par = par
break
return settings_par, counters_par
[docs]def add_counters_par(
doc_info: DocInfo,
settings_par: DocParagraph,
counters_par: DocParagraph | None,
values: str,
) -> DocParagraph:
new_values = f"```\n{values}```"
if counters_par:
return doc_info.document.modify_paragraph(counters_par.id, new_values)
return doc_info.document.insert_paragraph(
new_values,
insert_after_id=settings_par.id,
attrs={"settings": COUNTERS_SETTINGS_KEY},
)
[docs]def handle_doc_numbering(doc_info: DocInfo, used_names: list[str] | None) -> str:
"""
Create automatic counters for document and all referenced documents.
:param doc_info: document to handle
:param used_names: list of already used names to avoid endless recursion
:return: Possible error string
"""
errors = ""
settings_par, counters_par = get_setting_and_counters_par(doc_info)
if not settings_par:
return f"{doc_info.short_name}: Add settings par first: Press Edit settings under Cogwheel"
autocounters = doc_info.document.get_settings().autocounters()
remote_refs = autocounters.get(REMOTE_REFS_KEY, {})
remote_counter_macros = ""
for remote_ref in remote_refs:
remote_doc_path = remote_refs.get(remote_ref).get("doc", None)
if not remote_doc_path:
continue
if remote_doc_path.startswith("/"):
remote_doc_path = remote_doc_path[1:]
else:
remote_doc_path = f"{doc_info.location}/{remote_doc_path}"
remote_doc_entry = DocEntry.find_by_path(remote_doc_path)
if not remote_doc_entry:
errors += f"\n Missing: {remote_doc_path}<br>\n"
continue
# check if recurse and name not used yet
if used_names is not None and remote_doc_path not in used_names:
used_names.append(remote_doc_path)
if not has_edit_access(doc_info):
errors += f"\n No edit access to {doc_info.location}<br>\n"
else:
error = handle_doc_numbering(remote_doc_entry, used_names)
if error:
errors += error + "<br>\n"
_, remote_counters = get_setting_and_counters_par(remote_doc_entry)
if not remote_counters:
continue
counters_settings = YamlBlock.from_markdown(remote_counters.get_markdown())
cnts = counters_settings.get("macros", {}).get(AUTOCNTS_KEY, {})
if not cnts:
continue
rcnts = f" {AUTOCNTS_PREFIX}{remote_ref}: {json.dumps(cnts)}\n"
remote_counter_macros += rcnts
printer = DocumentPrinter(doc_info, template_to_use=None, urlroot="")
fullname = f"Error in {doc_info.location}/{doc_info.short_name}:<br>\n"
try:
counters = printer.get_autocounters(UserContext.from_one_user(g.user))
except PrintingError as err:
return f"{fullname}{errors}<br>\n{err}<br>\n"
new_counter_macro_values = counters.get_counter_macros() + remote_counter_macros
add_counters_par(doc_info, settings_par, counters_par, new_counter_macro_values)
if not errors:
return ""
return fullname + errors
[docs]@print_blueprint.post("/numbering/<path:doc_path>")
def get_numbering(doc_path: str, recurse: bool = False) -> Response:
"""
renumber autocounters
:param doc_path: from what document
:param recurse: Should the referenced documents be renumbered as well?
:return: ok-response
"""
doc_entry = DocEntry.find_by_path(doc_path)
if doc_entry is None:
raise NotExist(doc_path)
verify_edit_access(doc_entry) # throws exception
used_names = None
if recurse:
used_names = [doc_path]
errors = handle_doc_numbering(doc_entry, used_names)
if errors:
raise RouteException(errors)
return ok_response()
[docs]@print_blueprint.get("/templates/<path:doc_path>")
def get_templates(doc_path: str) -> Response:
doc = g.doc_entry
template_name = get_doc_template_name(doc)
if template_name: # do not give choices if template fixed in doc
return json_response({"templates": [], "doctemplate": template_name})
user = g.user
templates = DocumentPrinter.get_templates_as_dict(doc, user)
return json_response({"templates": templates, "doctemplate": ""})
[docs]def check_print_cache(
doc_entry: DocInfo,
template: DocInfo | None,
file_type: PrintFormat,
plugins_user_print: bool = False,
) -> str | None:
"""
Fetches the given document from the database.
:param doc_entry:
:param template:
:param file_type:
:param plugins_user_print:
:return:
"""
printer = DocumentPrinter(doc_entry=doc_entry, template_to_use=template, urlroot="")
# if plugins_user_print:
# path = printer.get_print_path(file_type=file_type, plugins_user_print=plugins_user_print)
# if path is not None and os.path.exists(path):
# return path
# return None
path = printer.get_printed_document_path_from_db(
file_type=file_type, plugins_user_print=plugins_user_print
)
if path is not None and os.path.exists(path):
return path
return None
# if plugins_user_print:
# return printer.get_print_path(file_type=file_type, plugins_user_print=plugins_user_print)
# return printer.get_printed_document_path_from_db(file_type=file_type)
[docs]def create_printed_doc(
doc_entry: DocInfo,
template_doc: DocInfo | None,
file_type: PrintFormat,
temp: bool,
user_ctx: UserContext,
plugins_user_print: bool = False,
urlroot: str = "",
eol_type: str = "native",
) -> str:
"""
Adds a marking for a printed document to the db
:param user_ctx: The user context.
:param doc_entry: Document that is being printed
:param template_doc: printing template used
:param file_type: File type for the document
:param temp: Is the document stored only temporarily (gets deleted after some time)
:param plugins_user_print: use users answers for plugins or not
:param urlroot: url root for this route
:param eol_type: EOL type. Same option as Pandoc (crlf, lf, native)
:return str: path to the created file
"""
printer = DocumentPrinter(
doc_entry=doc_entry, template_to_use=template_doc, urlroot=urlroot
)
path = printer.get_print_path(
file_type=file_type, plugins_user_print=plugins_user_print
)
if path.exists():
path.unlink()
folder: Path = path.parent
folder.mkdir(parents=True, exist_ok=True)
try:
printer.write_to_format(
user_ctx,
target_format=file_type,
path=path,
plugins_user_print=plugins_user_print,
eol_type=eol_type,
)
pdferror = None
except LaTeXError as err:
pdferror = err.value
except PrintingError as err:
raise PrintingError(str(err))
p_doc = PrintedDoc(
doc_id=doc_entry.id,
template_doc_id=printer.get_template_id(),
version=printer.hash_doc_print(plugins_user_print=plugins_user_print),
path_to_file=path.as_posix(),
file_type=file_type.value,
temp=temp,
)
db.session.add(p_doc)
if pdferror:
raise LaTeXError(pdferror)
return p_doc.path_to_file
[docs]def remove_images(doc_id: int) -> None:
# noinspection PyBroadException
try:
shutil.rmtree(os.path.join(DOWNLOADED_IMAGES_ROOT, str(doc_id)))
except:
pass