Skip to content
Merged
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
e486dce
backend initial code
siddhant3030 Feb 26, 2026
8d867f2
cleanup
siddhant3030 Mar 6, 2026
e87bb3a
public sharing
siddhant3030 Mar 9, 2026
2f33e7b
report filter added
siddhant3030 Mar 10, 2026
922a932
delete report can be done only who has created it
siddhant3030 Mar 11, 2026
0fb828f
added test cases and ability to find dashboard filter first
siddhant3030 Mar 12, 2026
36de2a0
add comment
siddhant3030 Mar 12, 2026
8fd15f8
adding filtering logic
siddhant3030 Mar 12, 2026
a372f50
claude.md changes
siddhant3030 Mar 13, 2026
b598be7
code style fixed
siddhant3030 Mar 13, 2026
5b03227
pdf download server side
siddhant3030 Mar 14, 2026
8f6a058
report comments initial crud operations
siddhant3030 Mar 15, 2026
4c035b4
new approach for auth
siddhant3030 Mar 16, 2026
83f49b0
we're not storing the pdf anywhere
siddhant3030 Mar 16, 2026
18a0e2f
cleaned up migration
siddhant3030 Mar 16, 2026
8a8f1cb
added test cases
siddhant3030 Mar 16, 2026
4f4818a
fixing test cases
siddhant3030 Mar 16, 2026
d425872
structuring of various service and api layer
siddhant3030 Mar 16, 2026
a54de8a
last few changes
siddhant3030 Mar 16, 2026
25ec2d6
cleanup
siddhant3030 Mar 16, 2026
e0e2ca7
fixed warehouse utils to warehouse func
siddhant3030 Mar 16, 2026
ebc814d
moved test to where other core file fixed
siddhant3030 Mar 16, 2026
c54c320
Merge branch 'main' into feature/report_backend
siddhant3030 Mar 17, 2026
b45de14
resolved comments for warehouse call
siddhant3030 Mar 17, 2026
60c154c
renaming, removing quotes, snapshotlist to snapshotresponse schema na…
siddhant3030 Mar 17, 2026
c075633
import fix
siddhant3030 Mar 17, 2026
63be05d
removed unused imports
siddhant3030 Mar 17, 2026
1b137d4
Removed the __all__ block
siddhant3030 Mar 17, 2026
3bdb256
removed iso format
siddhant3030 Mar 17, 2026
efe2974
now its injecting filter on every chart
siddhant3030 Mar 18, 2026
389d915
Merge remote-tracking branch 'origin/feature/report_backend' into fea…
siddhant3030 Mar 18, 2026
296860b
revert comments
siddhant3030 Mar 18, 2026
6cae4e7
we
siddhant3030 Mar 18, 2026
53ee77f
mention part is working fine now
siddhant3030 Mar 18, 2026
b7688ce
remove notification for now
siddhant3030 Mar 19, 2026
ca62bfb
added comment in .env
siddhant3030 Mar 19, 2026
8b588b3
removed archived snapshot
siddhant3030 Mar 19, 2026
69d25fa
snapshot update
siddhant3030 Mar 19, 2026
77afcae
completely removed report status column
siddhant3030 Mar 19, 2026
649a3ea
Added FrozenDashboardConfig and FrozenChartConfig Pydantic schemas
siddhant3030 Mar 19, 2026
4093eb4
added one rule in claude.md
siddhant3030 Mar 19, 2026
7a101dc
migration change
siddhant3030 Mar 19, 2026
3da2fd2
reordering of datetime
siddhant3030 Mar 19, 2026
7adada5
Merge origin/main into feature/report_backend
siddhant3030 Mar 20, 2026
b528487
Merge feature/report_backend into feature/report-comments
siddhant3030 Mar 20, 2026
7ae4b18
soft delete is done
siddhant3030 Mar 21, 2026
afb800a
added report link to dashboard
siddhant3030 Mar 21, 2026
d639d19
changes in comment
siddhant3030 Mar 21, 2026
e0f0d1c
mention notification is built now
siddhant3030 Mar 22, 2026
7da0c28
chart name is showing when people are tagging
siddhant3030 Mar 22, 2026
4727898
redirecting to comments
siddhant3030 Mar 22, 2026
fce0e30
revert unrelated changes: restore admin.py, settings.py, and dependen…
siddhant3030 Mar 23, 2026
b813b29
removed unwanted files
siddhant3030 Mar 23, 2026
e96dcea
local import removed
siddhant3030 Mar 23, 2026
9d3df11
refactored and added more test cases
siddhant3030 Mar 24, 2026
d983652
Merge remote-tracking branch 'origin/main' into feature/report-comments
siddhant3030 Mar 24, 2026
7eac83e
formatted files
siddhant3030 Mar 25, 2026
442c2df
Merge branch 'main' into feature/report-comments
siddhant3030 Mar 25, 2026
bda1fcb
removed admin.py file
siddhant3030 Mar 25, 2026
10354af
removed local imports
siddhant3030 Mar 25, 2026
c607d23
changed the structure for how we're sending read and unread to frontend
siddhant3030 Mar 25, 2026
cb1283b
query change
siddhant3030 Mar 25, 2026
a98a9bf
removed some unused checks
siddhant3030 Mar 25, 2026
892922c
now mentioned user comes from frontend
siddhant3030 Mar 25, 2026
7308a18
enum type added
siddhant3030 Mar 25, 2026
7fdce79
logger update
siddhant3030 Mar 25, 2026
ca38ddb
code changes
siddhant3030 Mar 25, 2026
58315ec
changes
siddhant3030 Mar 25, 2026
2a33ae3
cleanup
siddhant3030 Mar 26, 2026
8eb077e
added index on snapshot id
siddhant3030 Mar 26, 2026
f989ddc
removed the helper function
siddhant3030 Mar 26, 2026
6adc87c
changed the query
siddhant3030 Mar 26, 2026
bcc01f4
structure the code a bit
siddhant3030 Mar 26, 2026
d9f1ca7
Merge branch 'main' into feature/report-comments
siddhant3030 Mar 27, 2026
74a169e
added more test cases
siddhant3030 Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ ddpui/
├── core/
│ └── {module}/ # Feature module (all business logic here)
│ ├── __init__.py
│ ├── __init__.py # Keep empty (no re-exports or __all__)
│ ├── {module}_service.py # Business logic and orchestration
│ ├── {module}_operations.py # Domain operations (optional)
│ └── exceptions.py # Custom exceptions for this feature
Expand Down Expand Up @@ -297,7 +297,7 @@ def delete_{module}(request, id: int):

