Skip to content

feat: Add documentation notes system with UI components and tests#999

Open
Flashl3opard wants to merge 4 commits intoalphaonelabs:mainfrom
Flashl3opard:docs/test
Open

feat: Add documentation notes system with UI components and tests#999
Flashl3opard wants to merge 4 commits intoalphaonelabs:mainfrom
Flashl3opard:docs/test

Conversation

@Flashl3opard
Copy link

@Flashl3opard Flashl3opard commented Mar 3, 2026

Description

Added a comprehensive documentation notes system with the following features:

New Features

  • Documentation Note Models: Created DocumentationNoteTopic, DocumentationNoteSection, DocumentationNoteContent, and DocumentationNoteProgress models for managing hierarchical documentation
  • API Endpoints: Implemented API views for creating, retrieving, updating, and deleting documentation content
  • UI Components:
    • Base template with navigation
    • Topic listing page
    • Topic detail view with progress tracking
    • Sidebar navigation component
    • Breadcrumb navigation
  • TypeScript Module: Added documentation-notes.ts for client-side functionality including progress tracking and interactive features
  • Management Command: Added create_sample_docs command for generating sample documentation data
  • Admin Interface: Integrated documentation models into Django admin panel

Images

image image image image

Files modified/created:

  • web/models.py - Added DocumentationNote models
  • web/documentation_views.py - New API views
  • web/urls.py - Added documentation routes
  • web/admin.py - Registered models in admin
  • web/migrations/0064_add_documentation_notes.py - Database schema
  • web/management/commands/create_sample_docs.py - Sample data generation
  • static/ts/documentation-notes.ts - Frontend functionality
  • web/templates/documentation_notes/ - UI templates
  • web/tests/test_views.py - Fixed flaky test

Checklist

  • Did you run the pre-commit? (If not, your PR will most likely not pass — please ensure it passes pre-commit)
  • Did you test the change? (Ensure you didn’t just prompt the AI and blindly commit — test the code and confirm it works)
  • Added screenshots to the PR description (if applicable)

Summary by CodeRabbit

  • New Features
    • Comprehensive documentation system with organized topics and sections and a themed topic listing with per-topic progress.
    • Sticky responsive sidebar with active highlighting, mobile toggle, smooth in-page anchors, and AJAX section loading.
    • Section-to-section navigation with Previous/Next controls and progress indicator; per-user progress persisted and shown via progress bars.
    • Rich content support: Markdown rendering and MathJax for LaTeX.
    • Includes a sample "Projectile Motion" documentation topic.

@github-actions github-actions bot added the files-changed: 15 PR changes 15 files label Mar 3, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

👀 Peer Review Required

Hi @Flashl3opard! This pull request does not yet have a peer review.

Before this PR can be merged, please request a review from one of your peers:

  • Go to the PR page and click "Reviewers" on the right sidebar.
  • Select a team member or contributor to review your changes.
  • Once they approve, this reminder will be automatically removed.

Thank you for contributing! 🎉

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

Warning

Rate limit exceeded

@Flashl3opard has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 12 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between ad12e29 and 6d52343.

📒 Files selected for processing (2)
  • web/documentation_views.py
  • web/templates/documentation_notes/topics_list.html

Walkthrough

Adds a documentation-notes feature: new Django models, migration, admin interfaces, views and URL routes, templates and components, a TypeScript frontend module for AJAX navigation/progress, and a management command to seed sample content.

Changes

