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-07-04 22:08:20 +00:00
|
|
|
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
|
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-07-04 22:08:20 +00:00
|
|
|
from devplacepy.services import presence
|
2026-06-09 16:48:08 +00:00
|
|
|
|
2026-06-14 23:00:30 +00:00
|
|
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
|
|
|
|
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-07-04 22:08:20 +00:00
|
|
|
templates.env.globals["is_online"] = presence.is_online
|
|
|
|
|
templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS
|
2026-06-08 15:38:33 +00:00
|
|
|
|
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-07-06 03:57:47 +00:00
|
|
|
_projects_cache = TTLCache(ttl=10, max_size=1000)
|
2026-06-09 16:48:08 +00:00
|
|
|
|
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-07-06 03:57:47 +00:00
|
|
|
def clear_user_projects_cache(user_uid: str) -> None:
|
|
|
|
|
_projects_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-07-06 03:57:47 +00:00
|
|
|
cached = _projects_cache.get(user_uid)
|
|
|
|
|
if cached is not None:
|
|
|
|
|
return cached
|
2026-05-10 07:08:12 +00:00
|
|
|
projects = get_table("projects")
|
2026-07-06 03:57:47 +00:00
|
|
|
rows = list(projects.find(user_uid=user_uid, deleted_at=None))
|
|
|
|
|
_projects_cache.set(user_uid, rows)
|
|
|
|
|
return rows
|
2026-05-10 07:08:12 +00:00
|
|
|
|
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-07-09 02:52:54 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def award_date(dt_str: str) -> Markup:
|
|
|
|
|
if not dt_str:
|
|
|
|
|
return Markup("")
|
|
|
|
|
iso = _normalize_iso(dt_str)
|
|
|
|
|
if iso is None:
|
|
|
|
|
return Markup(escape(dt_str))
|
|
|
|
|
try:
|
|
|
|
|
parsed = datetime.fromisoformat(iso)
|
|
|
|
|
if parsed.tzinfo is None:
|
|
|
|
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
|
|
|
fallback = parsed.strftime("%A, %d-%m-%Y")
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
fallback = _format_date(dt_str)
|
|
|
|
|
return Markup(
|
|
|
|
|
f'<time datetime="{escape(iso)}" data-dt data-dt-mode="award">'
|
|
|
|
|
f"{escape(fallback)}</time>"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from devplacepy.database.awards import award_is_prominent
|
|
|
|
|
|
|
|
|
|
templates.env.globals["award_date"] = award_date
|
|
|
|
|
templates.env.globals["award_is_prominent"] = award_is_prominent
|
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",
|
2026-07-19 18:57:43 +02:00
|
|
|
"markdown_rendered": "Markdown Rendered",
|
2026-06-09 16:48:08 +00:00
|
|
|
"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-07-07 15:28:28 +02:00
|
|
|
from devplacepy.services.deepsearch.citations import link_citations
|
2026-06-18 22:09:34 +00:00
|
|
|
|
|
|
|
|
templates.env.globals["render_content"] = render_content
|
|
|
|
|
templates.env.globals["render_title"] = render_title
|
2026-07-07 15:28:28 +02:00
|
|
|
templates.env.globals["link_citations"] = link_citations
|
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
|