Source code for timApp.document.editing.routes

"""Routes for editing a document."""
import re
from dataclasses import dataclass

from flask import Blueprint, render_template
from flask import current_app
from flask import request

from timApp.admin.associate_old_uploads import upload_regexes
from timApp.answer.answer import Answer
from timApp.auth.accesshelper import (
    verify_edit_access,
    verify_view_access,
    get_doc_or_abort,
    verify_teacher_access,
    verify_manage_access,
    verify_ownership,
    verify_seeanswers_access,
    has_edit_access,
)
from timApp.auth.get_user_rights_for_item import get_user_rights_for_item
from timApp.auth.sessioninfo import (
    get_current_user_object,
    logged_in,
    get_current_user_group,
    user_context_with_logged_in,
)
from timApp.bookmark.bookmarks import LAST_EDITED_GROUP
from timApp.document.docentry import DocEntry
from timApp.document.docinfo import DocInfo
from timApp.document.docparagraph import DocParagraph
from timApp.document.docsettings import DocSettings
from timApp.document.document import Document, get_duplicate_id_msg
from timApp.document.editing.documenteditresult import DocumentEditResult
from timApp.document.editing.editrequest import get_pars_from_editor_text, EditRequest
from timApp.document.editing.globalparid import GlobalParId
from timApp.document.editing.proofread import proofread_pars, process_spelling_errors
from timApp.document.exceptions import ValidationException, ValidationWarning
from timApp.document.hide_names import is_hide_names
from timApp.document.post_process import post_process_pars
from timApp.document.preloadoption import PreloadOption
from timApp.document.prepared_par import PreparedPar
from timApp.document.translation.synchronize_translations import (
    synchronize_translations,
)
from timApp.document.version import Version
from timApp.document.viewcontext import ViewRoute, ViewContext, default_view_ctx
from timApp.document.yamlblock import YamlBlock
from timApp.item.validation import validate_uploaded_document_content
from timApp.markdown.markdownconverter import md_to_html
from timApp.notification.notification import NotificationType
from timApp.notification.notify import notify_doc_watchers
from timApp.plugin.plugin import Plugin
from timApp.plugin.qst.qst import question_convert_js_to_yaml
from timApp.plugin.save_plugin import save_plugin
from timApp.readmark.readings import mark_read
from timApp.timdb.dbaccess import get_timdb
from timApp.timdb.exceptions import TimDbException
from timApp.timdb.sqa import db
from timApp.upload.uploadedfile import UploadedFile
from timApp.util.flask.requesthelper import (
    verify_json_params,
    use_model,
    RouteException,
    NotExist,
)
from timApp.util.flask.responsehelper import json_response, ok_response, Response
from timApp.util.utils import get_error_html

edit_page = Blueprint("edit_page", __name__, url_prefix="")  # TODO: Better URL prefix.