Cohort / File(s) Summary
Models & Migration
web/models.py, web/migrations/0064_add_documentation_notes.py
Adds models: DocumentationNoteTopic, DocumentationNoteSection, DocumentationNoteContent, DocumentationNoteProgress and sections_viewed M2M; creates indexes and unique constraints via migration.
Admin
web/admin.py
Adds admin classes and registrations for topic, section, content, and progress; includes DocumentationNoteSectionInline and tailored list displays, filters, searches, and fieldsets.
Views & URLs
web/documentation_views.py, web/urls.py
Adds endpoints: documentation_topics_list, documentation_topic_detail, documentation_section_detail, mark_section_viewed, user_progress; registers /docs/... URL patterns and AJAX handlers for marking/viewing progress.
Frontend (TypeScript)
static/ts/documentation-notes.ts
New DocumentationNotes module and initializeDocumentationNotes factory implementing client-side navigation, content fetching, markdown/MathJax rendering, progress marking (CSRF-aware), UI highlighting, smooth scrolling, and a small public API (navigate/goNext/goPrev/getCurrent/getSections).
Templates & Components
web/templates/documentation_notes/base.html, web/templates/documentation_notes/topic_detail.html, web/templates/documentation_notes/topics_list.html, web/templates/documentation_notes/components/sidebar.html, .../navigation.html, .../breadcrumb.html
Adds base layout, topic list and detail pages, and reusable components (sidebar, navigation, breadcrumb) with color theming, progress UI, markdown/TeX hooks, and conditional auth-based displays.
Management Command & Init
web/management/commands/create_sample_docs.py, web/management/commands/__init__.py
Adds create_sample_docs management command to seed a "projectile-motion" topic with sections and content; adds module docstring to commands __init__.
Tests
web/tests/test_views.py
Adjusts test_index_shows_recent_courses to create courses with explicit created_at timestamps for deterministic ordering.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser
    participant Frontend as Docs TS
    participant Server as Django Server
    participant DB as Database

    Browser->>Frontend: user clicks section link
    Frontend->>Server: GET /docs/{topic}/{section}/ (AJAX/content fetch)
    Server->>DB: query topic, sections, section content, progress
    DB-->>Server: return data
    Server-->>Frontend: HTML/JSON content
    Frontend->>Browser: render content (marked, MathJax), update UI
    Browser->>Frontend: user finishes viewing
    Frontend->>Server: POST /docs/{topic}/section/{section}/viewed/ (CSRF)
    Server->>DB: mark section viewed, update progress
    DB-->>Server: updated progress
    Server-->>Frontend: progress JSON
    Frontend->>Browser: update progress bar and sidebar state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add documentation notes system with UI components and tests' is clear, specific, and directly summarizes the primary change—introducing a new documentation notes feature with associated UI and testing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

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: 18

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@static/ts/documentation-notes.ts`:
- Around line 62-64: Current direct binding using
document.querySelectorAll('a[href^="#"]') causes new anchors added by
contentArea.innerHTML replacements to miss the onAnchorClick handler; replace
this with event delegation by attaching a single click listener on the
containing element (e.g., contentArea or document) that checks event.target (or
closest) for 'a[href^="#"]' and calls this.onAnchorClick(event) so newly
injected anchors work without re-binding; update both locations that currently
call document.querySelectorAll (lines ~62-64 and ~184-189) to use the delegated
listener and remove the per-anchor addEventListener usage.
- Around line 184-185: The code sets innerHTML directly from
newContent.innerHTML via contentArea.innerHTML (and another similar assignment
later) — add a sanitization step to defensively clean the HTML before
assignment: call a sanitizer (e.g., DOMPurify.sanitize or an existing
sanitizeHTML function) on newContent.innerHTML and assign the sanitized result
to contentArea.innerHTML; ensure you import/require DOMPurify or wire the
sanitizer into the module and apply it both where contentArea.innerHTML =
newContent.innerHTML and at the other innerHTML assignment spot.

In `@web/documentation_views.py`:
- Around line 82-88: The GET handlers documentation_topic_detail and
documentation_section_detail currently call
DocumentationNoteProgress.objects.get_or_create(...) and
progress.mark_section_as_viewed(section), which mutates state; remove those
lines from the GET views and instead route progress updates through the existing
POST endpoint (or call the same service function used by that POST handler).
Concretely: delete or comment out the get_or_create + mark_section_as_viewed
calls in the two GET view functions, and ensure the POST progress-update
endpoint (the function that currently handles progress writes) is used by
client-side code or internal server calls to record section views; reuse the
same helper/service used by that POST handler so no duplicate write logic
remains.
- Around line 168-169: The mark_section_viewed handler is fetching the topic by
slug without checking publication status; update its topic lookup to mirror the
read views by calling get_object_or_404(DocumentationNoteTopic, slug=topic_slug,
is_published=True) so unpublished topics are inaccessible, and keep the existing
section lookup (get_object_or_404(DocumentationNoteSection, slug=section_slug,
topic=topic)) as-is; ensure you modify the lookup inside the mark_section_viewed
function to use the is_published=True filter on DocumentationNoteTopic.

In `@web/management/commands/create_sample_docs.py`:
- Around line 427-473: Currently get_or_create is used for
DocumentationNoteTopic, DocumentationNoteSection, and DocumentationNoteContent
so rerunning the command won't refresh changed fields; replace each
get_or_create call with update_or_create (or call get() then set fields and
save) so the title, description, icon, color, order and markdown_content are
updated from PROJECTILE_MOTION_CONTENT when the record exists (use
slugify(section_data["title"]) for section lookups and keep created/updated
messaging logic intact).

In `@web/models.py`:
- Around line 3324-3341: mark_section_as_viewed currently accepts sections from
any topic; ensure the section belongs to the same topic as the progress instance
before mutating state by validating section.topic (or section.topic_id) equals
self.topic (or self.topic_id) at the start of mark_section_as_viewed; if it does
not match, either raise a ValueError (preferred) or return without change; only
then call self.sections_viewed.add(section), set self.current_section, call
update_progress() and save() so completion_percentage and completed_at stay
consistent; also consider using the existing update_progress and sections_viewed
identifiers to locate where to add the guard.
- Around line 3206-3207: Remove the duplicate indexes from the model's indexes
list: delete the explicit models.Index(fields=["slug"]) (since
SlugField(unique=True) already creates a unique index) and remove any explicit
index duplicating the unique_together (user, topic) unique constraint; retain
the models.Index(fields=["is_published"]) entry for query performance. Locate
the indexes = [...] declaration (the models.Index usage and the
unique_together/SlugField(unique=True) definitions) and update it so only
is_published remains indexed.

In `@web/templates/documentation_notes/components/breadcrumb.html`:
- Around line 2-12: The breadcrumb <nav> lacks an accessible label and the
icon-only Home link lacks an accessible name; update the <nav> element used for
breadcrumbs to include an ARIA label (e.g., aria-label="Breadcrumb" or
aria-label="Breadcrumb navigation") and ensure the home anchor (<a href="{% url
'index' %}" ...>) includes an accessible name (e.g., aria-label="Home" or a
visually hidden text label) so screen readers can identify both the navigation
region and the icon-only link.

In `@web/templates/documentation_notes/components/navigation.html`:
- Around line 45-47: Replace runtime Tailwind classes in the Next button (anchor
in navigation.html that builds the docs_section_detail link) with an explicit
mapping from topic.color values to concrete class strings (e.g., map "blue" =>
"bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700
text-white focus:outline-none focus:ring-2 focus:ring-offset-2
focus:ring-blue-400") and use that mapped class variable in the template; ensure
you add dark mode variants and focus state classes in each mapping entry; apply
the same pattern to the similar dynamic color usages in sidebar.html (e.g.,
mappings for dark:bg-.../20 and text-... classes) so Tailwind can statically
detect and preserve the classes.

In `@web/templates/documentation_notes/components/sidebar.html`:
- Around line 21-22: The mobile toggle button is targeting "#sidebar-content"
which doesn't exist; update the sidebar markup so the element being toggled has
the matching id (add id="sidebar-content" to the <nav class="space-y-1"> element
that wraps the {% for section in sections %} loop) or change the button's
data-target to the existing element id—ensure the id referenced by the toggle
and the nav element (or the container around the sections) match so the mobile
show/hide works.
- Line 40: The template is performing per-row DB membership checks via "section
in progress.sections_viewed.all" inside the sections loop; change the view that
renders this template to precompute a collection of viewed section IDs (e.g.,
viewed_section_ids = set(progress.sections_viewed.values_list('id', flat=True)))
and pass it into the template context, then update the template check to use
"section.id in viewed_section_ids" (keep existing symbols like
user.is_authenticated, section, and progress) to avoid repeated queryset
evaluation.

In `@web/templates/documentation_notes/topic_detail.html`:
- Around line 5-29: Remove the <style> block and any inline style attributes and
replace the custom CSS classes and variables (e.g., :root color variables and
classes topic-styled, topic-accent, topic-border, topic-bg) with equivalent
Tailwind utility classes; for example, convert the
gradient/foreground/background/border uses in
topic-styled/topic-accent/topic-border/topic-bg to Tailwind gradient,
text-color, border-color and bg-opacity utilities (choose the nearest
teal/teal-dark shades and rgba background using bg-opacity or bg-opacity +
bg-teal-... classes), and update all occurrences noted (lines referenced in the
comment) to use those Tailwind utilities instead of the custom classes or inline
styles.
- Around line 1-4: The template topic_detail.html currently extends "base.html"
and reimplements breadcrumb/sidebar/shell markup that already exists in the
docs-specific base; change the extends to "documentation_notes/base.html",
remove the duplicated breadcrumb/sidebar/shell markup (the repeated block around
lines 73-93) so the layout comes from the parent, and keep only the
page-specific content inside the existing block title and block content (use {{
block.super() }} or ensure blocks match the parent if you need to preserve any
parent content).
- Around line 137-142: The template inside the script tag with id
"doc-markdown-source" is emitting literal braces instead of rendering the Django
variable content.markdown_content; update the interpolation so the template
engine outputs the variable (use Django template variable output for
content.markdown_content with the correct brace syntax and no extra
spaces/newlines that make the braces literal) so the markdown is rendered rather
than shown as raw text.
- Around line 234-235: The code assigns window.marked.parse(rawMarkdown)
directly to markdownTarget.innerHTML, which is an XSS risk; update the template
to sanitize the parsed HTML before assigning it by either: (A) using a
client-side sanitizer function (e.g., integrate the project’s existing sanitizer
or a bundled DOM-sanitizer) to clean the output of
window.marked.parse(rawMarkdown) and then set markdownTarget.innerHTML to the
sanitized HTML, or (B) perform server-side sanitization with the existing bleach
usage so rawMarkdown is already safe when rendered; reference markdownTarget,
window.marked.parse, rawMarkdown, and innerHTML when making the change.
- Around line 243-252: Guard the progress POST by only running the fetch when
the user is authenticated and use the cookie-based CSRF token instead of
querying a non-existent form field: wrap or conditionalize the fetch block using
the template authentication flag (e.g. {% if user.is_authenticated %} ... {%
endif %}) so anonymous users never run the request, and replace the CSRF source
referenced by document.querySelector('[name=csrfmiddlewaretoken]') with a cookie
lookup (getCookie('csrftoken') or equivalent) and include that token in the
'X-CSRFToken' header for the fetch to the URL generated by {% url
"docs_mark_section_viewed" topic.slug current_section.slug %}.

In `@web/templates/documentation_notes/topics_list.html`:
- Around line 5-35: Remove the <style> block and any inline styles and replace
the .topic-color-header, .topic-card and the variant selectors
(.topic-card.color-blue, .topic-card.color-purple, .topic-card.color-pink,
.topic-card.color-green) with Tailwind utility classes only: pick equivalent
Tailwind background, gradient and height utilities (e.g., h-1, bg-gradient-to-r,
from-*, to-*, bg-*) and apply them conditionally in the template where the topic
card is rendered (map each variant name – color-blue, color-purple, color-pink,
color-green – to the appropriate Tailwind classes), remove custom CSS variables
(--topic-light, --topic-dark) and ensure no inline style attributes remain on
elements like the topic header or card container.

In `@web/urls.py`:
- Around line 83-96: The route for documentation_views.user_progress is being
shadowed by the dynamic slug routes (paths using "docs/<slug:topic_slug>/" and
"docs/<slug:topic_slug>/<slug:section_slug>/"); move the
path("docs/user/progress/", documentation_views.user_progress,
name="docs_user_progress") entry higher in the urlpatterns (place it before any
"docs/<slug:...>/" routes) so the literal "user/progress" URL is matched before
the slug-based patterns.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c94caf8 and 9e72dbd.

📒 Files selected for processing (15)
  • static/ts/documentation-notes.ts
  • web/admin.py
  • web/documentation_views.py
  • web/management/commands/__init__.py
  • web/management/commands/create_sample_docs.py
  • web/migrations/0064_add_documentation_notes.py
  • web/models.py
  • web/templates/documentation_notes/base.html
  • web/templates/documentation_notes/components/breadcrumb.html
  • web/templates/documentation_notes/components/navigation.html
  • web/templates/documentation_notes/components/sidebar.html
  • web/templates/documentation_notes/topic_detail.html
  • web/templates/documentation_notes/topics_list.html
  • web/tests/test_views.py
  • web/urls.py

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This PR has 10 unresolved review conversations. Please resolve them before this PR can be merged.

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: 7

♻️ Duplicate comments (12)
web/models.py (2)

3324-3326: ⚠️ Potential issue | 🟠 Major

Handle zero-section topics by resetting progress state.

Early return leaves stale completion_percentage/completed_at values if a topic ends up with no sections.

Suggested fix
     def update_progress(self):
         """Calculate and update completion percentage"""
         total_sections = self.topic.sections.count()
         if total_sections == 0:
-            return
+            self.completion_percentage = 0.0
+            self.completed_at = None
+            self.save(update_fields=["completion_percentage", "completed_at", "last_accessed_at"])
+            return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/models.py` around lines 3324 - 3326, When total_sections is zero the
current early return leaves stale progress on the model; in the branch where
total_sections == 0 (the check using self.topic.sections.count()) reset progress
fields by setting completion_percentage to 0 and completed_at to None on the
instance (and persist the change via save() or an update_fields call), instead
of returning immediately so the topic's progress state is cleared.

