Skip to content

Conversation

@stephherbers
Copy link
Contributor

@stephherbers stephherbers commented Dec 10, 2025

Resolves #2419
When you edit a message in the dataset table, only that specific row now updates instead of reloading the entire table.

Technical Description

Why render the full table when we only need one row?
I initially tried the straightforward approach: render just the single row that changed. But it broke with KeyError: 'request' because django-tables2's TemplateColumn rendering happens in a nested context where the request object isn't accessible when rendering rows manually.
I tried three different ways to fix the context propagation - none worked without fighting django-tables2's internals.

This approach
Use HTMX's hx-select attribute:
Server renders the full table
hx-select="#message-row-123" extracts only the target row from the response
hx-target="#message-row-123" replaces the existing row
HTMX discards the rest

From the user's perspective, only one row updates. From the server's perspective, we use existing working code.

Is it inefficient?
Slightly - we render extra HTML that gets thrown away. But the table is paginated, and this approach has zero maintenance burden compared to rewriting the Actions column or creating custom table rendering.

Docs and Changelog

  • This PR requires docs/changelog update

no

@stephherbers
Copy link
Contributor Author

@coderabbitai please review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

@stephherbers I'll review the changes in this PR for you.


🧠 Learnings used
Learnt from: CR
Repo: dimagi/open-chat-studio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T16:17:28.782Z
Learning: Applies to apps/*/templates/**/*.html : Use HTMX for dynamic content loading in Django templates with hx-get, hx-trigger, and other HTMX attributes
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

📝 Walkthrough

Walkthrough

This pull request modifies the evaluation messages workflow to support row-level updates in the dataset messages table. It adds unique id attributes to table rows in apps/evaluations/tables.py by extending the DJANGO_TABLES2_ROW_ATTRS configuration. In apps/evaluations/views/dataset_views.py, the message update and add operations now return the full rendered dataset messages table HTML instead of plain responses, enabling HTMX to replace specific rows. The template edit_message_modal_content.html is updated to target specific message rows for updates and close the modal after successful submission. These changes allow users to edit messages while maintaining the current table page context.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Table row ID generation: Verify the lambda function pattern for generating unique row IDs and ensure the message.id attribute is consistently available
  • View rendering logic: Review the DatasetMessagesTableView invocation in both add and update paths to confirm the table HTML is correctly serialized and returned
  • HTMX targeting: Validate that the hx-target, hx-select, and hx-swap configurations correctly target and replace individual message rows without affecting table structure
  • Modal closure: Ensure the after-request hook properly closes the modal and handles both success and error scenarios

Possibly Related PRs

  • PR #2281: Modifies the evaluations dataset message workflow with related changes to how messages are added and rendered for datasets in the same view and template files.

Suggested Reviewers

  • snopoke
  • SmittieC
  • proteusvacuum

Pre-merge checks

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: updating only a single row instead of the entire dataset table when editing a message.
Description check ✅ Passed The PR description includes a comprehensive technical explanation of the problem, the solution approach using HTMX, and trade-offs, though the docs/changelog checkbox is marked 'no'.
Linked Issues check ✅ Passed The changes fully address issue #2419 by implementing HTMX-based single-row updates that keep the table on the current page, avoiding full table reloads.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the objective of updating only single rows: table row ID attributes, view logic for rendering full tables with HTMX selection, and template targeting.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@stephherbers
Copy link
Contributor Author

@coderabbitai please review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

@stephherbers I'll review the changes in this PR for you.


🧠 Learnings used
Learnt from: CR
Repo: dimagi/open-chat-studio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T16:17:28.782Z
Learning: Applies to apps/*/templates/**/*.html : Use HTMX for dynamic content loading in Django templates with hx-get, hx-trigger, and other HTMX attributes
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/evaluations/tables.py (1)

354-355: Row ids for HTMX look good; tidy up RUF012 on mutable class attribute

The new row_attrs with an id per record is exactly what you need for #message-row-{{ message.id }} targeting; behavior-wise this is solid.

Ruff’s RUF012 warning here is about having a mutable class attribute (dict) on the inner Meta class. If your lint is strict, consider one of:

+from typing import ClassVar
@@ class DatasetMessagesTable(tables.Table):
     class Meta:
@@
-        row_attrs = {**settings.DJANGO_TABLES2_ROW_ATTRS, "id": lambda record: f"message-row-{record.id}"}
+        row_attrs: ClassVar[dict] = {
+            **settings.DJANGO_TABLES2_ROW_ATTRS,
+            "id": lambda record: f"message-row-{record.id}",
+        }

or, if you prefer not to annotate Meta attributes, add a targeted ignore:

row_attrs = {**settings.DJANGO_TABLES2_ROW_ATTRS, "id": lambda record: f"message-row-{record.id}"}  # noqa: RUF012
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24fc84e and d316689.

