2026-06-12 01:58:46 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
2026-06-19 00:09:34 +02:00
|
|
|
import time
|
2026-06-16 05:32:19 +02:00
|
|
|
from datetime import datetime, timezone
|
2026-06-19 10:06:09 +02:00
|
|
|
import jinja2
|
2026-05-10 09:08:12 +02:00
|
|
|
from fastapi.templating import Jinja2Templates
|
2026-06-16 05:32:19 +02:00
|
|
|
from markupsafe import Markup, escape
|
2026-05-23 06:20:27 +02:00
|
|
|
from devplacepy.cache import TTLCache
|
2026-06-14 16:46:36 +02:00
|
|
|
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD
|
2026-06-06 16:31:42 +02:00
|
|
|
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
2026-06-09 20:02:50 +02:00
|
|
|
from devplacepy.database import get_int_setting, get_setting, get_table
|
feat: add per-user avatar seed regeneration with irreversible random avatar replacement
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
2026-06-28 00:31:34 +02:00
|
|
|
from devplacepy.avatar import avatar_url, avatar_seed
|
2026-05-12 12:45:52 +02:00
|
|
|
from devplacepy.utils import format_date as _format_date
|
2026-06-16 05:32:19 +02:00
|
|
|
from devplacepy.utils import time_ago as _time_ago
|
2026-06-17 16:08:28 +02:00
|
|
|
from devplacepy.utils import get_badge, is_admin, is_primary_admin, pretty_json
|
2026-06-09 20:02:50 +02:00
|
|
|
from devplacepy.attachments import format_file_size, file_icon_emoji
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
from devplacepy.content import is_owner as _owns
|
2026-06-09 20:02:50 +02:00
|
|
|
from devplacepy.customization import custom_css_tag, custom_js_tag, page_type_for
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-06-19 10:06:09 +02: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))
|
|
|
|
|
|
|
|
|
|
|
feat: add admin/internal database API with CRUD, read-only query, and natural-language SQL endpoints
Add a new `/dbapi` router package providing a generic database API over `dataset`, restricted to admin sessions, admin API keys, and the internal gateway key. Includes:
- `tables.py`: list all tables and inspect table schemas
- `crud.py`: full CRUD operations (GET, POST, PATCH, DELETE) with soft-delete awareness, born-live inserts, `?include_deleted`, `.../restore`, and `?hard=true` purge
- `query.py`: validated read-only SELECT execution via sqlglot parsing, classification, and EXPLAIN dry-run; async query jobs with WebSocket streaming via `DbApiJobService`
- `nl.py`: natural-language-to-SQL conversion using the platform AI gateway with re-prompting until validation passes
Also register `DbApiJobService` and `PubSubService` in the service manager, add `DBAPI_DIR` to config data paths, and force cleartext `http://` connections to HTTP/1.1 in `curl_transport` to fix large request failures against uvicorn's HTTP/1.1-only internal gateway.
2026-06-15 01:00:30 +02:00
|
|
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
2026-06-19 10:06:09 +02:00
|
|
|
templates.env.template_class = _Template
|
feat: add admin/internal database API with CRUD, read-only query, and natural-language SQL endpoints
Add a new `/dbapi` router package providing a generic database API over `dataset`, restricted to admin sessions, admin API keys, and the internal gateway key. Includes:
- `tables.py`: list all tables and inspect table schemas
- `crud.py`: full CRUD operations (GET, POST, PATCH, DELETE) with soft-delete awareness, born-live inserts, `?include_deleted`, `.../restore`, and `?hard=true` purge
- `query.py`: validated read-only SELECT execution via sqlglot parsing, classification, and EXPLAIN dry-run; async query jobs with WebSocket streaming via `DbApiJobService`
- `nl.py`: natural-language-to-SQL conversion using the platform AI gateway with re-prompting until validation passes
Also register `DbApiJobService` and `PubSubService` in the service manager, add `DBAPI_DIR` to config data paths, and force cleartext `http://` connections to HTTP/1.1 in `curl_transport` to fix large request failures against uvicorn's HTTP/1.1-only internal gateway.
2026-06-15 01:00:30 +02:00
|
|
|
templates.env.auto_reload = TEMPLATE_AUTO_RELOAD
|
2026-05-10 09:08:12 +02:00
|
|
|
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02: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 14:05:44 +02:00
|
|
|
def guest_disabled(user) -> Markup:
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
if user:
|
|
|
|
|
return Markup("")
|
2026-06-19 10:06:09 +02:00
|
|
|
return Markup(' disabled aria-disabled="true"')
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02: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 14:05:44 +02:00
|
|
|
def static_url(path) -> str:
|
2026-06-14 02:16:22 +02: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
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
templates.env.globals["is_admin"] = is_admin
|
2026-06-17 16:08:28 +02:00
|
|
|
templates.env.globals["is_primary_admin"] = is_primary_admin
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
templates.env.globals["owns"] = _owns
|
|
|
|
|
templates.env.globals["is_self"] = is_self
|
|
|
|
|
templates.env.globals["guest_disabled"] = guest_disabled
|
|
|
|
|
|
2026-06-14 09:48:10 +02:00
|
|
|
from devplacepy.docs_devrant import devrant_endpoints
|
|
|
|
|
|
|
|
|
|
templates.env.globals["devrant_endpoints"] = devrant_endpoints
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
_unread_cache = TTLCache(ttl=10, max_size=1000)
|
|
|
|
|
_messages_cache = TTLCache(ttl=10, max_size=1000)
|
|
|
|
|
|
2026-05-11 03:14:43 +02:00
|
|
|
|
2026-05-16 02:49:53 +02:00
|
|
|
def clear_unread_cache(user_uid: str) -> None:
|
2026-05-23 06:20:27 +02:00
|
|
|
_unread_cache.pop(user_uid)
|
2026-05-16 02:49:53 +02:00
|
|
|
|
|
|
|
|
|
2026-06-06 16:31:42 +02:00
|
|
|
def clear_messages_cache(user_uid: str) -> None:
|
|
|
|
|
_messages_cache.pop(user_uid)
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 20:02:50 +02:00
|
|
|
def _cached_count(cache: TTLCache, table_name: str, cache_key: str, **filters) -> int:
|
|
|
|
|
cached = cache.get(cache_key)
|
2026-05-23 06:20:27 +02:00
|
|
|
if cached is not None:
|
|
|
|
|
return cached
|
2026-06-09 20:02:50 +02:00
|
|
|
table = get_table(table_name)
|
|
|
|
|
count = table.count(**filters)
|
|
|
|
|
cache.set(cache_key, count)
|
2026-05-11 03:14:43 +02:00
|
|
|
return count
|
2026-05-10 09:08:12 +02:00
|
|
|
|
2026-06-06 16:31:42 +02:00
|
|
|
|
2026-06-09 20:02:50 +02: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 16:31:42 +02:00
|
|
|
def jinja_unread_messages(user_uid: str) -> int:
|
2026-06-09 20:02:50 +02:00
|
|
|
return _cached_count(_messages_cache, "messages", user_uid, receiver_uid=user_uid, read=False)
|
2026-06-06 16:31:42 +02:00
|
|
|
|
2026-06-09 18:48:08 +02: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 14:05:44 +02:00
|
|
|
def jinja_user_projects(user_uid: str) -> list[dict]:
|
2026-05-10 09:08:12 +02:00
|
|
|
projects = get_table("projects")
|
|
|
|
|
return list(projects.find(user_uid=user_uid))
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-10 09:08:12 +02:00
|
|
|
templates.env.globals["get_unread_count"] = jinja_unread_count
|
2026-06-06 16:31:42 +02:00
|
|
|
templates.env.globals["get_unread_messages"] = jinja_unread_messages
|
2026-05-10 09:08:12 +02:00
|
|
|
templates.env.globals["get_user_projects"] = jinja_user_projects
|
2026-05-10 21:33:53 +02:00
|
|
|
templates.env.globals["avatar_url"] = avatar_url
|
feat: add per-user avatar seed regeneration with irreversible random avatar replacement
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
2026-06-28 00:31:34 +02:00
|
|
|
templates.env.globals["avatar_seed"] = avatar_seed
|
2026-05-12 12:45:52 +02:00
|
|
|
templates.env.globals["format_date"] = _format_date
|
2026-06-16 05:32:19 +02: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 05:37:12 +02:00
|
|
|
templates.env.globals["badge_info"] = get_badge
|
2026-05-13 21:17:57 +02:00
|
|
|
templates.env.globals["TOPICS"] = TOPICS
|
2026-06-06 16:31:42 +02:00
|
|
|
templates.env.globals["REACTION_EMOJI"] = REACTION_EMOJI
|
2026-06-09 18:48:08 +02: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 14:05:44 +02:00
|
|
|
def jinja_max_upload_size_mb() -> int:
|
2026-05-23 06:55:11 +02:00
|
|
|
return get_int_setting("max_upload_size_mb", 10)
|
2026-05-12 15:07:34 +02:00
|
|
|
|
2026-06-09 18:48:08 +02: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 14:05:44 +02:00
|
|
|
def jinja_max_attachments() -> int:
|
2026-05-23 06:55:11 +02:00
|
|
|
return get_int_setting("max_attachments_per_resource", 10)
|
2026-05-12 15:07:34 +02:00
|
|
|
|
2026-06-09 18:48:08 +02: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 14:05:44 +02:00
|
|
|
def jinja_allowed_file_types() -> str:
|
2026-05-12 15:07:34 +02:00
|
|
|
return get_setting("allowed_file_types", "")
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-12 15:07:34 +02: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 21:17:57 +02:00
|
|
|
|
|
|
|
|
_LANGUAGE_NAMES = {
|
2026-06-09 18:48:08 +02: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 21:17:57 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-13 21:17:57 +02:00
|
|
|
def jinja_language_name(code: str) -> str:
|
|
|
|
|
return _LANGUAGE_NAMES.get(code, code)
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-13 21:17:57 +02:00
|
|
|
templates.env.globals["language_name"] = jinja_language_name
|
2026-06-12 08:30:17 +02:00
|
|
|
templates.env.globals["pretty_json"] = pretty_json
|
2026-05-13 21:17:57 +02:00
|
|
|
|
|
|
|
|
templates.env.globals["format_file_size"] = format_file_size
|
|
|
|
|
templates.env.globals["file_icon_emoji"] = file_icon_emoji
|
2026-06-09 16:06:02 +02: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-19 00:09:34 +02:00
|
|
|
|
2026-06-19 22:15:22 +02: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
|
|
|
|
|
|
feat: add content_preview helper and apply rendered content across templates
Introduce a new `content_preview()` function in `rendering.py` that strips HTML from rendered content and truncates to a given length with an ellipsis. Add a corresponding `plainText()` and `preview()` method to the `ContentRenderer` JS class. Register `content_preview` as a Jinja2 global in `templating.py`. Update multiple templates (`feed.html`, `gists.html`, `landing.html`, `messages.html`, `news.html`, `notifications.html`, `post.html`, `profile.html`) to use `render_title`, `render_content`, or `content_preview` instead of raw string slicing, ensuring consistent rendering of rich text (e.g., Markdown, HTML) in previews, descriptions, bios, and notification messages. Also fix the conversation preview in `MessagesLayout.js` to use the new JS preview method.
2026-06-22 23:17:54 +02:00
|
|
|
from devplacepy.rendering import content_preview, render_content, render_title
|
2026-06-19 00:09:34 +02:00
|
|
|
|
|
|
|
|
templates.env.globals["render_content"] = render_content
|
|
|
|
|
templates.env.globals["render_title"] = render_title
|
feat: add content_preview helper and apply rendered content across templates
Introduce a new `content_preview()` function in `rendering.py` that strips HTML from rendered content and truncates to a given length with an ellipsis. Add a corresponding `plainText()` and `preview()` method to the `ContentRenderer` JS class. Register `content_preview` as a Jinja2 global in `templating.py`. Update multiple templates (`feed.html`, `gists.html`, `landing.html`, `messages.html`, `news.html`, `notifications.html`, `post.html`, `profile.html`) to use `render_title`, `render_content`, or `content_preview` instead of raw string slicing, ensuring consistent rendering of rich text (e.g., Markdown, HTML) in previews, descriptions, bios, and notification messages. Also fix the conversation preview in `MessagesLayout.js` to use the new JS preview method.
2026-06-22 23:17:54 +02:00
|
|
|
templates.env.globals["content_preview"] = content_preview
|
2026-06-19 00:09:34 +02:00
|
|
|
|
|
|
|
|
|
2026-06-19 10:06:09 +02: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-19 00:09:34 +02: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
|