3206-3206: 🧹 Nitpick | 🔵 Trivial

Remove redundant indexes covered by uniqueness constraints.

SlugField(unique=True) and unique_together = ("user", "topic") already enforce indexed uniqueness; the extra explicit indexes duplicate that work.

Also applies to: 3317-3317

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/models.py` at line 3206, The Meta.indexes lists include explicit indexes
that duplicate uniqueness constraints — remove the redundant models.Index that
targets the slug (since SlugField(unique=True) already creates a unique index)
and any Index that duplicates the unique_together (e.g., the pair enforced by
unique_together = ("user", "topic")); keep only non-duplicative indexes such as
models.Index(fields=["is_published"]) in the class Meta.indexes definitions
(apply the same removal to the second occurrence around the unique_together
mention).
static/ts/documentation-notes.ts (2)

187-187: ⚠️ Potential issue | 🟠 Major

Harden HTML insertion paths before assigning to innerHTML.

Both fetched HTML and markdown output are inserted directly into the DOM. This is an avoidable XSS risk in the client layer.

Also applies to: 261-261

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/ts/documentation-notes.ts` at line 187, The assignment
contentArea.innerHTML = newContent.innerHTML (seen where contentArea and
newContent are used) directly injects fetched/generated HTML and is an XSS risk;
replace direct innerHTML usage by sanitizing the HTML before insertion (e.g.,
run newContent.innerHTML through a sanitizer like DOMPurify or a
project-approved allowlist sanitizer) or convert to safe DOM nodes via a
DOMParser and strip unsafe tags/attributes, then insert using appendChild or
textContent for untrusted strings; update both occurrences (the assignments
involving contentArea and newContent at the two locations) to use the sanitizer
utility so only allowed tags/attributes are injected.

62-66: ⚠️ Potential issue | 🔴 Critical

Fix delegated anchor handling: currentTarget is wrong in this flow.

With delegated listeners, event.currentTarget is the document, not the anchor. The current implementation can throw when calling getAttribute and breaks smooth scrolling.