```
ddpui/core/{module}/
├── __init__.py # Export public interfaces
├── __init__.py # Keep empty — do NOT add re-exports or __all__
├── {module}_service.py # Business logic and orchestration
├── {module}_operations.py # Domain-specific operations (optional)
└── exceptions.py # Custom exceptions
Expand Down Expand Up @@ -1105,15 +1105,9 @@ ddpui/

#### `core/charts/__init__.py`
```python
from .chart_service import ChartService
from .exceptions import ChartError, ChartNotFoundError, ChartValidationError

__all__ = [
"ChartService",
"ChartError",
"ChartNotFoundError",
"ChartValidationError",
]
# Keep empty — all imports should use full paths like:
# from ddpui.core.charts.chart_service import ChartService
# Do NOT add re-exports or __all__ here.
```

#### `exceptions.py`
Expand Down
174 changes: 165 additions & 9 deletions ddpui/api/report_api.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Report API endpoints"""

import secrets
from typing import List

from django.http import HttpResponse
from ninja import Router
from ninja.errors import HttpError

from ddpui.auth import has_permission
from ddpui.core.reports.comment_service import CommentService
from ddpui.core.reports.exceptions import (
CommentNotFoundError,
CommentPermissionError,
CommentValidationError,
SnapshotNotFoundError,
SnapshotPermissionError,
SnapshotValidationError,
Expand All @@ -19,7 +22,13 @@
from ddpui.models.org_user import OrgUser
from ddpui.schemas.dashboard_schema import ShareResponse, ShareStatus, ShareToggle
from ddpui.schemas.report_schema import (
CommentCreate,
CommentResponse,
CommentStatesResponse,
CommentUpdate,
DatetimeColumnResponse,
MarkReadRequest,
MentionableUserResponse,
SnapshotCreate,
SnapshotDeleteResponse,
SnapshotResponse,
Expand Down Expand Up @@ -80,6 +89,19 @@ def create_snapshot(request, payload: SnapshotCreate):
raise HttpError(500, "Failed to create snapshot") from e


@report_router.get("/mentionable-users/", response=ApiResponse[List[MentionableUserResponse]])
@has_permission(["can_view_dashboards"])
def get_mentionable_users(request):
"""List org users available for @mention"""
orguser: OrgUser = request.orguser

users = CommentService.get_mentionable_users(orguser.org)
return api_response(
success=True,
data=[MentionableUserResponse.from_orguser(u) for u in users],
)


@report_router.get("/{snapshot_id}/view/", response=ApiResponse[SnapshotViewResponse])
@has_permission(["can_view_dashboards"])
def get_snapshot_view(request, snapshot_id: int):
Expand Down Expand Up @@ -144,14 +166,8 @@ def export_report_pdf(request, snapshot_id: int):
raise HttpError(404, str(err)) from err

try:
# Ensure the snapshot has a share token (needed for the URL).
# This does NOT make the report publicly accessible — Playwright
# authenticates via the render secret header, not is_public.
if not snapshot.public_share_token:
snapshot.public_share_token = secrets.token_urlsafe(48)
snapshot.save(update_fields=["public_share_token"])

pdf_bytes = PdfExportService.generate_pdf(snapshot_id, snapshot.public_share_token)
share_token = ReportService.ensure_share_token(snapshot)
pdf_bytes = PdfExportService.generate_pdf(snapshot_id, share_token)

safe_title = "".join(c for c in snapshot.title if c.isalnum() or c in " -_").strip()
filename = f"{safe_title or 'report'}.pdf"
Expand Down Expand Up @@ -217,3 +233,143 @@ def get_report_sharing_status(request, snapshot_id: int):
raise HttpError(404, str(err)) from err
except SnapshotPermissionError as err:
raise HttpError(403, str(err)) from err


# ===== Comment Endpoints (nested under /{snapshot_id}/comments/) =====


@report_router.get("/{snapshot_id}/comments/states/", response=ApiResponse[CommentStatesResponse])
@has_permission(["can_view_dashboards"])
def get_comment_states(request, snapshot_id: int):
"""Get icon states for all targets in a snapshot"""
orguser: OrgUser = request.orguser

try:
states = CommentService.get_comment_states(
snapshot_id=snapshot_id,
org=orguser.org,
orguser=orguser,
)
return api_response(
success=True,
data=CommentStatesResponse(states=states),
)
except CommentValidationError as err:
raise HttpError(400, str(err)) from err


@report_router.post("/{snapshot_id}/comments/mark-read/", response=ApiResponse)
@has_permission(["can_view_dashboards"])
def mark_as_read(request, snapshot_id: int, payload: MarkReadRequest):
"""Mark a target's comments as read"""
orguser: OrgUser = request.orguser

try:
CommentService.mark_as_read(
snapshot_id=snapshot_id,
orguser=orguser,
target_type=payload.target_type,
chart_id=payload.chart_id,
)
return api_response(success=True, message="Marked as read")
except CommentValidationError as err:
raise HttpError(400, str(err)) from err


@report_router.get("/{snapshot_id}/comments/", response=ApiResponse[List[CommentResponse]])
@has_permission(["can_view_dashboards"])
def list_comments(
request,
snapshot_id: int,
target_type: str,
chart_id: int = None,
):
"""List comments for a report target"""
orguser: OrgUser = request.orguser

try:
comments = CommentService.list_comments(
snapshot_id=snapshot_id,
org=orguser.org,
target_type=target_type,
chart_id=chart_id,
orguser=orguser,
)
return api_response(
success=True,
data=[CommentResponse.from_model(c) for c in comments],
)
except CommentValidationError as err:
raise HttpError(400, str(err)) from err


@report_router.post("/{snapshot_id}/comments/", response=ApiResponse[CommentResponse])
@has_permission(["can_view_dashboards"])
def create_comment(request, snapshot_id: int, payload: CommentCreate):
"""Create a comment on a report snapshot"""
orguser: OrgUser = request.orguser

try:
comment = CommentService.create_comment(
snapshot_id=snapshot_id,
org=orguser.org,
orguser=orguser,
target_type=payload.target_type,
content=payload.content,
chart_id=payload.chart_id,
mentioned_emails=payload.mentioned_emails,
)
return api_response(
success=True,
data=CommentResponse.from_model(comment),
message="Comment created",
)
except CommentValidationError as err:
raise HttpError(400, str(err)) from err
except Exception as e:
logger.error(f"Error creating comment: {e}", exc_info=True)
raise HttpError(500, "Failed to create comment") from e


@report_router.put("/{snapshot_id}/comments/{comment_id}/", response=ApiResponse[CommentResponse])
@has_permission(["can_view_dashboards"])
def update_comment(request, snapshot_id: int, comment_id: int, payload: CommentUpdate):
"""Update a comment (author-only)"""
orguser: OrgUser = request.orguser

try:
comment = CommentService.update_comment(
comment_id=comment_id,
org=orguser.org,
orguser=orguser,
content=payload.content,
mentioned_emails=payload.mentioned_emails,
)
return api_response(
success=True,
data=CommentResponse.from_model(comment),
message="Comment updated",
)
except CommentNotFoundError as err:
raise HttpError(404, str(err)) from err
except CommentPermissionError as err:
raise HttpError(403, str(err)) from err


@report_router.delete("/{snapshot_id}/comments/{comment_id}/", response=ApiResponse)
@has_permission(["can_view_dashboards"])
def delete_comment(request, snapshot_id: int, comment_id: int):
"""Delete a comment (author-only)"""
orguser: OrgUser = request.orguser

try:
CommentService.delete_comment(
comment_id=comment_id,
org=orguser.org,
orguser=orguser,
)
return api_response(success=True, message="Comment deleted")
except CommentNotFoundError as err:
raise HttpError(404, str(err)) from err
except CommentPermissionError as err:
raise HttpError(403, str(err)) from err
Loading
Loading