Source code for timApp.document.macroinfo

from __future__ import annotations

import re
from copy import deepcopy
from dataclasses import dataclass, field
from html import escape
from typing import TYPE_CHECKING, Any, Mapping

from timApp.document.usercontext import UserContext
from timApp.document.viewcontext import ViewContext
from timApp.markdown.markdownconverter import (
    create_environment,
    TimSandboxedEnvironment,
)
from timApp.util.rndutils import get_rands_as_dict
from timApp.util.utils import cached_property

if TYPE_CHECKING:
    from timApp.user.user import User
    from timApp.document.document import Document


[docs]@dataclass class MacroInfo: """Represents information required for expanding macros in a DocParagraph.""" view_ctx: ViewContext doc: Document | None = None macro_map: dict[str, object] = field(default_factory=dict) """The mapping of macro keys to their values.""" macro_delimiter: str = "%%" """The delimiter used for macros in the markdown.""" user_ctx: UserContext | None = None preserve_user_macros: bool = False """If True and user is not provided, get_macros() will preserve the user-specific-macros (instead of replacing them with empty values).""" def __post_init__(self) -> None: from timApp.tim_app import app doc = self.doc self.macro_map["host"] = app.config["TIM_HOST"] if doc is not None: docinfo = doc.get_docinfo() self.macro_map.update( { "docid": doc.doc_id, "docpath": docinfo.path, "doctitle": docinfo.title, "docname": docinfo.short_name, } ) urlmacros = doc.get_settings().urlmacros() if urlmacros: self.macro_map.update( get_url_macros( self.macro_map, urlmacros, {key: val for (key, val) in self.view_ctx.urlmacros}, ) ) extramacros = self.view_ctx.extra_macros if extramacros: self.macro_map.update(extramacros) rndmacros = doc.get_settings().rndmacros() if rndmacros: self.macro_map.update( get_rnd_macros( rndmacros, self.user_ctx.user if self.user_ctx else None ) )
[docs] def get_macros(self) -> dict[str, object]: user = self.user_ctx if user is None: if not self.preserve_user_macros: return self.macro_map else: return self.get_macros_preserving_user() else: return self.get_macros_with_user_specific(user)
[docs] def get_macro_delimiter(self) -> str: return self.macro_delimiter
@cached_property def jinja_env(self) -> TimSandboxedEnvironment: return create_environment( self.macro_delimiter, self.user_ctx, self.view_ctx, self.macro_map, self.doc )
[docs] def get_macros_preserving_user(self) -> dict[str, object]: """Gets the macros and defines user-specific variables in such a way that the macro replacement for user variables does effectively nothing.""" macros = deepcopy(self.macro_map) macros.update( { "userid": f"{self.macro_delimiter}userid{self.macro_delimiter}", "username": f"{self.macro_delimiter}username{self.macro_delimiter}", "realname": f"{self.macro_delimiter}realname{self.macro_delimiter}", "useremail": f"{self.macro_delimiter}useremail{self.macro_delimiter}", "loggedUsername": f"{self.macro_delimiter}loggedUsername{self.macro_delimiter}", "userfolder": f"{self.macro_delimiter}userfolder{self.macro_delimiter}", } ) return macros
[docs] def get_macros_with_user_specific( self, user: UserContext | None = None ) -> dict[str, object]: if not user: return self.macro_map macros = deepcopy(self.macro_map) macros.update(get_user_specific_macros(user)) return macros
[docs]def get_user_specific_macros(user_ctx: UserContext) -> dict[str, str | None]: """ Gets the macros that are specific to the user. The user macros are defined as follows: - userid: The user id of the user. - username: The username of the user. - realname: The real name of the user. - useremail: The email address of the user. - loggedUsername: The username of the user that is logged in. - userfolder: The personal folder of the user. :param user_ctx: User context to get the macros for. :return: Dictionary of user macros. """ user = user_ctx.user return { "userid": escape(str(user.id)), "username": escape(user.name), "realname": escape(user.real_name) if user.real_name else None, "useremail": escape(user.email) if user.email else None, "loggedUsername": escape(user_ctx.logged_user.name), "userfolder": escape( user.get_personal_folder().path ), # personal folder object is cached and usually reused }
[docs]def get_rnd_macros( rndmacros_setting: dict[str, str], user: User | None ) -> dict[str, str]: rnd_seed = user.name if user else None state = None ret = {} rndm = rndmacros_setting if "rndnames" not in rndm: rndnames = [] for rnd_name in rndm: if rnd_name not in ["seed"]: # todo put other non rnd names here rndnames.append(rnd_name) rndm["rndnames"] = ",".join(rndnames) rands, rnd_seed, state = get_rands_as_dict(rndm, rnd_seed, state) if rands: for rnd_name, rnd in rands.items(): ret[rnd_name] = rnd return ret
urlmacros_tester = re.compile(r"[^0-9A-Za-zÅÄÖåäöÜü.,_ \-/@+=]+")
[docs]def get_url_macros( docmacros: dict[str, Any], urlmacros: Mapping[str, int | float | str], urlargs: dict[str, str], ) -> dict[str, str]: ret = {} for um in urlmacros: if not um: continue # TODO: if already value and urlmacros.get(um) then old value wins urlvalue = urlargs.get(um, urlmacros.get(um)) if not urlvalue: continue try: uvalue = None try: uvalue = float(urlvalue) except ValueError: pass if uvalue is not None: maxvalue = docmacros.get("MAX" + um, None) if maxvalue is not None: if uvalue > maxvalue: urlvalue = maxvalue minvalue = docmacros.get("MIN" + um, None) if minvalue is not None: if uvalue < minvalue: urlvalue = minvalue urlvalue = urlmacros_tester.sub("", str(urlvalue)) ret[um] = urlvalue except TypeError: pass return ret