Suggested fix
-        document.addEventListener('click', (e) => {
-            const link = (e.target as HTMLElement).closest('a[href^="#"]');
-            if (link) {
-                this.onAnchorClick(e as MouseEvent);
-            }
-        });
+        document.addEventListener('click', (e) => {
+            const link = (e.target as HTMLElement | null)?.closest('a[href^="#"]') as HTMLAnchorElement | null;
+            if (link) {
+                this.onAnchorClick(e, link);
+            }
+        });
@@
-    private onAnchorClick(event: Event): void {
-        const link = event.currentTarget as HTMLAnchorElement;
-        const href = link.getAttribute('href');
+    private onAnchorClick(event: Event, link?: HTMLAnchorElement): void {
+        const anchor = link ?? (event.currentTarget as HTMLAnchorElement);
+        const href = anchor.getAttribute('href');

Also applies to: 86-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/ts/documentation-notes.ts` around lines 62 - 66, The delegated click
handler uses event.currentTarget (document) which breaks anchor handling; locate
the document.addEventListener('click', ...) block and the onAnchorClick method
and change the call to pass the resolved anchor element (the result of (e.target
as HTMLElement).closest('a[href^="#"]')) into onAnchorClick (e.g.,
this.onAnchorClick(e as MouseEvent, link as HTMLAnchorElement)), and update
onAnchorClick to prefer the passed anchor parameter instead of reading
event.currentTarget; apply the same change for the other delegated listener at
the 86-89 region.
web/templates/documentation_notes/topic_detail.html (4)

1-1: 🛠️ Refactor suggestion | 🟠 Major

Extend the docs base template instead of duplicating the docs layout shell.

This file still reimplements breadcrumb/sidebar/shell markup that should be centralized in the docs base, which increases drift risk.

Also applies to: 73-93

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topic_detail.html` at line 1,
topic_detail.html reimplements the docs layout (breadcrumb/sidebar/shell)
instead of using the centralized docs base; update the template to extend the
docs base (use the docs base template name used in the project, e.g.
"docs/base.html" or the canonical docs layout) and remove the duplicated markup
(the breadcrumb/sidebar/shell markup blocks found in topic_detail.html and lines
~73-93), leaving only the page-specific blocks (e.g. block content, block title
or any small overrides). Ensure you reference the template symbol
topic_detail.html and use the docs base's block names so the page content
renders inside the centralized docs layout.

241-245: ⚠️ Potential issue | 🟠 Major

Current markdown “sanitization” is incomplete for safe innerHTML assignment.

Replacing a few encoded tags is not sufficient to neutralize unsafe HTML/URLs emitted by markdown parsing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topic_detail.html` around lines 241 - 245,
The code assigns parsed markdown (window.marked.parse(rawMarkdown)) directly to
markdownTarget.innerHTML and attempts weak tag replacements; instead, sanitize
the parsed HTML with a proven sanitizer before assigning: call a sanitizer
(e.g., DOMPurify.sanitize(parsedHtml)) on the value returned from
window.marked.parse(rawMarkdown) and assign that sanitized string to
markdownTarget.innerHTML (or use markdownTarget.textContent for plain text
output); update the code paths around markdownTarget, window.marked.parse, and
rawMarkdown to import/configure DOMPurify (or equivalent) and replace the manual
.replace(...) lines with a single sanitize step.

257-270: ⚠️ Potential issue | 🟠 Major

Use reliable CSRF sourcing and robust JSON response handling for progress POST.

Token lookup may be empty on this page, and the response is parsed as JSON without checking response.ok/content type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topic_detail.html` around lines 257 - 270,
The progress POST currently grabs csrfToken only from meta or form and calls
response.json() unconditionally; update the code around csrfToken and the fetch
to reliably source CSRF by adding a fallback getCookie('csrftoken') lookup (use
a small getCookie helper) and only send the X-CSRFToken header when a token
exists, and make the response handling for the fetch to '{% url
"docs_mark_section_viewed" topic.slug current_section.slug %}' robust by
checking response.ok and the Content-Type header (e.g., const ct =
response.headers.get('content-type') || '') before calling response.json(), and
handle non-OK or non-JSON responses gracefully (skip parsing or catch JSON parse
errors) instead of assuming response.json() will always succeed.

5-29: ⚠️ Potential issue | 🟠 Major

Remove custom CSS and inline styles; use Tailwind utilities only.

The template still uses a <style> block and inline style= attributes.

As per coding guidelines: "**/*.{html,htm}: Never use inline styles" and "**/*.{html,htm,css}: Never use custom CSS classes".

Also applies to: 111-112, 153-157, 177-178, 186-186

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topic_detail.html` around lines 5 - 29,
Remove the embedded <style> block and all custom CSS classes (.topic-styled,
.topic-accent, .topic-border, .topic-bg and any :root vars) and replace them in
the template markup with equivalent Tailwind utility classes (e.g., gradient
background, text-white, text-teal-500, border-teal-500, bg-teal-50, etc.); also
drop any inline style="..." usages referenced in the review and convert them to
Tailwind utilities on the same elements so the template uses only Tailwind
classes and no custom CSS or inline styles.
web/documentation_views.py (2)

91-100: ⚠️ Potential issue | 🟠 Major

Precompute viewed_section_ids in context for sidebar rendering.

The sidebar currently performs per-section membership checks against progress.sections_viewed; pass a precomputed id set from the view instead.

Also applies to: 140-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/documentation_views.py` around lines 91 - 100, Precompute a set of viewed
section IDs in the view and pass it into the template context instead of having
the template call membership on progress.sections_viewed repeatedly; e.g. build
viewed_section_ids = {s.id for s in progress.sections_viewed} (or use
values_list('id', flat=True) if it's a queryset) and add "viewed_section_ids":
viewed_section_ids to the context alongside topic, sections, current_section,
current_section_index, content, progress, next_section
(section.get_next_section()), and previous_section
(section.get_previous_section()); do the same replacement for the second context
block around the other occurrence (lines ~140-149) so the sidebar uses
viewed_section_ids for membership checks.

83-86: ⚠️ Potential issue | 🟠 Major

Avoid state mutation in GET views by removing get_or_create from read handlers.

The two GET detail endpoints still write progress rows on page render; writes should stay in the POST progress endpoint.

Also applies to: 132-135

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/documentation_views.py` around lines 83 - 86, The GET detail handlers
currently mutate state by calling
DocumentationNoteProgress.objects.get_or_create (assigning to progress); change
those calls to read-only queries (e.g., use
DocumentationNoteProgress.objects.filter(user=request.user, topic=topic).first()
or a try/except with DocumentationNoteProgress.objects.get) so no row is created
during GET rendering, remove the get_or_create call and any code that assumes
creation there, and ensure that creation/updating of DocumentationNoteProgress
remains only in the POST progress endpoint (leave that logic unchanged).
web/templates/documentation_notes/components/sidebar.html (1)

66-69: ⚠️ Potential issue | 🟡 Minor

Add ARIA state/relationship to the mobile sections toggle.

The button toggles content visibility but does not expose aria-controls/aria-expanded, so assistive tech cannot track state changes.

As per coding guidelines: "Ensure proper HTML structure and accessibility in templates" and "Include proper ARIA labels where needed for accessibility".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/components/sidebar.html` around lines 66 -
69, The toggle button that calls
document.getElementById('sidebar-content').classList.toggle('hidden') must
expose ARIA state and relationship so assistive tech can track it: add
aria-controls="sidebar-content" on the button and add an aria-expanded attribute
that is programmatically updated to "true"/"false" when the click handler
toggles the 'hidden' class; also include an accessible label (e.g., aria-label
or keep the visible text) and ensure the controlled element has the id
"sidebar-content" (or update the id reference) so aria-controls points to the
correct element.
web/templates/documentation_notes/topics_list.html (1)

60-63: ⚠️ Potential issue | 🟠 Major

Replace inline progress width styling with a Tailwind-compatible pattern.

The progress bar still uses inline style="width: ...".

As per coding guidelines: "**/*.{html,htm}: Never use inline styles".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topics_list.html` around lines 60 - 63, The
progress bar is using an inline style for width; replace it by mapping the
computed percentage to a Tailwind-compatible class instead. Add a template
filter (e.g., progress_width_class) that takes the value from
user_progress|dict_key:topic.id, normalizes/rounds it to your supported buckets
(e.g., 0,10,20,...,100) and returns a class name like "w-10p" or an existing
Tailwind class; update the inner div in topics_list.html to remove
style="width:..." and add the returned class (e.g., class="h-full transition-all
duration-300 {{ user_progress|dict_key:topic.id|progress_width_class }}"), and
add the corresponding CSS/Tailwind utilities (or generate w-0..w-100% utilities)
so the mapping classes (progress_width_class) control width without inline
styles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@static/ts/documentation-notes.ts`:
- Around line 190-192: The renderer currently calls renderMarkdown(contentArea)
but internally scans for [data-markdown] which doesn't match the page DOM;
update the DOM query logic so renderMarkdown (or the code path that scans for
markdown) looks for the actual selectors '#doc-markdown-source' and
'.doc-markdown' inside the provided container (contentArea) and processes those
nodes after AJAX replacement; also update the other occurrence in the same file
where renderMarkdown is invoked (the block that mirrors lines ~255-263) to use
the same selector logic so newly loaded sections are rendered correctly.
- Around line 206-214: Replace the brittle input-based CSRF lookup used before
the POST fetch with cookie-based retrieval: implement or call a
getCookie('csrftoken') helper to read the CSRF token from document.cookie
(falling back to the existing
document.querySelector('input[name="csrfmiddlewaretoken"]') only if the cookie
is missing) and use that value for the 'X-CSRFToken' header in the fetch; update
the code that assigns csrfToken and the fetch call to reference the
cookie-derived value (variables: csrfToken, and the POST fetch to
`/docs/${this.config.topicSlug}/section/${sectionSlug}/viewed/`).

In `@web/documentation_views.py`:
- Line 29: Replace the prefetch-only query with an annotated count to avoid
per-card DB hits: import Count from django.db.models, change the query that sets
topics (DocumentationNoteTopic.objects.filter(...)) to include
.annotate(section_count=Count("sections")) (you can keep
.prefetch_related("sections") if you still need the related objects), and update
the template to use topic.section_count instead of calling
topic.sections.count(), so each topic row uses the annotated value rather than
triggering extra queries.

In `@web/models.py`:
- Around line 3346-3349: Remove the redundant second DB write by deleting the
trailing self.save() after setting self.sections_viewed and
self.current_section; keep the calls to self.sections_viewed.add(section),
self.current_section = section, and self.update_progress() since
update_progress() already persists the model, so do not call save() again.

In `@web/templates/documentation_notes/topic_detail.html`:
- Line 127: The template currently checks current_section.icon but renders a
hardcoded emoji; update the conditional to render the configured icon value
(current_section.icon) instead of "📚". Locate the line using the conditional
with current_section.icon in the topic detail template and replace the hardcoded
emoji with the template expression that outputs current_section.icon (ensuring
it is escaped/safe as needed for your templating engine).
- Around line 256-284: The template has unbalanced template and JS block
boundaries: the `{% if user.is_authenticated %}` is never closed and the
trailing closing tokens around the fetch promise chain are mismatched. Fix by
closing the Django if block with `{% endif %}` after the fetch/.catch(...)
promise chain and correct the JS punctuation so the fetch call's
parentheses/braces are balanced (ensure the promise chain ends with `});` for
the fetch invocation and then place `{% endif %}` before the closing
`</script>`). Reference symbols: `user.is_authenticated`, the fetch to `{% url
"docs_mark_section_viewed" topic.slug current_section.slug %}`, and the
`progressBar` handling.

In `@web/templates/documentation_notes/topics_list.html`:
- Around line 50-53: The current conditional uses truthiness on
user_progress|dict_key:topic.id which treats 0 as false and hides valid 0%
progress; change the check to explicitly test for presence (not None) so zero is
allowed — e.g. replace occurrences like "{% if user.is_authenticated and
user_progress|dict_key:topic.id %}" with "{% if user.is_authenticated and
user_progress|dict_key:topic.id is not None %}", and make the same change at the
other occurrence around lines 59-60; reference symbols:
user_progress|dict_key:topic.id and topic.id.

---

Duplicate comments:
In `@static/ts/documentation-notes.ts`:
- Line 187: The assignment contentArea.innerHTML = newContent.innerHTML (seen
where contentArea and newContent are used) directly injects fetched/generated
HTML and is an XSS risk; replace direct innerHTML usage by sanitizing the HTML
before insertion (e.g., run newContent.innerHTML through a sanitizer like
DOMPurify or a project-approved allowlist sanitizer) or convert to safe DOM
nodes via a DOMParser and strip unsafe tags/attributes, then insert using
appendChild or textContent for untrusted strings; update both occurrences (the
assignments involving contentArea and newContent at the two locations) to use
the sanitizer utility so only allowed tags/attributes are injected.
- Around line 62-66: The delegated click handler uses event.currentTarget
(document) which breaks anchor handling; locate the
document.addEventListener('click', ...) block and the onAnchorClick method and
change the call to pass the resolved anchor element (the result of (e.target as
HTMLElement).closest('a[href^="#"]')) into onAnchorClick (e.g.,
this.onAnchorClick(e as MouseEvent, link as HTMLAnchorElement)), and update
onAnchorClick to prefer the passed anchor parameter instead of reading
event.currentTarget; apply the same change for the other delegated listener at
the 86-89 region.

In `@web/documentation_views.py`:
- Around line 91-100: Precompute a set of viewed section IDs in the view and
pass it into the template context instead of having the template call membership
on progress.sections_viewed repeatedly; e.g. build viewed_section_ids = {s.id
for s in progress.sections_viewed} (or use values_list('id', flat=True) if it's
a queryset) and add "viewed_section_ids": viewed_section_ids to the context
alongside topic, sections, current_section, current_section_index, content,
progress, next_section (section.get_next_section()), and previous_section
(section.get_previous_section()); do the same replacement for the second context
block around the other occurrence (lines ~140-149) so the sidebar uses
viewed_section_ids for membership checks.
- Around line 83-86: The GET detail handlers currently mutate state by calling
DocumentationNoteProgress.objects.get_or_create (assigning to progress); change
those calls to read-only queries (e.g., use
DocumentationNoteProgress.objects.filter(user=request.user, topic=topic).first()
or a try/except with DocumentationNoteProgress.objects.get) so no row is created
during GET rendering, remove the get_or_create call and any code that assumes
creation there, and ensure that creation/updating of DocumentationNoteProgress
remains only in the POST progress endpoint (leave that logic unchanged).

In `@web/models.py`:
- Around line 3324-3326: When total_sections is zero the current early return
leaves stale progress on the model; in the branch where total_sections == 0 (the
check using self.topic.sections.count()) reset progress fields by setting
completion_percentage to 0 and completed_at to None on the instance (and persist
the change via save() or an update_fields call), instead of returning
immediately so the topic's progress state is cleared.
- Line 3206: The Meta.indexes lists include explicit indexes that duplicate
uniqueness constraints — remove the redundant models.Index that targets the slug
(since SlugField(unique=True) already creates a unique index) and any Index that
duplicates the unique_together (e.g., the pair enforced by unique_together =
("user", "topic")); keep only non-duplicative indexes such as
models.Index(fields=["is_published"]) in the class Meta.indexes definitions
(apply the same removal to the second occurrence around the unique_together
mention).

In `@web/templates/documentation_notes/components/sidebar.html`:
- Around line 66-69: The toggle button that calls
document.getElementById('sidebar-content').classList.toggle('hidden') must
expose ARIA state and relationship so assistive tech can track it: add
aria-controls="sidebar-content" on the button and add an aria-expanded attribute
that is programmatically updated to "true"/"false" when the click handler
toggles the 'hidden' class; also include an accessible label (e.g., aria-label
or keep the visible text) and ensure the controlled element has the id
"sidebar-content" (or update the id reference) so aria-controls points to the
correct element.

In `@web/templates/documentation_notes/topic_detail.html`:
- Line 1: topic_detail.html reimplements the docs layout
(breadcrumb/sidebar/shell) instead of using the centralized docs base; update
the template to extend the docs base (use the docs base template name used in
the project, e.g. "docs/base.html" or the canonical docs layout) and remove the
duplicated markup (the breadcrumb/sidebar/shell markup blocks found in
topic_detail.html and lines ~73-93), leaving only the page-specific blocks (e.g.
block content, block title or any small overrides). Ensure you reference the
template symbol topic_detail.html and use the docs base's block names so the
page content renders inside the centralized docs layout.
- Around line 241-245: The code assigns parsed markdown
(window.marked.parse(rawMarkdown)) directly to markdownTarget.innerHTML and
attempts weak tag replacements; instead, sanitize the parsed HTML with a proven
sanitizer before assigning: call a sanitizer (e.g.,
DOMPurify.sanitize(parsedHtml)) on the value returned from
window.marked.parse(rawMarkdown) and assign that sanitized string to
markdownTarget.innerHTML (or use markdownTarget.textContent for plain text
output); update the code paths around markdownTarget, window.marked.parse, and
rawMarkdown to import/configure DOMPurify (or equivalent) and replace the manual
.replace(...) lines with a single sanitize step.
- Around line 257-270: The progress POST currently grabs csrfToken only from
meta or form and calls response.json() unconditionally; update the code around
csrfToken and the fetch to reliably source CSRF by adding a fallback
getCookie('csrftoken') lookup (use a small getCookie helper) and only send the
X-CSRFToken header when a token exists, and make the response handling for the
fetch to '{% url "docs_mark_section_viewed" topic.slug current_section.slug %}'
robust by checking response.ok and the Content-Type header (e.g., const ct =
response.headers.get('content-type') || '') before calling response.json(), and
handle non-OK or non-JSON responses gracefully (skip parsing or catch JSON parse
errors) instead of assuming response.json() will always succeed.
- Around line 5-29: Remove the embedded <style> block and all custom CSS classes
(.topic-styled, .topic-accent, .topic-border, .topic-bg and any :root vars) and
replace them in the template markup with equivalent Tailwind utility classes
(e.g., gradient background, text-white, text-teal-500, border-teal-500,
bg-teal-50, etc.); also drop any inline style="..." usages referenced in the
review and convert them to Tailwind utilities on the same elements so the
template uses only Tailwind classes and no custom CSS or inline styles.

In `@web/templates/documentation_notes/topics_list.html`:
- Around line 60-63: The progress bar is using an inline style for width;
replace it by mapping the computed percentage to a Tailwind-compatible class
instead. Add a template filter (e.g., progress_width_class) that takes the value
from user_progress|dict_key:topic.id, normalizes/rounds it to your supported
buckets (e.g., 0,10,20,...,100) and returns a class name like "w-10p" or an
existing Tailwind class; update the inner div in topics_list.html to remove
style="width:..." and add the returned class (e.g., class="h-full transition-all
duration-300 {{ user_progress|dict_key:topic.id|progress_width_class }}"), and
add the corresponding CSS/Tailwind utilities (or generate w-0..w-100% utilities)
so the mapping classes (progress_width_class) control width without inline
styles.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e72dbd and 0b6e186.

📒 Files selected for processing (8)
  • static/ts/documentation-notes.ts
  • web/documentation_views.py
  • web/models.py
  • web/templates/documentation_notes/components/breadcrumb.html
  • web/templates/documentation_notes/components/sidebar.html
  • web/templates/documentation_notes/topic_detail.html
  • web/templates/documentation_notes/topics_list.html
  • web/urls.py

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

♻️ Duplicate comments (11)
static/ts/documentation-notes.ts (3)

206-213: ⚠️ Potential issue | 🟠 Major

CSRF retrieval should use the csrftoken cookie first.

Lines 207-210 depend on optional DOM nodes and can skip progress POSTs when those nodes are absent.

Suggested fix
+            const getCookie = (name: string): string => {
+                const cookie = document.cookie
+                    .split('; ')
+                    .find((row) => row.startsWith(`${name}=`));
+                return cookie ? decodeURIComponent(cookie.split('=')[1]) : '';
+            };
+
-            let csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
+            let csrfToken = getCookie('csrftoken');
             if (!csrfToken) {
+                csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
+            }
+            if (!csrfToken) {
                 const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]') as HTMLInputElement;
                 csrfToken = csrfInput?.value;
             }
#!/bin/bash
# Check whether docs templates expose csrf meta/input fallbacks.
rg -n "csrf-token|csrfmiddlewaretoken" web/templates/documentation_notes --glob '*.html'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/ts/documentation-notes.ts` around lines 206 - 213, The CSRF lookup
currently only checks a meta tag and form input (variable csrfToken via
document.querySelector), which can be absent and cause POSTs to be skipped;
modify the retrieval so it first reads the "csrftoken" cookie, then falls back
to the meta tag ('meta[name="csrf-token"]') and finally to the input
('input[name="csrfmiddlewaretoken"]') before bailing, ensuring any code that
uses csrfToken continues to work when templates omit the meta/input.

62-66: ⚠️ Potential issue | 🟠 Major

Delegated anchor handling is using the wrong target object.

Line 87 reads event.currentTarget, but with delegation from Line 62 that value is document, not the clicked anchor.

Suggested fix
-        document.addEventListener('click', (e) => {
-            const link = (e.target as HTMLElement).closest('a[href^="#"]');
-            if (link) {
-                this.onAnchorClick(e as MouseEvent);
-            }
-        });
+        document.addEventListener('click', (e) => {
+            const link = (e.target as HTMLElement | null)?.closest('a[href^="#"]') as HTMLAnchorElement | null;
+            if (link) {
+                this.onAnchorClick(e as MouseEvent, link);
+            }
+        });
@@
-    private onAnchorClick(event: Event): void {
-        const link = event.currentTarget as HTMLAnchorElement;
-        const href = link.getAttribute('href');
+    private onAnchorClick(event: Event, link: HTMLAnchorElement): void {
+        const href = link.getAttribute('href');
#!/bin/bash
# Verify delegated click binding and onAnchorClick target usage.
sed -n '60,95p' static/ts/documentation-notes.ts

Also applies to: 86-88

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/ts/documentation-notes.ts` around lines 62 - 66, Delegated click
handler uses document as currentTarget, so update the delegation to pass the
clicked anchor to onAnchorClick instead of relying on event.currentTarget:
inside the document.addEventListener callback, capture the anchor (const link =
(e.target as HTMLElement).closest('a[href^="#"]') as HTMLAnchorElement) and call
this.onAnchorClick(e as MouseEvent, link) or this.onAnchorClick.call(this, e as
MouseEvent, link); then update the onAnchorClick method signature
(onAnchorClick(event: MouseEvent, anchor?: HTMLAnchorElement)) to use the
provided anchor (or fallback to event.target) when computing the target element
instead of using event.currentTarget.

271-273: ⚠️ Potential issue | 🔴 Critical

Markdown output is injected without real sanitization.

Replacing encoded &lt;script strings does not sanitize actual HTML returned by marked.parse(...) before innerHTML.

Suggested fix
+import DOMPurify from 'dompurify';
@@
-                    (el as any).innerHTML = (window as any).marked.parse(content).replace(/&lt;script/gi, '&amp;lt;script').replace(/&lt;iframe/gi, '&amp;lt;iframe').replace(/&lt;object/gi, '&amp;lt;object');
+                    const rendered = (window as any).marked.parse(content);
+                    (el as HTMLElement).innerHTML = DOMPurify.sanitize(rendered);
#!/bin/bash
# Verify markdown parsing output is directly assigned to innerHTML.
rg -n "marked\\.parse|innerHTML|replace\\(/&lt;script" static/ts/documentation-notes.ts -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/ts/documentation-notes.ts` around lines 271 - 273, The code assigns
marked.parse(content) directly into (el as any).innerHTML after only replacing
encoded tag prefixes, which doesn't sanitize real HTML; wrap the parsed HTML
with a proper sanitizer before assignment (e.g., import and use
DOMPurify.sanitize(parsed) or an equivalent HTML sanitizer) and assign the
sanitized string to innerHTML instead of the raw marked output; reference the
code around marked.parse(content) and the (el as any).innerHTML assignment, and
ensure you configure the sanitizer (allowed tags/attributes or remove
scripts/iframes) and fall back to setting textContent when sanitization isn't
available.
web/templates/documentation_notes/topic_detail.html (5)

249-253: ⚠️ Potential issue | 🔴 Critical

marked output is injected without robust sanitization.

The replace chain only targets encoded substrings and does not safely sanitize full parsed HTML before innerHTML.

Suggested fix
+  <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
@@
-                  markdownTarget.innerHTML = window.marked
-                      .parse(rawMarkdown)
-                      .replace(/&lt;script/gi, '&amp;lt;script')
-                      .replace(/&lt;iframe/gi, '&amp;lt;iframe')
-                      .replace(/&lt;object/gi, '&amp;lt;object');
+                  const rendered = window.marked.parse(rawMarkdown)
+                  markdownTarget.innerHTML = DOMPurify.sanitize(rendered)
#!/bin/bash
# Verify markdown parse result is assigned directly to innerHTML.
rg -n "marked\\.parse|innerHTML|replace\\(/&lt;script" web/templates/documentation_notes/topic_detail.html -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topic_detail.html` around lines 249 - 253,
The code assigns window.marked.parse(rawMarkdown) into markdownTarget.innerHTML
with only minimal replace calls, which is unsafe; update the rendering in the
handler that calls window.marked.parse(rawMarkdown) and sets
markdownTarget.innerHTML to instead sanitize the parsed HTML (e.g., pass the
parse result through a sanitizer like DOMPurify or use a safe renderer) before
assigning to markdownTarget.innerHTML, ensuring all uses of window.marked.parse,
rawMarkdown, and markdownTarget.innerHTML are updated so the parsed HTML cannot
inject scripts/iframes/objects.

5-29: ⚠️ Potential issue | 🟠 Major

Remove custom CSS and inline styles; keep Tailwind utility classes only.

This block reintroduces <style>-defined custom classes and multiple inline style= attributes.

As per coding guidelines: "**/*.{html,htm}: Always use Tailwind CSS classes for styling HTML elements", "**/*.{html,htm}: Never use inline styles", and "**/*.{html,htm,css}: Never use custom CSS classes".

Also applies to: 111-112, 159-163, 183-184, 192-193

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topic_detail.html` around lines 5 - 29,
Remove the entire <style> block (including :root variables and the CSS classes
.topic-styled, .topic-accent, .topic-border, .topic-bg) and replace usages of
those classes in the template with equivalent Tailwind utility classes (e.g.,
gradient, text, border, and bg utilities) applied directly on the elements that
reference .topic-styled, .topic-accent, .topic-border, and .topic-bg; ensure no
inline style= attributes remain and update all occurrences mentioned (lines
referenced in the review) so styling comes solely from Tailwind classes.

137-143: ⚠️ Potential issue | 🔴 Critical

Fix malformed markdown template interpolation.

Lines 137-143 render literal braces instead of content.markdown_content, so markdown source is not output correctly.

Suggested fix
-                  <script id="doc-markdown-source" type="text/plain">
-                      {
-                          {
-                              content.markdown_content
-                          }
-                      }
-                  </script>
+                  <script id="doc-markdown-source" type="text/plain">{{ content.markdown_content }}</script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topic_detail.html` around lines 137 - 143,
The template currently outputs literal braces instead of the variable; update
the script block with correct Django template interpolation so the markdown is
rendered from content.markdown_content (the script element with id
"doc-markdown-source"). Replace the malformed "{{ { content.markdown_content }
}}" with the proper "{{ content.markdown_content }}" and, if the markdown must
be included unescaped, append the |safe filter (e.g.,
content.markdown_content|safe).