[docs]@edit_page.post("/update/<int:doc_id>") def update_document(doc_id): """Route for updating a document as a whole. :param doc_id: The id of the document to be modified. :return: A JSON object containing the versions of the document. """ timdb = get_timdb() docentry = get_doc_or_abort(doc_id) verify_edit_access(docentry) if "file" in request.files: file = request.files["file"] content = validate_uploaded_document_content(file) original = request.form["original"] strict_validation = not request.form.get("ignore_warnings", False) elif "template_name" in request.get_json(): template = DocEntry.find_by_path(request.get_json()["template_name"]) if not template: raise NotExist("Template not found") verify_view_access(template) content = template.document.export_markdown() if content == "": raise RouteException("The selected template is empty.") existing_pars = docentry.document_as_current_user.get_paragraphs() if existing_pars: raise RouteException( "Cannot load a template because the document is not empty." ) original = "" strict_validation = True else: request_json = request.get_json() if "fulltext" not in request_json: raise RouteException("Malformed request - fulltext missing.") content = request_json["fulltext"] original = request_json["original"] strict_validation = not request_json.get("ignore_warnings", False) if original is None: raise RouteException("Missing parameter: original") if content is None: return json_response({"message": "Failed to convert the file to UTF-8."}, 400) doc = docentry.document_as_current_user doc.preload_option = PreloadOption.all ver_before = doc.get_version() try: # To verify view rights for possible referenced paragraphs, we call this first: editor_pars = get_pars_from_editor_text( doc, content, break_on_elements=True, skip_access_check=True ) # TODO: Access check should be more fine-grained. Should only check pars that were actually edited. for p in doc.get_paragraphs(): verify_par_edit_access(p) _, _, edit_result = doc.update(content, original, strict_validation) check_and_rename_pluginnamehere(editor_pars, doc) old_pars = doc.get_paragraphs() for op, ep in zip(old_pars, editor_pars): if ep.get_attr("taskId") and op.get_attr("taskId"): if ep.get_attr("taskId") != op.get_attr("taskId"): p = doc.modify_paragraph( par_id=op.get_id(), new_text=op.get_markdown(), new_attrs=ep.get_attrs(), ) edit_result.changed.append(p) if not edit_result.empty: docentry.update_last_modified() db.session.commit() synchronize_translations(docentry, edit_result) except ValidationWarning as e: return json_response({"error": str(e), "is_warning": True}, status_code=400) except (TimDbException, ValidationException) as e: raise RouteException(str(e)) pars = doc.get_paragraphs() return manage_response(docentry, pars, timdb, ver_before)
[docs]def manage_response( docentry: DocInfo, pars: list[DocParagraph], timdb, ver_before: Version ): doc = docentry.document_as_current_user chg = doc.get_changelog() notify_doc_watchers( docentry, "", NotificationType.DocModified, old_version=ver_before ) duplicates = check_duplicates(pars, doc) db.session.commit() return json_response( {"versions": chg, "fulltext": doc.export_markdown(), "duplicates": duplicates} )
[docs]@edit_page.post("/postNewTaskNames/") def rename_task_ids(): timdb = get_timdb() doc_id, duplicates = verify_json_params("docId", "duplicates") manage_view = verify_json_params("manageView", require=False, default=False) docinfo = get_doc_or_abort(doc_id) verify_edit_access(docinfo) doc = docinfo.document_as_current_user ver_before = doc.get_version() pars = [] old_pars = doc.get_paragraphs() # Get paragraphs with taskIds for paragraph in old_pars: if not paragraph.is_task(): old_pars.remove(paragraph) i = 0 while len(duplicates) > i: duplicate = duplicates[i] # Check that paragraphs that were to be modified aren't deleted if not doc.has_paragraph(duplicate[2]): duplicates.remove(duplicate) continue else: original_par = doc.get_paragraph(duplicate[2]) verify_par_edit_access(original_par) attrs = original_par.get_attrs() if attrs["taskId"]: # If a new taskId was given use that if duplicate[1]: attrs["taskId"] = duplicate[1] duplicates.remove(duplicate) # Otherwise determine a new one else: if old_pars: # Remove the duplicate in question from duplicates duplicates.remove(duplicate) task_id = get_next_available_task_id( attrs, old_pars, duplicates, duplicate[2] ) attrs["taskId"] = task_id # Modify the paragraph with the new taskId par = doc.modify_paragraph( par_id=duplicate[2], new_text=original_par.get_markdown(), new_attrs=attrs, ) pars.append(par) # Update old pars for old_par in old_pars: if old_par.get_id() == duplicate[2]: old_pars.remove(old_par) break old_pars.append(par) if not manage_view: return par_response( pars, docinfo, spellcheck=False, update_cache=current_app.config["IMMEDIATE_PRELOAD"], ) else: return manage_response(docinfo, pars, timdb, ver_before)
[docs]@edit_page.post("/postParagraphQ/") def modify_paragraph_q(): """Route for modifying a question editor paragraph in a document. :return: A JSON object containing the paragraphs in HTML form along with JS, CSS and Angular module dependencies. """ question_data, doc_id, par_id, is_task = verify_json_params( "question", "docId", "par", "isTask" ) (task_id,) = verify_json_params("taskId", require=False) md = question_convert_js_to_yaml(question_data, is_task, task_id) ret = modify_paragraph_common(doc_id, md, par_id, par_next_id=None) return ret
[docs]@edit_page.post("/postParagraph/") def modify_paragraph(): """Route for modifying a paragraph in a document. :return: A JSON object containing the paragraphs in HTML form along with JS, CSS and Angular module dependencies. """ doc_id, md, par_id = verify_json_params("docId", "text", "par") (par_next_id,) = verify_json_params("par_next", require=False) return modify_paragraph_common(doc_id, md, par_id, par_next_id)
[docs]def verify_par_edit_access(par: DocParagraph): """Verifies that the current user has edit access to the specified DocParagraph.""" edit_attr = par.get_attr("edit", "edit") message = ( f"Only users with {edit_attr} access can edit this paragraph ({par.get_id()})." ) d = par.doc.get_docinfo() if edit_attr == "edit": verify_edit_access(d, message=message) elif edit_attr == "teacher": verify_teacher_access(d, message=message) elif edit_attr == "see_answers": verify_seeanswers_access(d, message=message) elif edit_attr == "manage": verify_manage_access(d, message=message) elif edit_attr == "owner": verify_ownership(d, message=message)
[docs]def modify_paragraph_common(doc_id: int, md: str, par_id: str, par_next_id: str | None): docinfo = get_doc_or_abort(doc_id) verify_edit_access(docinfo) doc = docinfo.document_as_current_user edit_request = EditRequest.from_request(doc, text=md) area_start = edit_request.area_start area_end = edit_request.area_end editing_area = edit_request.editing_area try: editor_pars = edit_request.get_pars(skip_access_check=True) except ValidationException as e: raise RouteException(str(e)) editor_pars = check_and_rename_pluginnamehere(editor_pars, doc) if editing_area: try: curr_section = doc.get_section(area_start, area_end) for p in curr_section: verify_par_edit_access(p) new_start, new_end, edit_result = doc.update_section( md, area_start, area_end ) pars = doc.get_section(new_start, new_end) except (ValidationException, TimDbException) as e: raise RouteException(str(e)) else: try: original_par = doc.get_paragraph(par_id) except TimDbException as e: raise NotExist(str(e)) edit_result = DocumentEditResult() pars = [] pars_to_add = editor_pars[1:] abort_if_duplicate_ids(doc, pars_to_add) p = editor_pars[0] # The ID of the first paragraph needs to match the ID of the paragraph to modify # This is needed for any edit logic that requires the ID of the paragraph (e.g. heading numbering) p.set_id(par_id) tr_opt = edit_request.mark_translated if tr_opt is None: pass elif tr_opt: if p.is_translation(): deref = mark_as_translated(p) if not deref: raise RouteException("Paragraph is not a translation.") else: p.set_attr("rt", None) if p.is_translation(): mark_translation_as_checked(p) if p.is_different_from(original_par): verify_par_edit_access(original_par) par = doc.modify_paragraph_obj(par_id=par_id, p=p) pars.append(par) edit_result.changed.append(par) else: # If the first paragraph was not modified at all, append the original one pars.append(original_par) for p in pars_to_add: par = doc.insert_paragraph_obj(p, insert_before_id=par_next_id) pars.append(par) edit_result.added.append(par) if not edit_result.empty: docinfo.update_last_modified() mark_pars_as_read_if_chosen(pars, doc) synchronize_translations(docinfo, edit_result) notify_doc_watchers( docinfo, md, NotificationType.ParModified, par=pars[0] if pars else None, old_version=edit_request.old_doc_version, ) return par_response( pars, docinfo, spellcheck=False, update_cache=current_app.config["IMMEDIATE_PRELOAD"], edit_request=edit_request, edit_result=edit_result, )
[docs]def mark_as_translated(p: DocParagraph): try: deref = p.get_referenced_pars() except TimDbException: deref = None if deref: p.set_attr("rt", deref[0].get_hash()) return deref
[docs]def mark_translation_as_checked(p: DocParagraph) -> None: """ Mark a paragraph as checked by removing its mt-attribute. :param p: The paragraph to mark as checked. :return: None. """ p.set_attr("mt", None)
[docs]def abort_if_duplicate_ids(doc: Document, pars_to_add: list[DocParagraph]): conflicting_ids = {p.get_id() for p in pars_to_add} & set(doc.get_par_ids()) if conflicting_ids: raise RouteException(get_duplicate_id_msg(conflicting_ids))
[docs]@edit_page.post("/preview/<int:doc_id>") def preview_paragraphs(doc_id): """Route for previewing paragraphs. :param doc_id: The id of the document in which the preview will be rendered. :return: A JSON object containing the paragraphs in HTML form along with JS, CSS and Angular module dependencies. """ (text,) = verify_json_params("text") (proofread,) = verify_json_params("proofread", require=False, default=False) (settings,) = verify_json_params("settings", require=False, default={}) extra_doc_settings = ( YamlBlock(settings) if settings and isinstance(settings, dict) else None ) docinfo = get_doc_or_abort(doc_id) rjson = request.get_json() if not rjson.get("isComment"): doc = docinfo.document edit_request = EditRequest.from_request(doc, preview=True) try: blocks = edit_request.get_pars() except ValidationException as e: blocks = [DocParagraph.create(doc=doc, md="", html=get_error_html(e))] proofread = False edit_request = None return par_response( blocks, docinfo, proofread, edit_request=edit_request, extra_doc_settings=extra_doc_settings, ) else: comment_html = md_to_html(text) if proofread: comment_html = process_spelling_errors(comment_html).new_html return json_response({"texts": comment_html, "js": [], "css": []})
[docs]def update_associated_uploads(pars: list[DocParagraph], doc: DocInfo): for p in pars: md = p.get_markdown() for r in upload_regexes: c = re.compile(r, re.DOTALL) for m in c.finditer(md): u_id = m.group("id") name = m.group("name") up = UploadedFile.get_by_id_and_filename(int(u_id), name) if not up: continue if not verify_manage_access(up, check_parents=True, require=False): continue if up.block in doc.children: continue doc.children.append(up.block)
[docs]def par_response( pars: list[DocParagraph], docu: DocInfo, spellcheck=False, update_cache=False, edit_request: EditRequest | None = None, edit_result: DocumentEditResult | None = None, filter_return: GlobalParId | None = None, partial_doc_pars: bool = False, extra_doc_settings: YamlBlock | None = None, ): """Return a JSON response containing updated paragraphs and updated HTMLs. ..note:: Applies additional processing to the paragraphs (e.g. spellchecking, filtering). :param pars: Paragraphs to process. :param docu: Document to which the paragraphs belong. :param spellcheck: If True, spellcheck the paragraph texts and return HTML with spellcheck suggestions. :param update_cache: If True, updates the HTML cache for the paragraphs. :param edit_request: Full edit request. :param edit_result: Result of the document edit request. :param filter_return: Return only paragraphs with this document and paragraph id. :param partial_doc_pars: If True, assumes that pars list includes partial document (e.g. areas may be incomplete). The option disables some checks that would be otherwise done for full paragraphs. :param extra_doc_settings: Extra settings to apply to the paragraph. :return: JSON object containing HTMLs, JS and CSS dependencies of changed paragraphs. """ user_ctx = user_context_with_logged_in(None) doc = docu.document new_doc_version = doc.get_version() settings = doc.get_settings() if extra_doc_settings: settings = DocSettings(doc, settings.get_dict().merge_with(extra_doc_settings)) if edit_result: preview = False else: preview = bool(edit_request and edit_request.preview) if edit_request: view_ctx = ViewContext( edit_request.viewname or ViewRoute.View, preview, hide_names_requested=is_hide_names(), partial=partial_doc_pars, ) else: view_ctx = ViewContext( ViewRoute.View, preview, hide_names_requested=is_hide_names(), partial=partial_doc_pars, ) if update_cache: changed_pars = DocParagraph.preload_htmls( doc.get_paragraphs(include_preamble=True), settings, view_ctx, persist=update_cache, ) else: changed_pars = [] ctx = None if edit_request: ctx = edit_request.context_par # If the document was changed, there is no HTML cache for the new version, so we "cheat" by lying the # document version so that the preload_htmls call is still fast. if edit_result: for p in pars: assert p.doc is doc doc.version = edit_request.old_doc_version doc.insert_temporary_pars(edit_request.get_pars(), ctx) DocParagraph.preload_htmls( pars, settings, view_ctx, context_par=ctx, persist=update_cache ) trdiff = None # Do not check for duplicates for preview because the operation is heavy if not preview: duplicates = check_duplicates(pars, doc) if edit_request and logged_in(): update_associated_uploads(pars, docu) if current_app.config["BOOKMARKS_ENABLED"]: bms = get_current_user_object().bookmarks bms.add_bookmark( LAST_EDITED_GROUP, docu.title, docu.get_relative_url_for_view( (edit_request.viewname or ViewRoute.View).value ), move_to_top=True, limit=current_app.config["LAST_EDITED_BOOKMARK_LIMIT"], ).save_bookmarks() else: duplicates = None if len(pars) == 1: p = pars[0] if p.is_translation(): try: deref = p.get_referenced_pars()[0].ref_chain except TimDbException: pass else: newest_exported = deref.get_exported_markdown() old_exported = "" rt = p.get_attr("rt") if rt: try: old_par = DocParagraph.get(deref.doc, deref.get_id(), rt) except TimDbException: pass else: old_exported = old_par.get_exported_markdown() trdiff = {"old": old_exported, "new": newest_exported} post_process_result = post_process_pars( doc, pars, user_ctx, view_ctx, filter_return=filter_return ) changed_post_process_result = post_process_pars( doc, changed_pars, user_ctx, view_ctx ) original_par = edit_request.original_par if edit_request else None if original_par and not has_edit_access(docu): original_par = None if spellcheck: proofed_text = proofread_pars(post_process_result.texts) for p, r in zip(post_process_result.texts, proofed_text): p.output = r.new_html final_texts = post_process_result.texts r = json_response( { "texts": render_template( "partials/paragraphs.jinja2", text=final_texts, rights=get_user_rights_for_item( doc.get_docinfo(), user_ctx.logged_user ), preview=preview, hide_readmarks=settings.hide_readmarks(), ), "js": post_process_result.js_paths, "css": post_process_result.css_paths, "trdiff": trdiff, "changed_pars": { p.id: render_template( "partials/paragraphs.jinja2", text=[p], rights=get_user_rights_for_item( doc.get_docinfo(), user_ctx.logged_user ), hide_readmarks=settings.hide_readmarks(), ) for p in changed_post_process_result.texts if not is_area_start_or_end(p) }, "version": new_doc_version, "duplicates": duplicates, "original_par": { "md": original_par.get_markdown(), "attrs": original_par.get_attrs(), } if original_par else None, "new_par_ids": edit_result.new_par_ids if edit_result else None, } ) db.session.commit() return r
[docs]def is_area_start_or_end(p: PreparedPar): return p.areainfo is not None or p.attrs.get("area") is not None
# Gets next available name for plugin
[docs]def get_next_available_task_id(attrs, old_pars, duplicates, par_id): task_id = attrs["taskId"] need_new_task_id = False # First try with original name i = 0 while i < len(old_pars): if old_pars[i].get_attr("taskId") == task_id: old_par_id = old_pars[i].get_id() # Ignore pars that are to be renamed if old_par_id == par_id: i += 1 continue else: for par in duplicates: if old_par_id == par[2]: # Flip this bool value for convenience need_new_task_id = not need_new_task_id break need_new_task_id = not need_new_task_id if need_new_task_id: break else: i += 1 continue else: i += 1 # If there was no previous par with the same task id keep it if not need_new_task_id: return task_id # Otherwise, determine a new one else: # Split the name into text and trailing number task_id_body = "" task_id_number = None i = len(task_id) - 1 while i >= 0: if task_id[i] in "0123456789": i -= 1 else: text_part = task_id[: i + 1] task_id_body = text_part if i < len(task_id) - 1: task_id_number = int(task_id[(i + 1) :]) break if not task_id_body: task_id_body = task_id if task_id_number is not None: task_id = task_id_body + str(task_id_number) else: task_id_number = 1 j = 0 while j < len(old_pars): if old_pars[j].get_attr("taskId") == task_id: task_id_number += 1 task_id = task_id_body + str(task_id_number) j = 0 else: j += 1 return task_id
# Automatically rename plugins with name pluginnamehere
[docs]def check_and_rename_pluginnamehere(blocks: list[DocParagraph], doc: Document): # Get the paragraphs from the document with taskids old_pars = None # lazy load for old_pars i = 1 j = 0 # For all blocks check if taskId is pluginnamehere, if it is find next available name. for p in blocks: # go through all new pars if they need to be renamed if p.is_task(): task_id = p.get_attr("taskId") if task_id == "PLUGINNAMEHERE": if old_pars is None: # now old_pars is needed, load them once pars = doc.get_paragraphs() old_pars = [] for paragraph in pars: if not paragraph.is_task(): old_pars.append(paragraph) task_id = "Plugin" + str(i) while j < len(old_pars): if task_id == old_pars[j].get_attr("taskId"): i += 1 task_id = "Plugin" + str(i) j = 0 else: j += 1 p.set_attr("taskId", task_id) old_pars.append(p) j = 0 return blocks
# Check new paragraphs with plugins for duplicate task ids
[docs]def check_duplicates(pars, doc): duplicates = [] all_pars = None # cache all_pars for par in pars: if par.is_task(): if all_pars is None: # now we need the pars doc.clear_mem_cache() docpars = doc.get_paragraphs() all_pars = [] for paragraph in docpars: if paragraph.is_task(): all_pars.append(paragraph) duplicate = [] task_id = par.get_attr("taskId") par_id = par.get_id() count_of_same_task_ids = 0 j = 0 while j < len(all_pars): if ( all_pars[j].get_id() != par_id and all_pars[j].get_attr("taskId") == task_id ): # count not self count_of_same_task_ids += 1 if count_of_same_task_ids > 0: duplicate.append(task_id) duplicate.append(par.get_id()) task_id_to_check = str(doc.doc_id) + "." + task_id if Answer.query.filter_by(task_id=task_id_to_check).first(): duplicate.append("hasAnswers") duplicates.append(duplicate) break j += 1 return duplicates
[docs]def mark_pars_as_read_if_chosen(pars, doc): """Marks the specified paragraphs as read if tags.markread is true in request's JSON data. :type doc: Document :type pars: list[DocParagraph] :param pars: The paragraphs to be marked as read :param doc: The document to which the paragraphs belong. """ mr = (request.get_json(silent=True) or {}).get("tags", {}).get("markread") if mr: for p in pars: mark_read(get_current_user_group(), doc, p)
[docs]@edit_page.post("/cancelChanges/") def cancel_save_paragraphs(): doc_id, original_par, new_pars, par_id = verify_json_params( "docId", "originalPar", "newPars", "parId" ) docentry = get_doc_or_abort(doc_id) verify_edit_access(docentry) doc = docentry.document_as_current_user for new_par in new_pars: try: par = doc.get_paragraph(new_par) except TimDbException: continue else: verify_par_edit_access(par) doc.delete_paragraph(new_par) if original_par: orig = doc.get_paragraph(par_id) verify_par_edit_access(orig) doc.modify_paragraph( par_id=par_id, new_text=original_par.get("md"), new_attrs=original_par.get("attrs"), ) return json_response({"status": "cancel"})
[docs]@edit_page.post("/newParagraphQ/") def add_paragraph_q(): question_data, doc_id, par_next_id, is_task = verify_json_params( "question", "docId", "par_next", "isTask" ) (task_id,) = verify_json_params("taskId", require=False) md = question_convert_js_to_yaml(question_data, is_task, task_id) return add_paragraph_common(md, doc_id, par_next_id)
[docs]@edit_page.post("/newParagraph/") def add_paragraph(): """Route for adding a new paragraph to a document. :return: A JSON object containing the paragraphs in HTML form along with JS, CSS and Angular module dependencies. """ md, doc_id = verify_json_params("text", "docId") (par_next_id,) = verify_json_params("par_next", require=False) return add_paragraph_common(md, doc_id, par_next_id)
[docs]def add_paragraph_common(md: str, doc_id: int, par_next_id: str | None): docinfo = get_doc_or_abort(doc_id) verify_edit_access(docinfo) doc = docinfo.document_as_current_user if par_next_id and not doc.has_paragraph(par_next_id): raise RouteException(doc.get_par_not_found_msg(par_next_id)) edit_result = DocumentEditResult() edit_request = EditRequest.from_request(doc, md) try: editor_pars = edit_request.get_pars() except ValidationException as e: raise RouteException(str(e)) abort_if_duplicate_ids(doc, editor_pars) editor_pars = check_and_rename_pluginnamehere(editor_pars, doc) pars = [] for p in editor_pars: par = doc.insert_paragraph_obj(p, insert_before_id=par_next_id) pars.append(par) edit_result.added.append(par) if not edit_result.empty: docinfo.update_last_modified() mark_pars_as_read_if_chosen(pars, doc) synchronize_translations(docinfo, edit_result) if pars: notify_doc_watchers( docinfo, md, NotificationType.ParAdded, par=pars[0], old_version=edit_request.old_doc_version, ) return par_response( pars, docinfo, spellcheck=False, update_cache=current_app.config["IMMEDIATE_PRELOAD"], edit_result=edit_result, edit_request=edit_request, )
[docs]@edit_page.post("/deleteParagraph/<int:doc_id>") def delete_paragraph(doc_id): """Route for deleting a paragraph from a document. :param doc_id: The id of the document. :return: A JSON object containing the version of the new document. """ docinfo = get_doc_or_abort(doc_id) verify_edit_access(docinfo) area_start, area_end = verify_json_params("area_start", "area_end", require=False) doc = docinfo.document_as_current_user version_before = doc.get_version() if area_end and area_start: for p in (area_start, area_end): if not doc.has_paragraph(p): raise RouteException(f"Paragraph {p} does not exist") md = doc.export_section(area_start, area_end) curr_section = doc.get_section(area_start, area_end) for p in curr_section: verify_par_edit_access(p) edit_result = doc.delete_section(area_start, area_end) else: (par_id,) = verify_json_params("par") if not doc.has_paragraph(par_id): raise RouteException(f"Paragraph {par_id} does not exist") par = doc.get_paragraph(par_id) verify_par_edit_access(par) md = par.get_markdown() doc.delete_paragraph(par_id) edit_result = DocumentEditResult(deleted=[par]) if not edit_result.empty: docinfo.update_last_modified() synchronize_translations(docinfo, edit_result) notify_doc_watchers( docinfo, md, NotificationType.ParDeleted, old_version=version_before ) return par_response( [], docinfo, spellcheck=False, update_cache=current_app.config["IMMEDIATE_PRELOAD"], edit_result=edit_result, )
[docs]@edit_page.get("/getUpdatedPars/<int:doc_id>") def get_updated_pars(doc_id): """Gets updated paragraphs that were changed e.g. as the result of adding headings or modifying macros. :param doc_id: The document id. """ d = get_doc_or_abort(doc_id) verify_view_access(d) d.document.preload_option = PreloadOption.all return par_response( [], d, spellcheck=False, update_cache=True, partial_doc_pars=True )
[docs]@edit_page.post("/name_area/<int:doc_id>/<area_name>") def name_area(doc_id, area_name): area_start, area_end = verify_json_params("area_start", "area_end", require=True) (options,) = verify_json_params("options", require=True) docentry = get_doc_or_abort(doc_id) verify_edit_access(docentry) if not area_name or " " in area_name or "´" in area_name: raise RouteException("Invalid area name") doc = docentry.document_as_current_user area_attrs = {"area": area_name} area_title = "" after_title = "" if options.get("collapsible"): area_attrs["collapse"] = "true" if options.get("collapse") else "false" if "title" in options: hlevel = options.get("hlevel", 0) if hlevel: area_title = ( "".join(["#" for _ in range(0, hlevel)]) + " " + options["title"] ) else: after_title = "\n" + options["title"] if options.get("timed"): if options.get("starttime"): area_attrs["starttime"] = str(options.get("starttime")) if options.get("endtime"): area_attrs["endtime"] = str(options.get("endtime")) if options.get("alttext"): area_attrs["alttext"] = str(options.get("alttext")) area_start = doc.insert_paragraph( area_title + after_title, insert_before_id=area_start, attrs=area_attrs ) area_end = doc.insert_paragraph( "", insert_after_id=area_end, attrs={"area_end": area_name} ) synchronize_translations(docentry, DocumentEditResult(added=[area_start, area_end])) return par_response( doc.get_named_section(area_name), docentry, update_cache=current_app.config["IMMEDIATE_PRELOAD"], )
[docs]@edit_page.post("/unwrap_area/<int:doc_id>/<area_name>") def unwrap_area(doc_id, area_name): docentry = get_doc_or_abort(doc_id) verify_edit_access(docentry) if not area_name or " " in area_name or "´" in area_name: raise RouteException("Invalid area name") try: doc = docentry.document_as_current_user area_pars = doc.get_named_section(area_name) # Remove the starting and ending paragraphs of the area area_start = area_pars[0] area_end = area_pars[-1] verify_par_edit_access(area_start) verify_par_edit_access(area_end) doc.delete_paragraph(area_start.get_id()) doc.delete_paragraph(area_end.get_id()) except TimDbException as e: raise RouteException(str(e)) return ok_response()
[docs]@edit_page.post("/markTranslated/<int:doc_id>") def mark_translated_route(doc_id): d = get_doc_or_abort(doc_id) verify_edit_access(d) for p in d.document_as_current_user.get_paragraphs(): if p.is_translation() and not p.is_setting(): old_rt = p.get_attr("rt") mark_as_translated(p) if old_rt != p.get_attr("rt"): p.save() return ok_response()
[docs]@edit_page.post("/markChecked/<int:doc_id>") def mark_all_checked_route(doc_id: int) -> Response: """ Marks all the paragraphs in a translation document checked. :param doc_id: The id of the translation document to be handled. :return: OK response if successful """ d = get_doc_or_abort(doc_id) verify_edit_access(d) for p in d.document_as_current_user.get_paragraphs(): if p.is_translation() and not p.is_setting(): old_rc = p.get_attr("mt") mark_translation_as_checked(p) if old_rc is not None: p.save() return ok_response()
[docs]@edit_page.post("/markChecked/<int:doc_id>/<par_id>") def mark_checked_route(doc_id: int, par_id: str) -> Response: """ Marks a paragraph checked. :param doc_id: The id of the document the paragraph belongs to. :param par_id: The id of the paragraph to be marked checked. :return: The modified paragraph. """ d = get_doc_or_abort(doc_id) verify_edit_access(d) doc = d.document_as_current_user par = doc.get_paragraph(par_id=par_id) old_rc = par.get_attr("mt") mark_translation_as_checked(par) if old_rc is not None: par.save() return par_response( [par], d, update_cache=current_app.config["IMMEDIATE_PRELOAD"], )
[docs]@dataclass class DrawIODataModel: data: str par_id: str doc_id: int
[docs]@edit_page.put("/jsframe/drawIOData") @use_model(DrawIODataModel) def set_drawio_base(args: DrawIODataModel): data, par_id, doc_id = args.data, args.par_id, args.doc_id doc = get_doc_or_abort(doc_id) verify_edit_access(doc) try: par = doc.document_as_current_user.get_paragraph(par_id) except TimDbException as e: raise NotExist(str(e)) plug = Plugin.from_paragraph(par, default_view_ctx) if plug.type != "csPlugin" or plug.values.get("type", "") != "drawio": raise RouteException("Invalid target") plug.values["data"] = data save_plugin(plug, max_attr_width=float("inf")) return ok_response()