2026-06-11 23:58:46 +00:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
2026-06-18 22:09:34 +00:00
|
|
|
import time
|
2026-06-16 03:32:19 +00:00
|
|
|
from datetime import datetime, timezone
|
2026-06-19 08:06:09 +00:00
|
|
|
import jinja2
|
2026-05-10 07:08:12 +00:00
|
|
|
from fastapi.templating import Jinja2Templates
|
2026-06-16 03:32:19 +00:00
|
|
|
from markupsafe import Markup, escape
|
2026-05-23 04:20:27 +00:00
|
|
|
from devplacepy.cache import TTLCache
|
2026-06-14 14:46:36 +00:00
|
|
|
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD
|
2026-06-06 14:31:42 +00:00
|
|
|
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
2026-06-09 18:02:50 +00:00
|
|
|
from devplacepy.database import get_int_setting, get_setting, get_table
|
2026-06-27 22:31:34 +00:00
|
|
|
from devplacepy.avatar import avatar_url, avatar_seed
|
2026-05-12 10:45:52 +00:00
|
|
|
from devplacepy.utils import format_date as _format_date
|
2026-06-16 03:32:19 +00:00
|
|
|
from devplacepy.utils import time_ago as _time_ago
|
2026-06-17 14:08:28 +00:00
|
|
|
from devplacepy.utils import get_badge, is_admin, is_primary_admin, pretty_json
|
2026-06-09 18:02:50 +00:00
|
|
|
from devplacepy.attachments import format_file_size, file_icon_emoji
|
2026-06-08 15:38:33 +00:00
|
|
|
from devplacepy.content import is_owner as _owns
|
2026-06-09 18:02:50 +00:00
|
|
|
from devplacepy.customization import custom_css_tag, custom_js_tag, page_type_for
|
2026-06-09 16:48:08 +00:00
|
|
|
|
2026-06-19 08:06:09 +00:00
|
|
|
EM_DASH_FORMS = ("\u2014", "—", "—", "—", "—")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_em_dash(text: str) -> str:
|
|
|
|
|
for form in EM_DASH_FORMS:
|
|
|
|
|
text = text.replace(form, "-")
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Template(jinja2.Template):
|
|
|
|
|
def render(self, *args, **kwargs) -> str:
|
|
|
|
|
return normalize_em_dash(super().render(*args, **kwargs))
|
|
|
|
|
|
|
|
|
|
async def render_async(self, *args, **kwargs) -> str:
|
|
|
|
|
return normalize_em_dash(await super().render_async(*args, **kwargs))
|
|
|
|
|
|
|
|
|
|
|
2026-06-14 23:00:30 +00:00
|
|
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
2026-06-19 08:06:09 +00:00
|
|
|
templates.env.template_class = _Template
|
2026-06-14 23:00:30 +00:00
|
|
|
templates.env.auto_reload = TEMPLATE_AUTO_RELOAD
|
2026-05-10 07:08:12 +00:00
|
|
|
|
2026-06-08 15:38:33 +00:00
|
|
|
|
|
|
|
|
def is_self(user, uid) -> bool:
|
|
|
|
|
return bool(user) and user["uid"] == uid
|
|
|
|
|
|
|
|
|
|
|
feat: add router table entries for uploads, media, zips, forks, tools, proxy, push, docs, openai, devii, xmlrpc, devrant, dbapi, pubsub and add type annotations to cache, config, responses, schemas, seo, and stealth modules
2026-06-16 12:05:44 +00:00
|
|
|
def guest_disabled(user) -> Markup:
|
2026-06-08 15:38:33 +00:00
|
|
|
if user:
|
|
|
|
|
return Markup("")
|
2026-06-19 08:06:09 +00:00
|
|
|
return Markup(' disabled aria-disabled="true"')
|
2026-06-08 15:38:33 +00:00
|
|
|
|
|
|
|
|
|
feat: add router table entries for uploads, media, zips, forks, tools, proxy, push, docs, openai, devii, xmlrpc, devrant, dbapi, pubsub and add type annotations to cache, config, responses, schemas, seo, and stealth modules
2026-06-16 12:05:44 +00:00
|
|
|
def static_url(path) -> str:
|
2026-06-14 00:16:22 +00:00
|
|
|
if not isinstance(path, str):
|
|
|
|
|
return path
|
|
|
|
|
if not path.startswith("/static/") or path.startswith("/static/uploads/"):
|
|
|
|
|
return path
|
|
|
|
|
return f"/static/v{STATIC_VERSION}{path[len('/static'):]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates.env.globals["static_url"] = static_url
|
|
|
|
|
templates.env.globals["static_version"] = STATIC_VERSION
|
2026-06-08 15:38:33 +00:00
|
|
|
templates.env.globals["is_admin"] = is_admin
|
2026-06-17 14:08:28 +00:00
|
|
|
templates.env.globals["is_primary_admin"] = is_primary_admin
|
2026-06-08 15:38:33 +00:00
|
|
|
templates.env.globals["owns"] = _owns
|
|
|
|
|
templates.env.globals["is_self"] = is_self
|
|
|
|
|
templates.env.globals["guest_disabled"] = guest_disabled
|
|
|
|
|
|
2026-06-14 07:48:10 +00:00
|
|
|
from devplacepy.docs_devrant import devrant_endpoints
|
|
|
|
|
|
|
|
|
|
templates.env.globals["devrant_endpoints"] = devrant_endpoints
|
|
|
|
|
|
2026-06-09 16:48:08 +00:00
|
|
|
_unread_cache = TTLCache(ttl=10, max_size=1000)
|
|
|
|
|
_messages_cache = TTLCache(ttl=10, max_size=1000)
|
|
|
|
|
|
2026-05-11 01:14:43 +00:00
|
|
|
|
2026-05-16 00:49:53 +00:00
|
|
|
def clear_unread_cache(user_uid: str) -> None:
|
2026-05-23 04:20:27 +00:00
|
|
|
_unread_cache.pop(user_uid)
|
2026-05-16 00:49:53 +00:00
|
|
|
|
|
|
|
|
|
2026-06-06 14:31:42 +00:00
|
|
|
def clear_messages_cache(user_uid: str) -> None:
|
|
|
|
|
_messages_cache.pop(user_uid)
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 18:02:50 +00:00
|
|
|
def _cached_count(cache: TTLCache, table_name: str, cache_key: str, **filters) -> int:
|
|
|
|
|
cached = cache.get(cache_key)
|
2026-05-23 04:20:27 +00:00
|
|
|
if cached is not None:
|
|
|
|
|
return cached
|
2026-06-09 18:02:50 +00:00
|
|
|
table = get_table(table_name)
|
|
|
|
|
count = table.count(**filters)
|
|
|
|
|
cache.set(cache_key, count)
|
2026-05-11 01:14:43 +00:00
|
|
|
return count
|
2026-05-10 07:08:12 +00:00
|
|
|
|
2026-06-06 14:31:42 +00:00
|
|
|
|
2026-06-09 18:02:50 +00:00
|
|
|
def jinja_unread_count(user_uid: str) -> int:
|
|
|
|
|
return _cached_count(_unread_cache, "notifications", user_uid, user_uid=user_uid, read=False)
|
|
|
|
|
|
|
|
|
|
|
2026-06-06 14:31:42 +00:00
|
|
|
def jinja_unread_messages(user_uid: str) -> int:
|
2026-06-09 18:02:50 +00:00
|
|
|
return _cached_count(_messages_cache, "messages", user_uid, receiver_uid=user_uid, read=False)
|
2026-06-06 14:31:42 +00:00
|
|
|
|
2026-06-09 16:48:08 +00:00
|
|
|
|
feat: add router table entries for uploads, media, zips, forks, tools, proxy, push, docs, openai, devii, xmlrpc, devrant, dbapi, pubsub and add type annotations to cache, config, responses, schemas, seo, and stealth modules
2026-06-16 12:05:44 +00:00
|
|
|
def jinja_user_projects(user_uid: str) -> list[dict]:
|
2026-05-10 07:08:12 +00:00
|
|
|
projects = get_table("projects")
|
|
|
|
|
return list(projects.find(user_uid=user_uid))
|
|
|
|
|
|
2026-06-09 16:48:08 +00:00
|
|
|
|
2026-05-10 07:08:12 +00:00
|
|
|
templates.env.globals["get_unread_count"] = jinja_unread_count
|
2026-06-06 14:31:42 +00:00
|
|
|
templates.env.globals["get_unread_messages"] = jinja_unread_messages
|
2026-05-10 07:08:12 +00:00
|
|
|
templates.env.globals["get_user_projects"] = jinja_user_projects
|
2026-05-10 19:33:53 +00:00
|
|
|
templates.env.globals["avatar_url"] = avatar_url
|
2026-06-27 22:31:34 +00:00
|
|
|
templates.env.globals["avatar_seed"] = avatar_seed
|
2026-05-12 10:45:52 +00:00
|
|
|
templates.env.globals["format_date"] = _format_date
|
2026-06-16 03:32:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_iso(dt_str: str) -> str | None:
|
|
|
|
|
try:
|
|
|
|
|
parsed = datetime.fromisoformat(dt_str)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
return None
|
|
|
|
|
if parsed.tzinfo is None:
|
|
|
|
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
|
|
|
return parsed.isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def local_dt(dt_str: str, mode: str = "datetime") -> Markup:
|
|
|
|
|
if not dt_str:
|
|
|
|
|
return Markup("")
|
|
|
|
|
iso = _normalize_iso(dt_str)
|
|
|
|
|
if iso is None:
|
|
|
|
|
return Markup(escape(dt_str))
|
|
|
|
|
if mode == "ago":
|
|
|
|
|
fallback = _time_ago(dt_str)
|
|
|
|
|
elif mode == "date":
|
|
|
|
|
fallback = _format_date(dt_str)
|
|
|
|
|
else:
|
|
|
|
|
fallback = _format_date(dt_str, include_time=True)
|
|
|
|
|
return Markup(
|
|
|
|
|
f'<time datetime="{escape(iso)}" data-dt data-dt-mode="{escape(mode)}">'
|
|
|
|
|
f"{escape(fallback)}</time>"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates.env.globals["local_dt"] = local_dt
|
|
|
|
|
templates.env.globals["dt_ago"] = lambda dt_str: local_dt(dt_str, "ago")
|
2026-06-12 03:37:12 +00:00
|
|
|
templates.env.globals["badge_info"] = get_badge
|
2026-05-13 19:17:57 +00:00
|
|
|
templates.env.globals["TOPICS"] = TOPICS
|
2026-06-06 14:31:42 +00:00
|
|
|
templates.env.globals["REACTION_EMOJI"] = REACTION_EMOJI
|
2026-06-09 16:48:08 +00:00
|
|
|
|
|
|
|
|
|
feat: add router table entries for uploads, media, zips, forks, tools, proxy, push, docs, openai, devii, xmlrpc, devrant, dbapi, pubsub and add type annotations to cache, config, responses, schemas, seo, and stealth modules
2026-06-16 12:05:44 +00:00
|
|
|
def jinja_max_upload_size_mb() -> int:
|
2026-05-23 04:55:11 +00:00
|
|
|
return get_int_setting("max_upload_size_mb", 10)
|
2026-05-12 13:07:34 +00:00
|
|
|
|
2026-06-09 16:48:08 +00:00
|
|
|
|
feat: add router table entries for uploads, media, zips, forks, tools, proxy, push, docs, openai, devii, xmlrpc, devrant, dbapi, pubsub and add type annotations to cache, config, responses, schemas, seo, and stealth modules
2026-06-16 12:05:44 +00:00
|
|
|
def jinja_max_attachments() -> int:
|
2026-05-23 04:55:11 +00:00
|
|
|
return get_int_setting("max_attachments_per_resource", 10)
|
2026-05-12 13:07:34 +00:00
|
|
|
|
2026-06-09 16:48:08 +00:00
|
|
|
|
feat: add router table entries for uploads, media, zips, forks, tools, proxy, push, docs, openai, devii, xmlrpc, devrant, dbapi, pubsub and add type annotations to cache, config, responses, schemas, seo, and stealth modules
2026-06-16 12:05:44 +00:00
|
|
|
def jinja_allowed_file_types() -> str:
|
2026-05-12 13:07:34 +00:00
|
|
|
return get_setting("allowed_file_types", "")
|
|
|
|
|
|
2026-06-09 16:48:08 +00:00
|
|
|
|
2026-05-12 13:07:34 +00:00
|
|
|
templates.env.globals["max_upload_size_mb"] = jinja_max_upload_size_mb
|
|
|
|
|
templates.env.globals["max_attachments_per_resource"] = jinja_max_attachments
|
|
|
|
|
templates.env.globals["allowed_file_types"] = jinja_allowed_file_types
|
2026-05-13 19:17:57 +00:00
|
|
|
|
|
|
|
|
_LANGUAGE_NAMES = {
|
2026-06-09 16:48:08 +00:00
|
|
|
"python": "Python",
|
|
|
|
|
"javascript": "JavaScript",
|
|
|
|
|
"typescript": "TypeScript",
|
|
|
|
|
"html": "HTML",
|
|
|
|
|
"css": "CSS",
|
|
|
|
|
"c": "C",
|
|
|
|
|
"cpp": "C++",
|
|
|
|
|
"java": "Java",
|
|
|
|
|
"go": "Go",
|
|
|
|
|
"rust": "Rust",
|
|
|
|
|
"sql": "SQL",
|
|
|
|
|
"bash": "Bash",
|
|
|
|
|
"yaml": "YAML",
|
|
|
|
|
"json": "JSON",
|
|
|
|
|
"markdown": "Markdown",
|
|
|
|
|
"swift": "Swift",
|
|
|
|
|
"php": "PHP",
|
|
|
|
|
"ruby": "Ruby",
|
|
|
|
|
"kotlin": "Kotlin",
|
|
|
|
|
"lua": "Lua",
|
|
|
|
|
"perl": "Perl",
|
|
|
|
|
"haskell": "Haskell",
|
|
|
|
|
"elixir": "Elixir",
|
|
|
|
|
"r": "R",
|
|
|
|
|
"dart": "Dart",
|
|
|
|
|
"scala": "Scala",
|
|
|
|
|
"plaintext": "Plain Text",
|
2026-05-13 19:17:57 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-09 16:48:08 +00:00
|
|
|
|
2026-05-13 19:17:57 +00:00
|
|
|
def jinja_language_name(code: str) -> str:
|
|
|
|
|
return _LANGUAGE_NAMES.get(code, code)
|
|
|
|
|
|
2026-06-09 16:48:08 +00:00
|
|
|
|
2026-05-13 19:17:57 +00:00
|
|
|
templates.env.globals["language_name"] = jinja_language_name
|
2026-06-12 06:30:17 +00:00
|
|
|
templates.env.globals["pretty_json"] = pretty_json
|
2026-05-13 19:17:57 +00:00
|
|
|
|
|
|
|
|
templates.env.globals["format_file_size"] = format_file_size
|
|
|
|
|
templates.env.globals["file_icon_emoji"] = file_icon_emoji
|
2026-06-09 14:06:02 +00:00
|
|
|
|
|
|
|
|
templates.env.globals["custom_css_tag"] = custom_css_tag
|
|
|
|
|
templates.env.globals["custom_js_tag"] = custom_js_tag
|
|
|
|
|
templates.env.globals["page_type_for"] = page_type_for
|
2026-06-18 22:09:34 +00:00
|
|
|
|
2026-06-19 20:15:22 +00:00
|
|
|
|
|
|
|
|
def extra_head_tag() -> Markup:
|
|
|
|
|
try:
|
|
|
|
|
code = get_setting("extra_head", "")
|
|
|
|
|
if not code.strip():
|
|
|
|
|
return Markup("")
|
|
|
|
|
return Markup(code)
|
|
|
|
|
except Exception:
|
|
|
|
|
return Markup("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates.env.globals["extra_head_tag"] = extra_head_tag
|
|
|
|
|
|
2026-06-22 21:17:54 +00:00
|
|
|
from devplacepy.rendering import content_preview, render_content, render_title
|
2026-06-18 22:09:34 +00:00
|
|
|
|
|
|
|
|
templates.env.globals["render_content"] = render_content
|
|
|
|
|
templates.env.globals["render_title"] = render_title
|
2026-06-22 21:17:54 +00:00
|
|
|
templates.env.globals["content_preview"] = content_preview
|
2026-06-18 22:09:34 +00:00
|
|
|
|
|
|
|
|
|
2026-06-19 08:06:09 +00:00
|
|
|
def nav_active(request, *prefixes: str, exact: bool = False, css_class: str = "active") -> str:
|
|
|
|
|
path = request.url.path
|
|
|
|
|
for prefix in prefixes:
|
|
|
|
|
if path == prefix:
|
|
|
|
|
return css_class
|
|
|
|
|
if exact:
|
|
|
|
|
continue
|
|
|
|
|
base = prefix.rstrip("/")
|
|
|
|
|
if base and path.startswith(base + "/"):
|
|
|
|
|
return css_class
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates.env.globals["nav_active"] = nav_active
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 22:09:34 +00:00
|
|
|
def response_time_ms(request=None) -> str:
|
|
|
|
|
start = getattr(getattr(request, "state", None), "request_start", None)
|
|
|
|
|
if start is None:
|
|
|
|
|
return ""
|
|
|
|
|
return f"{(time.perf_counter() - start) * 1000:.1f}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates.env.globals["response_time_ms"] = response_time_ms
|