268-276: ⚠️ Potential issue | 🟠 Major

Use cookie-derived CSRF token for the POST request.

Lines 268-269 rely on meta/input lookups that are often missing in this template path.

Suggested fix
-          const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content ||
-              document.querySelector('[name=csrfmiddlewaretoken]')?.value || '';
+          function getCookie(name) {
+              const value = `; ${document.cookie}`;
+              const parts = value.split(`; ${name}=`);
+              return parts.length === 2 ? parts.pop().split(';').shift() : '';
+          }
+          const csrfToken = getCookie('csrftoken') ||
+              document.querySelector('meta[name="csrf-token"]')?.content ||
+              document.querySelector('[name=csrfmiddlewaretoken]')?.value || '';
#!/bin/bash
# Verify CSRF token sources referenced in this template.
rg -n "csrf-token|csrfmiddlewaretoken|docs_mark_section_viewed" web/templates/documentation_notes/topic_detail.html -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topic_detail.html` around lines 268 - 276,
The current CSRF lookup for csrfToken (variable csrfToken) only checks meta and
input elements which may be absent; update the fetch request preparation (the
fetch to {% url "docs_mark_section_viewed" topic.slug current_section.slug %})
to also derive the CSRF token from the cookie (the standard Django "csrftoken"
cookie) as a fallback: implement a small cookie-parsing fallback that reads
document.cookie for "csrftoken" and uses that value when meta/input lookups
return empty so the 'X-CSRFToken' header always has a token.

263-267: ⚠️ Potential issue | 🔴 Critical

The auth conditional is invalid template syntax and breaks JavaScript parsing.

Lines 263-267 and 294-296 emit % tokens as literal JS, which can prevent the whole handler from running.

Suggested fix
-          {
-              %
-              if user.is_authenticated %
-          }
+          {% if user.is_authenticated %}
@@
-          } {
-              % endif %
-          }
+          {% endif %}
#!/bin/bash
# Inspect the broken auth-guard block in script.
sed -n '260,299p' web/templates/documentation_notes/topic_detail.html

Also applies to: 294-296

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topic_detail.html` around lines 263 - 267,
The template contains malformed Jinja tags that emit literal "%" into the
JavaScript (the auth-guard block using "if user.is_authenticated" currently has
stray braces/percent tokens); fix the block by replacing the invalid tokens with
proper Jinja control tags so it reads {% if user.is_authenticated %} ... {%
endif %} (no extra braces or standalone "%" characters) around the JS
progress-tracking code that references the auth conditional, and ensure the same
fix is applied to the second occurrence of the auth block so no "%" tokens are
output into the script.
web/models.py (1)

