From 7cc39076bf6253c6fac01df82ecd147b07042dd6 Mon Sep 17 00:00:00 2001 From: Typosaurus Date: Sun, 19 Jul 2026 18:16:23 +0000 Subject: [PATCH] ticket #65 attempt 1 --- .env.example | 4 ++++ README.md | 1 + devplacepy/config.py | 1 + devplacepy/docs_api/groups/content.py | 5 +++-- devplacepy/docs_devrant.py | 4 +++- devplacepy/models.py | 6 +++--- devplacepy/routers/devrant/comments.py | 3 ++- devplacepy/routers/devrant/rants.py | 3 ++- .../devii/actions/catalog/comments.py | 3 ++- devplacepy/static/js/CommentManager.js | 2 ++ devplacepy/templates/_comment_form.html | 4 ++-- devplacepy/templating.py | 3 ++- dpc.log | 20 +++++++++++++++++++ tests/api/comments/create.py | 19 ++++++++++++++++++ tests/api/comments/edit.py | 16 +++++++++++++++ 15 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 dpc.log diff --git a/.env.example b/.env.example index 6d4402e8..ec0cca69 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,10 @@ NGINX_MAX_BODY_SIZE=50m NGINX_CACHE_ENABLED=false NGINX_CACHE_MAX_SIZE=1g +# Max length of comment text (applied server-side on write and client-side on input). +# Change at any time via the env var; the DB has no length constraint. +DEVPLACE_COMMENT_MAX_LENGTH=1000 + # Run the app container as this host user so shared files keep dev ownership. DEVPLACE_UID=1000 DEVPLACE_GID=1000 diff --git a/README.md b/README.md index dd3e7ca8..2cb042ba 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ The log is **administrator-only**. `/admin/audit-log` is a paginated, filterable | `DEVPLACE_PRESENCE_TIMEOUT_SECONDS` | `60` | Online-presence window: a user counts as online for this many seconds after their last activity. `last_seen` is refreshed by a throttled in-place update at most once per half this interval per worker (no per-load inserts, no data growth) | | `DEVPLACE_PRESENCE_ONLINE_LIMIT` | `30` | Maximum avatars shown in the feed's live "Online now" panel (ordered alphabetically by username) | | `DEVPLACE_PRESENCE_ONLINE_MARGIN_SECONDS` | `20` | Grace margin before an online user drops to offline (hysteresis): online at the timeout, offline only after timeout + this. Prevents online/offline flicker for users hovering at the boundary | +| `DEVPLACE_COMMENT_MAX_LENGTH` | `1000` | Maximum character length for comment body text. Applied server-side via Pydantic validation and client-side via `maxLength` on textareas. The database has no column-length constraint, so this can be changed at any time via the environment variable (requires restart) | ### Runtime settings diff --git a/devplacepy/config.py b/devplacepy/config.py index 651f54e0..63f3829d 100644 --- a/devplacepy/config.py +++ b/devplacepy/config.py @@ -68,6 +68,7 @@ INTERNAL_GATEWAY_URL = f"{INTERNAL_BASE_URL}/openai/v1/chat/completions" INTERNAL_EMBED_URL = f"{INTERNAL_BASE_URL}/openai/v1/embeddings" INTERNAL_MODEL = "molodetz" INTERNAL_EMBED_MODEL = "molodetz~embed" +COMMENT_MAX_LENGTH = int(environ.get("DEVPLACE_COMMENT_MAX_LENGTH", "1000")) DEFAULT_CORRECTION_PROMPT = "Leave literary as is, only do punctuation and casing" DEFAULT_MODIFIER_PROMPT = ( "Execute what is behind `@ai` (the prompt) and replace that part including `@ai`" diff --git a/devplacepy/docs_api/groups/content.py b/devplacepy/docs_api/groups/content.py index fac43470..7bfb6fcc 100644 --- a/devplacepy/docs_api/groups/content.py +++ b/devplacepy/docs_api/groups/content.py @@ -1,5 +1,6 @@ # retoor +from devplacepy.config import COMMENT_MAX_LENGTH from .._shared import COMMENT_TARGETS, GIST_LANGUAGES, PROJECT_TYPES, endpoint, field from devplacepy.constants import TOPICS @@ -229,7 +230,7 @@ four ways to sign requests. "textarea", True, "Nice work.", - "Body, 3-1000 characters.", + f"Body, 3-{COMMENT_MAX_LENGTH} characters.", ), field( "target_uid", @@ -290,7 +291,7 @@ four ways to sign requests. "textarea", True, "Edited body.", - "New body, 3-1000 characters.", + f"New body, 3-{COMMENT_MAX_LENGTH} characters.", ), ], sample_response={ diff --git a/devplacepy/docs_devrant.py b/devplacepy/docs_devrant.py index 99b2e251..5fa83a9a 100644 --- a/devplacepy/docs_devrant.py +++ b/devplacepy/docs_devrant.py @@ -1,5 +1,7 @@ # retoor +from devplacepy.config import COMMENT_MAX_LENGTH + ROLE_LABELS = {"public": "Public", "user": "Member"} @@ -237,7 +239,7 @@ DEVRANT_GROUPS = { encoding="form", params=[ field("rant_id", "path", type="int", required=True, example="1", description="Rant id."), - field("comment", "body", type="textarea", required=True, example="Great rant!", description="Comment text, 1-1000 chars."), + field("comment", "body", type="textarea", required=True, example="Great rant!", description=f"Comment text, 1-{COMMENT_MAX_LENGTH} chars."), ], sample_response={"success": True}, ), diff --git a/devplacepy/models.py b/devplacepy/models.py index 05732547..be7c3185 100644 --- a/devplacepy/models.py +++ b/devplacepy/models.py @@ -6,7 +6,7 @@ from datetime import datetime from typing import Literal, Optional from pydantic import BaseModel, Field, field_validator, model_validator from devplacepy.constants import TOPICS, REACTION_EMOJI -from devplacepy.config import DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT +from devplacepy.config import COMMENT_MAX_LENGTH, DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT def normalize_european_date(value): @@ -157,7 +157,7 @@ class PostEditForm(BaseModel): class CommentForm(BaseModel): - content: str = Field(min_length=3, max_length=1000) + content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH) target_uid: str = Field(default="", max_length=36) post_uid: str = Field(default="", max_length=36) target_type: Literal["post", "project", "news", "issue", "gist"] = "post" @@ -172,7 +172,7 @@ class CommentForm(BaseModel): class CommentEditForm(BaseModel): - content: str = Field(min_length=3, max_length=1000) + content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH) class ProjectForm(BaseModel): diff --git a/devplacepy/routers/devrant/comments.py b/devplacepy/routers/devrant/comments.py index a5c6ddcb..1c910d3d 100644 --- a/devplacepy/routers/devrant/comments.py +++ b/devplacepy/routers/devrant/comments.py @@ -69,7 +69,8 @@ async def edit_comment(request: Request, comment_id: str): if not is_owner(comment, user): return dr_error("You can only edit your own comments.", fail_reason="not_owner") text = (params.get("comment") or "").strip() - if len(text) < 1 or len(text) > 1000: + from devplacepy.config import COMMENT_MAX_LENGTH + if len(text) < 1 or len(text) > COMMENT_MAX_LENGTH: return dr_error("Invalid comment length.", fail_reason="length") get_table("comments").update( { diff --git a/devplacepy/routers/devrant/rants.py b/devplacepy/routers/devrant/rants.py index 0695d474..634afd55 100644 --- a/devplacepy/routers/devrant/rants.py +++ b/devplacepy/routers/devrant/rants.py @@ -227,7 +227,8 @@ async def comment_rant(request: Request, rant_id: str): text = (params.get("comment") or "").strip() if len(text) < 1: return dr_error("Your comment is too short.") - if len(text) > 1000: + from devplacepy.config import COMMENT_MAX_LENGTH + if len(text) > COMMENT_MAX_LENGTH: return dr_error("Your comment is too long.") create_comment_record(request, user, "post", post["uid"], text) return dr_ok() diff --git a/devplacepy/services/devii/actions/catalog/comments.py b/devplacepy/services/devii/actions/catalog/comments.py index 3eac99ff..401306c4 100644 --- a/devplacepy/services/devii/actions/catalog/comments.py +++ b/devplacepy/services/devii/actions/catalog/comments.py @@ -2,6 +2,7 @@ from __future__ import annotations +from devplacepy.config import COMMENT_MAX_LENGTH from ..spec import Action from ._shared import ATTACHMENTS, TARGET_TYPE, body, confirm, path @@ -28,7 +29,7 @@ COMMENTS_ACTIONS: tuple[Action, ...] = ( summary="Edit the body of one of your own comments", params=( path("comment_uid", "Uid of the comment."), - body("content", "New comment body, 3-1000 characters.", required=True), + body("content", f"New comment body, 3-{COMMENT_MAX_LENGTH} characters.", required=True), ), ), Action( diff --git a/devplacepy/static/js/CommentManager.js b/devplacepy/static/js/CommentManager.js index 0000c9b5..dbbe6b14 100644 --- a/devplacepy/static/js/CommentManager.js +++ b/devplacepy/static/js/CommentManager.js @@ -83,6 +83,8 @@ export class CommentManager { textarea.className = "emoji-picker-target"; textarea.value = text.dataset.raw || text.textContent; textarea.rows = 3; + const ref = document.querySelector("[data-comment-maxlength]"); + textarea.maxLength = ref ? parseInt(ref.dataset.commentMaxlength, 10) : 1000; form.appendChild(textarea); const actions = document.createElement("div"); diff --git a/devplacepy/templates/_comment_form.html b/devplacepy/templates/_comment_form.html index 2af8a2eb..2a4ffcd8 100644 --- a/devplacepy/templates/_comment_form.html +++ b/devplacepy/templates/_comment_form.html @@ -1,9 +1,9 @@ {% if user %} -
+ {% set _user = user %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %} - +
allow=0 deny=0 ask=0 +2026-07-19T15:03:09 INFO logging initialised at /workspace/repo/dpc.log +2026-07-19T15:03:09 DEBUG model=molodetz-pro fps=30 +2026-07-19T15:03:09 INFO read task from file: /workspace/prompts/research-2.txt +2026-07-19T15:03:09 INFO settings merged: model= allow=0 deny=0 ask=0 +2026-07-19T15:40:16 INFO logging initialised at /workspace/repo/dpc.log +2026-07-19T15:40:16 DEBUG model=molodetz-pro fps=30 +2026-07-19T15:40:16 INFO read task from file: /workspace/prompts/research-3.txt +2026-07-19T15:40:16 INFO settings merged: model= allow=0 deny=0 ask=0 +2026-07-19T17:35:33 INFO logging initialised at /workspace/repo/dpc.log +2026-07-19T17:35:33 DEBUG model=molodetz-pro fps=30 +2026-07-19T17:35:33 INFO read task from file: /workspace/prompts/research-4.txt +2026-07-19T17:35:33 INFO settings merged: model= allow=0 deny=0 ask=0 +2026-07-19T18:11:47 INFO logging initialised at /workspace/repo/dpc.log +2026-07-19T18:11:47 DEBUG model=molodetz-pro fps=30 +2026-07-19T18:11:47 INFO read task from file: /workspace/prompts/execution-1.txt +2026-07-19T18:11:47 INFO settings merged: model= allow=0 deny=0 ask=0 diff --git a/tests/api/comments/create.py b/tests/api/comments/create.py index c440d5f9..e62bd842 100644 --- a/tests/api/comments/create.py +++ b/tests/api/comments/create.py @@ -345,3 +345,22 @@ def test_comment_links_multiple_attachments(app_server): assert u1 in r.text and u2 in r.text, ( "both attachments must be linked and displayed on the comment" ) + + +def test_create_comment_too_long_rejected(app_server): + from devplacepy.config import COMMENT_MAX_LENGTH + s, _ = _session_comments() + post_uid = _create_post_comments(s, f"cmt-long-{int(time.time() * 1000)}") + r = s.post( + f"{BASE_URL}/comments/create", + data={ + "content": "x" * (COMMENT_MAX_LENGTH + 1), + "target_type": "post", + "post_uid": post_uid, + "target_uid": post_uid, + }, + allow_redirects=False, + ) + assert r.status_code in (400, 422), r.text[:300] + refresh_snapshot() + assert get_table("comments").find_one(target_uid=post_uid) is None diff --git a/tests/api/comments/edit.py b/tests/api/comments/edit.py index 5d916d2f..92d5692f 100644 --- a/tests/api/comments/edit.py +++ b/tests/api/comments/edit.py @@ -129,3 +129,19 @@ def test_edit_comment_too_short_rejected(app_server): assert r.status_code in (400, 422) refresh_snapshot() assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Long enough body" + + +def test_edit_comment_too_long_rejected(app_server): + from devplacepy.config import COMMENT_MAX_LENGTH + s, _ = _session_edit() + post_uid = _create_post_edit(s, f"edit-long-{int(time.time() * 1000)}") + comment = _create_comment_edit(s, post_uid, "Normal body length") + r = s.post( + f"{BASE_URL}/comments/edit/{comment['uid']}", + headers=JSON_edit, + data={"content": "x" * (COMMENT_MAX_LENGTH + 1)}, + allow_redirects=False, + ) + assert r.status_code in (400, 422), r.text[:300] + refresh_snapshot() + assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Normal body length"