⛔ Files ignored due to path filters (1)
  • cypress/screenshots/dashboard.cy.js/Dashboard Application -- Dashboard Reset -- clicks the reset filters button (failed).png is excluded by !**/*.png
📒 Files selected for processing (3)
  • apps/evaluations/tables.py (1 hunks)
  • apps/evaluations/views/dataset_views.py (1 hunks)
  • templates/evaluations/edit_message_modal_content.html (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use Ruff for Python linting and formatting with a line length of 120 characters
Use isort via Ruff for Python import sorting

Files:

  • apps/evaluations/tables.py
  • apps/evaluations/views/dataset_views.py

⚙️ CodeRabbit configuration file

**/*.py: Do not review for Python compatibility below 3.13

Files:

  • apps/evaluations/tables.py
  • apps/evaluations/views/dataset_views.py
apps/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.py: Avoid importing heavy AI/ML libraries (langchain_google_vertexai, langchain_google_genai, langchain_anthropic, langchain_openai, boto3, pandas, numpy) at module level; use lazy imports inside functions instead
Use TYPE_CHECKING guard for type-only imports from heavy AI/ML libraries to avoid module-level import costs

Files:

  • apps/evaluations/tables.py
  • apps/evaluations/views/dataset_views.py
apps/*/views{,/**}/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

apps/*/views{,/**}/*.py: Use @login_and_team_required decorator or LoginAndTeamRequiredMixin for all Django views to enforce team-based security
Use @permission_required decorator or PermissionRequiredMixin to enforce permission checks on sensitive views
Use class-based views with appropriate mixins (LoginAndTeamRequiredMixin, PermissionRequiredMixin) instead of function-based views

Files:

  • apps/evaluations/views/dataset_views.py
**/*.{html,tsx,ts,jsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Use DaisyUI components as the primary UI framework for all frontend components before falling back to TailwindCSS custom styling

Files:

  • templates/evaluations/edit_message_modal_content.html
**/*.{html,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All UI components must support both light and dark modes using theme-aware styling

Files:

  • templates/evaluations/edit_message_modal_content.html
🧬 Code graph analysis (1)
apps/evaluations/tables.py (1)
apps/channels/datamodels.py (1)
  • id (41-42)
🪛 Ruff (0.14.8)
apps/evaluations/tables.py

354-354: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: python-tests

Comment on lines 397 to 410
dataset = message.evaluationdataset_set.first()
if dataset:
# Get the full table view to render properly with request context
dataset_id = dataset.id
table_view = DatasetMessagesTableView()
table_view.request = request
table_view.kwargs = {"dataset_id": dataset_id}

queryset = table_view.get_queryset()
table = table_view.table_class(queryset, request=request)

# Render the full table - HTMX will extract just the updated row via hx-select
return render(request, "table/single_table.html", {"table": table})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd /tmp/repo && find . -type f -name "*.py" | grep -E "(dataset_views|views)" | head -20

Repository: dimagi/open-chat-studio

Length of output: 126


🏁 Script executed:

cd /tmp/repo && rg "class DatasetMessagesTableView" -A 20

Repository: dimagi/open-chat-studio

Length of output: 126


🏁 Script executed:

cd /tmp/repo && rg "def update_message" -A 30 apps/evaluations/views/

Repository: dimagi/open-chat-studio

Length of output: 126


🏁 Script executed:

cd /tmp/repo && find . -type f -name "*.html" | xargs grep -l "edit_message_modal" | head -5

Repository: dimagi/open-chat-studio

Length of output: 126


🏁 Script executed:

cd /tmp/repo && find . -type f -name "*.html" | xargs grep -l "update_url\|update_message" | head -10

Repository: dimagi/open-chat-studio

Length of output: 126


🏁 Script executed:

git ls-files | grep -E "dataset_views|views\.py" | head -20

Repository: dimagi/open-chat-studio

Length of output: 701


🏁 Script executed:

rg "class DatasetMessagesTableView" -A 20

Repository: dimagi/open-chat-studio

Length of output: 1709


🏁 Script executed:

rg "def update_message" -B 5 -A 30 apps/evaluations/views/

Repository: dimagi/open-chat-studio

Length of output: 2657


🏁 Script executed:

find . -type f -name "*.html" | xargs grep -l "edit_message_modal" 2>/dev/null | head -5

Repository: dimagi/open-chat-studio

Length of output: 123


🏁 Script executed:

rg "def update_message" -A 50 apps/evaluations/views/dataset_views.py

Repository: dimagi/open-chat-studio

Length of output: 1824


🏁 Script executed:

cat -n apps/evaluations/views/dataset_views.py | sed -n '360,420p'

Repository: dimagi/open-chat-studio

Length of output: 2541


🏁 Script executed:

cat apps/templates/evaluations/dataset_message_edit_action.html

Repository: dimagi/open-chat-studio

Length of output: 158


🏁 Script executed:

find . -type f -name "edit_message_modal_content.html"

Repository: dimagi/open-chat-studio

Length of output: 122


🏁 Script executed:

cat apps/templates/evaluations/edit_message_modal_content.html

Repository: dimagi/open-chat-studio

Length of output: 157


🏁 Script executed:

cat ./templates/evaluations/dataset_message_edit_action.html

Repository: dimagi/open-chat-studio

Length of output: 400


🏁 Script executed:

cat ./templates/evaluations/edit_message_modal_content.html

Repository: dimagi/open-chat-studio

Length of output: 1533


🏁 Script executed:

rg "edit_message_modal\|update_message" ./templates/evaluations/ -A 5 -B 2

Repository: dimagi/open-chat-studio

Length of output: 49


🏁 Script executed:

rg "def add_message_to_dataset" -A 40 apps/evaluations/views/dataset_views.py

Repository: dimagi/open-chat-studio

Length of output: 1742


🏁 Script executed:

rg "def edit_message_modal" -A 20 apps/evaluations/views/dataset_views.py

Repository: dimagi/open-chat-studio

Length of output: 1007


🏁 Script executed:

rg "test.*update_message\|test.*edit.*message" apps/evaluations/tests/ -l

Repository: dimagi/open-chat-studio

Length of output: 49


🏁 Script executed:

rg "SingleTableView\|table_pagination" apps/evaluations/views/dataset_views.py -B 2 -A 5

Repository: dimagi/open-chat-studio

Length of output: 49


🏁 Script executed:

rg "event.detail.successful" ./templates/ -r

Repository: dimagi/open-chat-studio

Length of output: 480


🏁 Script executed:

cat ./templates/evaluations/edit_message_modal_content.html | grep -A 2 "hx-on"

Repository: dimagi/open-chat-studio

Length of output: 199


Fix pagination and error handling in update_message flow

The current implementation has two critical issues:

  1. Pagination is lost on edit: When update_message renders the table (line 406), it passes request=request to the table constructor, which causes django-tables2 to check request.GET for a page parameter. Since the update_url constructed at line 371 has no query string (only reverse("evaluations:update_message", args=[team_slug, message_id])), the POST request contains no page param. The table then defaults to page 1, so if a user edited a message on page > 1, that row won't appear in the rendered response, causing hx-select="#message-row-{{ message.id }}" to silently fail.

  2. Error responses close the modal prematurely: Line 382 returns status=200 for validation errors. The modal template checks event.detail.successful, which is true for all 2xx responses, so the modal closes even when errors are returned (lines showing the errors briefly). Use a 4xx status code for validation errors so the modal stays open.

Fixes:

  • Propagate the current page into update_url in edit_message_modal:
update_url = reverse("evaluations:update_message", args=[team_slug, message_id])
+page = request.GET.get("page")
+if page:
+    update_url = f"{update_url}?page={page}"
  • Return a 4xx status on validation errors:
-            status=200,
+            status=400,
  • Optionally align add_message_to_dataset (line 326) for consistency by adding request=request to the table constructor to match the update path, though note that the add path avoids the pagination issue by not passing request in the first place.

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephherbers point 2 here seems like it could be valid. Have you tested that this works when there are form errors? An easy for error to try is to remove the 'Human Message' field.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

@stephherbers I'll review the changes in this PR for you.


🧠 Learnings used
Learnt from: CR
Repo: dimagi/open-chat-studio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T16:17:28.782Z
Learning: Applies to apps/*/templates/**/*.html : Use HTMX for dynamic content loading in Django templates with hx-get, hx-trigger, and other HTMX attributes
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov-commenter
Copy link

codecov-commenter commented Dec 10, 2025

Codecov Report

❌ Patch coverage is 25.00000% with 3 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
apps/evaluations/views/dataset_views.py 25.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@snopoke snopoke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephherbers this approach looks good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be deleted

@snopoke
Copy link
Contributor

snopoke commented Dec 17, 2025

@stephherbers bump

Comment on lines +11 to +14
hx-target="{% if errors %}#editMessageModal{% else %}#record-{{ message.id }}{% endif %}"
hx-select="{% if errors %}#editMessageModal{% else %}#record-{{ message.id }}{% endif %}"
hx-swap="{% if errors %}innerHTML{% else %}outerHTML{% endif %}"
hx-on::after-request="if(event.detail.successful) document.getElementById('editMessageModal').close()"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this will work if the user first submits with an error in the form:

  1. User clicks edit and modal loads
    • errors is False so target is record-N
  2. User submits form but there are errors
    • View returns the re-rendered form with errors but htmx swaps it into the table row instead of the dialog.

Since the decision to change the target happens on the server side I think you'll need to use the htmx headers instead of these attributes:

You can use the django-htmx utilities: https://django-htmx.readthedocs.io/en/latest/http.html#response-modifying-functions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[evals] Editing a dataset message should keep the table on the current page

4 participants