3324-3326: ⚠️ Potential issue | 🟠 Major

Reset persisted progress when a topic has zero sections.

When total_sections == 0, Line 3326 returns immediately and leaves any previous completion_percentage/completed_at values unchanged.

Suggested fix
     def update_progress(self):
         """Calculate and update completion percentage"""
         total_sections = self.topic.sections.count()
         if total_sections == 0:
-            return
+            self.completion_percentage = 0.0
+            self.completed_at = None
+            self.save(update_fields=["completion_percentage", "completed_at", "last_accessed_at"])
+            return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/models.py` around lines 3324 - 3326, When total_sections computed by
self.topic.sections.count() is zero you must reset persisted progress instead of
returning early: set the model fields completion_percentage = 0 and completed_at
= None (or null), persist the change (e.g. self.save() or an update with
update_fields=['completion_percentage','completed_at']), then return; use the
existing total_sections check, the completion_percentage and completed_at
fields, and the enclosing method where self.topic.sections.count() is used to
locate where to apply this change.
web/documentation_views.py (1)

85-88: ⚠️ Potential issue | 🟠 Major

GET detail views still write progress rows via get_or_create().

Lines 85-88 and 134-137 mutate state during read handlers; use read-only lookup in GET and keep writes in mark_section_viewed.

Suggested fix
-    progress = None
-    if request.user.is_authenticated:
-        progress, _ = DocumentationNoteProgress.objects.get_or_create(
-            user=request.user,
-            topic=topic,
-        )
+    progress = None
+    if request.user.is_authenticated:
+        progress = DocumentationNoteProgress.objects.filter(
+            user=request.user,
+            topic=topic,
+        ).first()

Also applies to: 134-137

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/documentation_views.py` around lines 85 - 88, The GET handlers currently
call DocumentationNoteProgress.objects.get_or_create(...) (see usage of
DocumentationNoteProgress and get_or_create) which mutates DB; change these to
read-only lookups (e.g.,
DocumentationNoteProgress.objects.filter(user=request.user, topic=topic).first()
or try/except DocumentationNoteProgress.DoesNotExist) so the GET detail views do
not create rows, and ensure any creation/updating of progress remains only
inside the existing mark_section_viewed function (call that from
POST/side-effect endpoints if needed).
web/templates/documentation_notes/topics_list.html (1)

