import json
import shutil
from pathlib import Path
from typing import Optional, Any
from timApp.auth.accesshelper import can_see_par_source
from timApp.document.docparagraph import DocParagraph, is_real_id
from timApp.document.document import Document
from timApp.document.documentparser import DocumentParser
from timApp.document.documentwriter import DocumentWriter
from timApp.document.randutils import random_id
from timApp.timdb.dbaccess import get_files_path
from timApp.timdb.exceptions import TimDbException
from timApp.user.user import User
[docs]class Clipboard:
[docs] def get_path(self) -> Path:
return get_files_path() / "clipboard"
[docs] def get(self, user: User):
return Clipboard.UserClipboard(self, user)
[docs] def clear_all(self):
path = self.get_path()
if path.exists():
shutil.rmtree(path)
[docs] class UserClipboard:
def __init__(self, parent: "Clipboard", user: User):
self.user = user
self.path = parent.get_path() / str(self.user.id)
[docs] def get_clipfilename(self) -> Path:
return self.path / "content"
[docs] def get_reffilename(self) -> Path:
return self.path / "ref-content"
[docs] def get_parreffilename(self) -> Path:
return self.path / "ref-parcontent"
[docs] def clear(self):
for name in (
self.get_clipfilename(),
self.get_reffilename(),
self.get_parreffilename(),
self.get_metafilename(),
):
if name.is_file():
name.unlink()
[docs] def clear_refs(self):
for name in (self.get_reffilename(), self.get_parreffilename()):
if name.is_file():
name.unlink()
[docs] def read(
self, as_ref: bool | None = False, force_parrefs: bool | None = False
) -> list[dict[str, str]] | None:
if as_ref:
clipfilename = (
self.get_parreffilename()
if force_parrefs
else self.get_reffilename()
)
else:
clipfilename = self.get_clipfilename()
if not clipfilename.is_file():
return None
with clipfilename.open("rt", encoding="utf-8") as clipfile:
content = clipfile.read()
dp = DocumentParser(content)
dp.validate_structure().raise_if_has_critical_issues()
return dp.get_blocks()
[docs] def write(self, pars: list[dict[str, Any]]):
self.path.mkdir(exist_ok=True, parents=True)
text = DocumentWriter(pars).get_text()
with self.get_clipfilename().open("wt", encoding="utf-8") as clipfile:
clipfile.write(text)
[docs] def write_refs(self, pars: list[DocParagraph], area_name: str | None):
self.path.mkdir(exist_ok=True, parents=True)
ref_pars = [p.create_reference(p.doc).dict() for p in pars]
reftext = DocumentWriter(ref_pars).get_text()
with self.get_parreffilename().open("wt", encoding="utf-8") as reffile:
reffile.write(reftext)
if area_name and len(pars) > 0:
self.path.mkdir(exist_ok=True, parents=True)
ref_pars = [
DocParagraph.create_area_reference(pars[0].doc, area_name).dict()
]
reftext = DocumentWriter(ref_pars).get_text()
with self.get_reffilename().open("wt", encoding="utf-8") as reffile:
reffile.write(reftext)
else:
shutil.copy(self.get_parreffilename(), self.get_reffilename())
[docs] def cut_pars(
self,
doc: Document,
par_start: str,
par_end: str,
area_name: str | None = None,
) -> list[DocParagraph]:
pars = self.copy_pars(doc, par_start, par_end, area_name, disable_ref=True)
doc.delete_section(par_start, par_end)
self.update_metadata(last_action="cut")
return pars
[docs] def copy_pars(
self,
doc: Document,
par_start: str,
par_end: str,
area_name: str | None = None,
disable_ref: bool = False,
) -> list[DocParagraph]:
par_objs = doc.get_section(par_start, par_end)
pars = [p.dict() for p in par_objs]
cannot_see_source = any(
not can_see_par_source(self.user, p) for p in par_objs
)
self.write_metadata(
area_name=area_name,
disable_ref=disable_ref,
disable_content=cannot_see_source,
last_action="copy",
)
self.write(pars)
self.write_refs(par_objs, area_name)
return par_objs
[docs] def paste_before(
self, doc: Document, par_id: str, as_ref: bool = False
) -> list[DocParagraph]:
pars = self.read(as_ref)
if pars is None:
raise TimDbException("There is nothing to paste.")
metadata = self.read_metadata()
if (
not as_ref
and metadata.get("area_name") is not None
and doc.named_section_exists(metadata["area_name"])
):
new_area_name = metadata["area_name"] + "_" + random_id()
pars[0]["attrs"]["area"] = new_area_name
pars[len(pars) - 1]["attrs"]["area_end"] = new_area_name
doc_pars = []
par_before = par_id
if is_real_id(par_before):
doc.raise_if_not_exist(par_before)
for par in reversed(pars):
# We need to reverse the sequence because we're inserting before, not after
new_par_id = (
par["id"] if not doc.has_paragraph(par["id"]) else random_id()
)
new_par = doc.insert_paragraph(
par["md"],
insert_before_id=par_before,
par_id=new_par_id,
attrs=par.get("attrs"),
)
doc_pars.append(new_par)
par_before = new_par.get_id()
self.update_metadata(last_action="paste")
doc_pars.reverse()
return doc_pars
[docs] def paste_after(
self, doc: Document, par_id: str, as_ref: bool = False
) -> list[DocParagraph]:
par_before = None
if is_real_id(par_id):
doc.raise_if_not_exist(par_id)
# todo: make the iterator accept ranges
i = doc.__iter__()
try:
while True:
if next(i).get_id() == par_id:
par_before = next(i).get_id()
raise StopIteration
except StopIteration:
pass
finally:
i.close()
return self.paste_before(doc, par_before, as_ref)