65-65: ⚠️ Potential issue | 🟡 Minor

Avoid inline width styling for progress fill.

Line 65 uses inline style, which violates the template styling constraints.

As per coding guidelines: "**/*.{html,htm}: Never use inline styles" and "**/*.{html,htm,css}: Never use custom CSS classes".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/documentation_notes/topics_list.html` at line 65, Replace the
inline style on the progress-fill div (the expression
user_progress|dict_key:topic.id|floatformat:0 used to set width) by emitting the
progress value as a data attribute (e.g. data-progress="{{
user_progress|dict_key:topic.id|floatformat:0 }}") and move the width logic into
the stylesheet using an attribute selector (e.g. targeting the existing
progress-fill element via [data-progress] and setting width via content/var or
attr() in the CSS); update the template to remove the style attribute and set
data-progress instead and update the CSS to compute the fill width from that
attribute so there are no inline styles or new custom classes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/documentation_views.py`:
- Line 80: The view currently uses get_object_or_404 to fetch
DocumentationNoteContent (the line setting content =
get_object_or_404(DocumentationNoteContent, section=section)), which causes a
404 when a section intentionally has no content; change this to fetch the
content non-exceptionally (e.g., use
DocumentationNoteContent.objects.filter(section=section).first() or try to get()
and catch DocumentationNoteContent.DoesNotExist to set content = None) so the
detail view can render an empty section; apply the same change to the other
occurrence around the code that currently uses get_object_or_404 for
DocumentationNoteContent (the second instance referenced at lines ~129).

In `@web/templates/documentation_notes/topics_list.html`:
- Line 51: Template currently calls topic.sections.count which triggers
per-topic DB queries; replace that with the annotated field topic.section_count
(the annotation added in the view) so the template reads topic.section_count and
uses the precomputed value from the queryset instead of invoking
topic.sections.count. Ensure any pluralization or display formatting still works
when switching to topic.section_count.

---

Duplicate comments:
In `@static/ts/documentation-notes.ts`:
- Around line 206-213: The CSRF lookup currently only checks a meta tag and form
input (variable csrfToken via document.querySelector), which can be absent and
cause POSTs to be skipped; modify the retrieval so it first reads the
"csrftoken" cookie, then falls back to the meta tag ('meta[name="csrf-token"]')
and finally to the input ('input[name="csrfmiddlewaretoken"]') before bailing,
ensuring any code that uses csrfToken continues to work when templates omit the
meta/input.
- Around line 62-66: Delegated click handler uses document as currentTarget, so
update the delegation to pass the clicked anchor to onAnchorClick instead of
relying on event.currentTarget: inside the document.addEventListener callback,
capture the anchor (const link = (e.target as
HTMLElement).closest('a[href^="#"]') as HTMLAnchorElement) and call
this.onAnchorClick(e as MouseEvent, link) or this.onAnchorClick.call(this, e as
MouseEvent, link); then update the onAnchorClick method signature
(onAnchorClick(event: MouseEvent, anchor?: HTMLAnchorElement)) to use the
provided anchor (or fallback to event.target) when computing the target element
instead of using event.currentTarget.
- Around line 271-273: The code assigns marked.parse(content) directly into (el
as any).innerHTML after only replacing encoded tag prefixes, which doesn't
sanitize real HTML; wrap the parsed HTML with a proper sanitizer before
assignment (e.g., import and use DOMPurify.sanitize(parsed) or an equivalent
HTML sanitizer) and assign the sanitized string to innerHTML instead of the raw
marked output; reference the code around marked.parse(content) and the (el as
any).innerHTML assignment, and ensure you configure the sanitizer (allowed
tags/attributes or remove scripts/iframes) and fall back to setting textContent
when sanitization isn't available.

In `@web/documentation_views.py`:
- Around line 85-88: The GET handlers currently call
DocumentationNoteProgress.objects.get_or_create(...) (see usage of
DocumentationNoteProgress and get_or_create) which mutates DB; change these to
read-only lookups (e.g.,
DocumentationNoteProgress.objects.filter(user=request.user, topic=topic).first()
or try/except DocumentationNoteProgress.DoesNotExist) so the GET detail views do
not create rows, and ensure any creation/updating of progress remains only
inside the existing mark_section_viewed function (call that from
POST/side-effect endpoints if needed).

In `@web/models.py`:
- Around line 3324-3326: When total_sections computed by
self.topic.sections.count() is zero you must reset persisted progress instead of
returning early: set the model fields completion_percentage = 0 and completed_at
= None (or null), persist the change (e.g. self.save() or an update with
update_fields=['completion_percentage','completed_at']), then return; use the
existing total_sections check, the completion_percentage and completed_at
fields, and the enclosing method where self.topic.sections.count() is used to
locate where to apply this change.

In `@web/templates/documentation_notes/topic_detail.html`:
- Around line 249-253: The code assigns window.marked.parse(rawMarkdown) into
markdownTarget.innerHTML with only minimal replace calls, which is unsafe;
update the rendering in the handler that calls window.marked.parse(rawMarkdown)
and sets markdownTarget.innerHTML to instead sanitize the parsed HTML (e.g.,
pass the parse result through a sanitizer like DOMPurify or use a safe renderer)
before assigning to markdownTarget.innerHTML, ensuring all uses of
window.marked.parse, rawMarkdown, and markdownTarget.innerHTML are updated so
the parsed HTML cannot inject scripts/iframes/objects.
- Around line 5-29: Remove the entire <style> block (including :root variables
and the CSS classes .topic-styled, .topic-accent, .topic-border, .topic-bg) and
replace usages of those classes in the template with equivalent Tailwind utility
classes (e.g., gradient, text, border, and bg utilities) applied directly on the
elements that reference .topic-styled, .topic-accent, .topic-border, and
.topic-bg; ensure no inline style= attributes remain and update all occurrences
mentioned (lines referenced in the review) so styling comes solely from Tailwind
classes.
- Around line 137-143: The template currently outputs literal braces instead of
the variable; update the script block with correct Django template interpolation
so the markdown is rendered from content.markdown_content (the script element
with id "doc-markdown-source"). Replace the malformed "{{ {
content.markdown_content } }}" with the proper "{{ content.markdown_content }}"
and, if the markdown must be included unescaped, append the |safe filter (e.g.,
content.markdown_content|safe).
- Around line 268-276: The current CSRF lookup for csrfToken (variable
csrfToken) only checks meta and input elements which may be absent; update the
fetch request preparation (the fetch to {% url "docs_mark_section_viewed"
topic.slug current_section.slug %}) to also derive the CSRF token from the
cookie (the standard Django "csrftoken" cookie) as a fallback: implement a small
cookie-parsing fallback that reads document.cookie for "csrftoken" and uses that
value when meta/input lookups return empty so the 'X-CSRFToken' header always
has a token.
- Around line 263-267: The template contains malformed Jinja tags that emit
literal "%" into the JavaScript (the auth-guard block using "if
user.is_authenticated" currently has stray braces/percent tokens); fix the block
by replacing the invalid tokens with proper Jinja control tags so it reads {% if
user.is_authenticated %} ... {% endif %} (no extra braces or standalone "%"
characters) around the JS progress-tracking code that references the auth
conditional, and ensure the same fix is applied to the second occurrence of the
auth block so no "%" tokens are output into the script.

In `@web/templates/documentation_notes/topics_list.html`:
- Line 65: Replace the inline style on the progress-fill div (the expression
user_progress|dict_key:topic.id|floatformat:0 used to set width) by emitting the
progress value as a data attribute (e.g. data-progress="{{
user_progress|dict_key:topic.id|floatformat:0 }}") and move the width logic into
the stylesheet using an attribute selector (e.g. targeting the existing
progress-fill element via [data-progress] and setting width via content/var or
attr() in the CSS); update the template to remove the style attribute and set
data-progress instead and update the CSS to compute the fill width from that
attribute so there are no inline styles or new custom classes.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b6e186 and ad12e29.

📒 Files selected for processing (6)
  • static/ts/documentation-notes.ts
  • web/documentation_views.py
  • web/models.py
  • web/templates/documentation_notes/components/breadcrumb.html
  • web/templates/documentation_notes/topic_detail.html
  • web/templates/documentation_notes/topics_list.html

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 3, 2026
@github-actions github-actions bot dismissed their stale review March 3, 2026 15:32

All review conversations have been resolved.

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

Labels

files-changed: 15 PR changes 15 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant