Skip to content

Conversation

@mrkeshav-05
Copy link
Contributor

Proposed change

This PR fixes the issue where unpublished programs were accessible via direct URLs. Now, unpublished programs return a 404 "Page Not Found" error. Similarly, modules belonging to unpublished programs also return 404, ensuring draft content remains hidden from public view.


Resolves: #2859

Checklist

  • Required: I read and followed the contributing guidelines
  • Required: I ran make check-test locally and all tests passed
  • I used AI for code, documentation, or tests in this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 31, 2025

Summary by CodeRabbit

  • New Features

    • Automatically clears program-related GraphQL cache when a program is saved.
  • Bug Fixes

    • Public queries now only surface published programs and modules.
    • Improved error handling and clearer feedback on program and module pages (loading, not-found, and retry states).
    • Program listings now consistently show only published programs.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds backend guards to prevent public access to unpublished programs and modules, a GraphQL resolver cache invalidation utility and signal handler, and frontend changes to surface Not Found/unavailable states and filter listings to published programs only.

Changes

Cohort / File(s) Summary
GraphQL cache util
backend/apps/core/utils/index.py
New invalidate_program_graphql_cache(program_key: str) — builds hashed GraphQL resolver keys for getProgram and getProgramModules, composes keys with GRAPHQL_RESOLVER_CACHE_PREFIX, deletes cache entries, and logs invalidations. Adds hashlib, json imports and uses settings.
Program & Module query access controls (backend)
backend/apps/mentorship/api/internal/queries/program.py, backend/apps/mentorship/api/internal/queries/module.py
Enforces published-status checks: get_program() raises ObjectDoesNotExist for unpublished/missing; get_program_modules() returns empty for unpublished/missing and logs; get_module() validates parent program is published and raises/logs consistently; get_project_modules() uses published_modules manager.
Program post_save signals
backend/apps/mentorship/signals/program.py
Adds program_post_save_clear_graphql_cache receiver that logs and calls invalidate_program_graphql_cache(instance.key) alongside existing Algolia cache clearing handler.
Frontend — program listing
frontend/src/app/mentorship/programs/page.tsx
Introduces publishedPrograms filter (status "Published", case-insensitive) and uses it for rendering program cards.
Frontend — program details page
frontend/src/app/mentorship/programs/[programKey]/page.tsx
Adds hasError/hasAttemptedLoad states, handles query error and absent getProgram, updates effect dependencies, gates loader until first attempt, and shows Not Found/Unavailable when program missing.
Frontend — module details page
frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx
Adds hasError state, explicit error handling (calls handleAppError, clears module/admins), guards admin data, updates effect deps, and renders ErrorDisplay when module missing or errored.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • arkid15r
  • kasya

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main objective of the changeset: hiding unpublished programs and modules from public view.
Description check ✅ Passed The description directly addresses the changeset by explaining how unpublished programs now return 404 errors and modules from unpublished programs are hidden, which matches the code changes.
Linked Issues check ✅ Passed The PR meets all core requirements from issue #2859: unpublished programs are filtered from listings [frontend], direct URL access to unpublished programs returns 404 [backend queries], modules from unpublished programs are hidden [backend queries and frontend], and GraphQL cache is invalidated on program changes [signals].
Out of Scope Changes check ✅ Passed All changes are directly scoped to hiding unpublished content: backend access control, frontend filtering, error handling, cache invalidation on program updates, and error display logic. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 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: 2

🧹 Nitpick comments (2)
backend/apps/mentorship/api/internal/queries/module.py (1)

21-43: Access control properly implemented.

The function correctly blocks access to modules from unpublished programs and logs security-relevant attempts. The empty-list return for missing/unpublished programs provides a consistent public API.

Optional: Minor efficiency improvement

Since you've already fetched the program object at line 27, you could use program.modules instead of filtering by program__key again:

-        return (
-            Module.objects.filter(program__key=program_key)
-            .select_related("program", "project")
-            .prefetch_related("mentors__github_user")
-            .order_by("started_at")
-        )
+        return (
+            program.modules
+            .select_related("program", "project")
+            .prefetch_related("mentors__github_user")
+            .order_by("started_at")
+        )
frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx (1)

50-50: Consider whether moduleKey and programKey dependencies are necessary.

Apollo Client automatically refetches the query when variables change (which include moduleKey and programKey). Including them in the useEffect dependencies may be redundant and could cause the effect to run with stale data before the new query completes.

During navigation between modules, the effect might briefly process old module data with new parameter values, though this doesn't leak unpublished content since the backend controls what's returned.

🔎 Simplified dependency array (if no specific edge case requires the extra deps)
-  }, [data, error, moduleKey, programKey])
+  }, [data, error])

If these dependencies address a specific edge case, consider adding a comment explaining why they're needed.

📜 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 c29a4db and 2bd5857.

📒 Files selected for processing (7)
  • backend/apps/core/utils/index.py
  • backend/apps/mentorship/api/internal/queries/module.py
  • backend/apps/mentorship/api/internal/queries/program.py
  • backend/apps/mentorship/signals/program.py
  • frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx
  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
  • frontend/src/app/mentorship/programs/page.tsx
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-08-31T13:47:15.861Z
Learnt from: rudransh-shrivastava
Repo: OWASP/Nest PR: 2155
File: frontend/src/server/queries/programsQueries.ts:81-81
Timestamp: 2025-08-31T13:47:15.861Z
Learning: In frontend/src/server/queries/programsQueries.ts, GET_PROGRAM_DETAILS is actively used in frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx for program editing functionality and cannot be removed. It serves a different purpose than GET_PROGRAM_ADMIN_DETAILS, providing comprehensive program information needed for editing.

Applied to files:

  • frontend/src/app/mentorship/programs/page.tsx
  • frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx
  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
  • backend/apps/mentorship/api/internal/queries/module.py
  • backend/apps/mentorship/api/internal/queries/program.py
📚 Learning: 2025-08-31T13:47:15.861Z
Learnt from: rudransh-shrivastava
Repo: OWASP/Nest PR: 2155
File: frontend/src/server/queries/programsQueries.ts:81-81
Timestamp: 2025-08-31T13:47:15.861Z
Learning: In frontend/src/server/queries/programsQueries.ts, GET_PROGRAM_DETAILS and GET_PROGRAM_ADMIN_DETAILS are two separate queries serving different purposes: GET_PROGRAM_DETAILS fetches comprehensive program information while GET_PROGRAM_ADMIN_DETAILS fetches only admin-related details. These queries cannot be removed or merged as they serve different use cases in the application.

Applied to files:

  • frontend/src/app/mentorship/programs/page.tsx
  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
  • backend/apps/mentorship/api/internal/queries/program.py
📚 Learning: 2025-08-31T13:47:15.861Z
Learnt from: rudransh-shrivastava
Repo: OWASP/Nest PR: 2155
File: frontend/src/server/queries/programsQueries.ts:81-81
Timestamp: 2025-08-31T13:47:15.861Z
Learning: In frontend/src/server/queries/programsQueries.ts, GET_PROGRAM_DETAILS and GET_PROGRAM_ADMIN_DETAILS are two separate queries serving different purposes: GET_PROGRAM_DETAILS fetches comprehensive program information while GET_PROGRAM_ADMIN_DETAILS fetches only admin-related details.

Applied to files:

  • frontend/src/app/mentorship/programs/page.tsx
  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
  • backend/apps/mentorship/api/internal/queries/program.py
📚 Learning: 2025-09-29T06:02:35.566Z
Learnt from: rudransh-shrivastava
Repo: OWASP/Nest PR: 2178
File: frontend/src/components/SingleModuleCard.tsx:54-54
Timestamp: 2025-09-29T06:02:35.566Z
Learning: In the Module type from types/mentorship.ts, the experienceLevel field is required (experienceLevel: ExperienceLevelEnum), not optional, so null/undefined checks are not needed when accessing this field.

Applied to files:

  • frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx
📚 Learning: 2025-07-12T17:14:28.536Z
Learnt from: Rajgupta36
Repo: OWASP/Nest PR: 1717
File: frontend/src/app/mentorship/programs/[programKey]/edit/page.tsx:90-128
Timestamp: 2025-07-12T17:14:28.536Z
Learning: Both ProgramForm (programCard.tsx) and ModuleForm (mainmoduleCard.tsx) components already implement HTML validation using the `required` attribute on form fields. The browser's native validation prevents form submission and displays error messages when required fields are empty, eliminating the need for additional JavaScript validation before GraphQL mutations.

Applied to files:

  • frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx
  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
📚 Learning: 2025-12-31T05:17:39.659Z
Learnt from: kart-u
Repo: OWASP/Nest PR: 3101
File: backend/apps/common/extensions.py:92-98
Timestamp: 2025-12-31T05:17:39.659Z
Learning: In this codebase, import OperationType for GraphQL operations from the graphql-core package rather than from strawberry. Use 'from graphql import OperationType'. Strawberry re-exports via graphql-core internally, so relying on strawberry's API may be brittle. Apply this rule to all Python files that deal with GraphQL operation types; ensure imports come from graphql (graphql-core) and not from strawberry packages. This improves compatibility and avoids coupling to strawberry's internals.

Applied to files:

  • backend/apps/core/utils/index.py
  • backend/apps/mentorship/signals/program.py
  • backend/apps/mentorship/api/internal/queries/module.py
  • backend/apps/mentorship/api/internal/queries/program.py
📚 Learning: 2025-07-11T15:57:56.648Z
Learnt from: Rajgupta36
Repo: OWASP/Nest PR: 1717
File: backend/apps/mentorship/graphql/queries/module.py:39-50
Timestamp: 2025-07-11T15:57:56.648Z
Learning: In the OWASP Nest mentorship GraphQL queries, Strawberry GraphQL automatically converts between Django Module instances and ModuleNode types, so methods can return Module instances directly without manual conversion even when typed as ModuleNode.

Applied to files:

  • backend/apps/mentorship/api/internal/queries/module.py
📚 Learning: 2025-07-13T05:55:46.436Z
Learnt from: Rajgupta36
Repo: OWASP/Nest PR: 1717
File: backend/apps/mentorship/graphql/mutations/program.py:166-166
Timestamp: 2025-07-13T05:55:46.436Z
Learning: In the OWASP Nest mentorship GraphQL mutations, Strawberry GraphQL automatically converts between Django Program instances and ProgramNode types, so mutations can return Program instances directly without manual conversion even when typed as ProgramNode, similar to the Module/ModuleNode pattern.

Applied to files:

  • backend/apps/mentorship/api/internal/queries/module.py
🧬 Code graph analysis (6)
frontend/src/app/mentorship/programs/page.tsx (1)
backend/apps/mentorship/api/internal/nodes/enum.py (1)
  • ProgramStatusEnum (22-27)
frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx (1)
frontend/src/app/global-error.tsx (2)
  • handleAppError (67-91)
  • ErrorDisplay (28-52)
backend/apps/mentorship/signals/program.py (2)
backend/apps/core/utils/index.py (2)
  • clear_index_cache (236-262)
  • invalidate_program_graphql_cache (264-279)
backend/apps/mentorship/models/program.py (1)
  • Program (18-87)
frontend/src/app/mentorship/programs/[programKey]/page.tsx (1)
frontend/src/app/global-error.tsx (1)
  • ErrorDisplay (28-52)
backend/apps/mentorship/api/internal/queries/module.py (2)
backend/apps/mentorship/models/program.py (2)
  • Program (18-87)
  • ProgramStatus (25-28)
backend/apps/mentorship/models/module.py (1)
  • Module (17-99)
backend/apps/mentorship/api/internal/queries/program.py (1)
backend/apps/mentorship/models/program.py (2)
  • Program (18-87)
  • ProgramStatus (25-28)
🪛 Ruff (0.14.10)
backend/apps/core/utils/index.py

267-267: Missing blank line after last section ("Args")

Add blank line after "Args"

(D413)


276-276: Undefined name json

(F821)


277-277: Undefined name hashlib

(F821)


279-279: No newline at end of file

Add trailing newline

(W292)

backend/apps/mentorship/signals/program.py

32-32: First argument of a method should be named self

Rename sender to self

(N805)

🔇 Additional comments (10)
backend/apps/mentorship/api/internal/queries/module.py (3)

10-10: LGTM—Program import added correctly.

The Program model import is necessary for the new status checks and properly used throughout the file.


61-90: LGTM—Comprehensive access control with proper error handling.

The function correctly enforces published-only access and provides uniform error messages across all failure paths (unpublished, missing program, missing module). The warning logs will help track unauthorized access attempts.


46-58: PublishedModuleManager correctly filters by program publication status.

The manager implementation at backend/apps/mentorship/models/managers/module.py confirms that get_queryset() filters by program__status=Program.ProgramStatus.PUBLISHED. The switch to Module.published_modules in get_project_modules() is correct and ensures only modules from published programs are returned.

backend/apps/mentorship/api/internal/queries/program.py (1)

23-45: LGTM—Access control correctly blocks unpublished programs.

The status check properly restricts public access to published programs only. The consistent error messages and warning logs align well with the module access controls in the related file.

backend/apps/mentorship/signals/program.py (1)

31-42: LGTM—GraphQL cache invalidation integrated correctly.

The signal handler properly clears GraphQL cache when a program is saved, ensuring cached queries reflect the latest program status. The logging provides good observability.

Note: The Ruff N805 warning about sender is a false positive—Django signal handlers use sender as the first parameter by convention, not self.

frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx (3)

20-49: LGTM! Error handling properly implements unpublished module restrictions.

The addition of the hasError state and the extended handling logic correctly addresses the PR objectives:

  • Lines 30-37: Properly handle GraphQL errors by calling handleAppError, clearing state, and setting the error flag.
  • Lines 44-49: Crucially handle the case where the backend returns successfully but getModule is null (unpublished or non-existent modules), ensuring these are treated as "not found."

This implementation ensures that unpublished modules are never displayed and users receive appropriate 404 responses.


41-41: Good addition of optional chaining for safety.

The optional chaining (data.getProgram?.admins || null) prevents potential crashes if getProgram is null or undefined, which could occur when a program is unpublished or unavailable.


54-59: Improved error condition and message clarity.

The updated condition (hasError || !module) makes the error state more explicit, and the revised message ("doesn't exist or is not available") appropriately communicates both missing and unpublished module scenarios to users.

frontend/src/app/mentorship/programs/[programKey]/page.tsx (1)

22-22: Excellent error handling for unpublished programs.

The changes correctly handle three cases:

  1. GraphQL errors (line 55): Caught and displayed as 404
  2. Null getProgram response (line 68): Treated as an error, which is correct when the backend returns null for unpublished programs
  3. Loading state management (lines 79-81): The hasAttemptedLoad flag prevents premature rendering of the error state before the initial query completes

The error message "doesn't exist or is not available" (line 88) is well-chosen—it doesn't leak information about whether a program exists but is unpublished versus truly not existing.

The dependency array (line 77) correctly includes error and programKey, ensuring the component re-processes when the route changes or error state updates.

Also applies to: 34-35, 55-77, 79-90

frontend/src/app/mentorship/programs/page.tsx (1)

36-39: Remove the redundant client-side filter for code clarity.

The backend Algolia index for programs is explicitly pre-filtered to include only PUBLISHED status programs (Program.objects.filter(status=Program.ProgramStatus.PUBLISHED) in ProgramIndex.get_entities()). This means the client-side filter at lines 36-39 is redundant—all items returned from the search are already published.

Additionally, the frontend pagination and result counts are correct because they reflect only published programs from the index, not all programs in the database.

The client-side filter can be safely removed:

const publishedPrograms = programs

Note: COMPLETED programs are intentionally excluded from the index (backend design), not as a result of this client-side filter.

Likely an incorrect or invalid review comment.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 31, 2025
@mrkeshav-05 mrkeshav-05 marked this pull request as ready for review December 31, 2025 14:03
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: 0

🧹 Nitpick comments (3)
backend/apps/mentorship/api/internal/queries/module.py (2)

26-43: Consider using the already-fetched program object in the query.

After fetching the program object on line 27, the query on line 39 still filters by program__key=program_key, which causes an extra join. You can use the fetched object directly.

🔎 Suggested optimization
         except Program.DoesNotExist:
             return []
 
         return (
-            Module.objects.filter(program__key=program_key)
+            Module.objects.filter(program=program)
             .select_related("program", "project")
             .prefetch_related("mentors__github_user")
             .order_by("started_at")
         )

59-89: Good security-conscious error handling.

The uniform error messages across all failure cases (unpublished, program not found, module not found) correctly prevent information leakage about publication status. The logging provides adequate context for debugging while keeping user-facing errors opaque.

Similar to get_program_modules, the query on line 80 could use program_id=program.id instead of program__key=program_key to avoid the redundant join.

🔎 Suggested optimization
             return (
                 Module.objects.select_related("program", "project")
                 .prefetch_related("mentors__github_user")
-                .get(key=module_key, program__key=program_key)
+                .get(key=module_key, program=program)
             )
backend/apps/core/utils/index.py (1)

268-286: Consider adding error handling and input validation for robustness.

The function is called from a post_save signal handler in backend/apps/mentorship/signals/program.py. While the cache key format is correct and matches the GraphQL resolver implementation, adding defensive measures would improve reliability:

  • Add a try-except block around cache operations to prevent exceptions from propagating to the signal handler
  • Validate that program_key is non-empty before proceeding
Suggested improvements
 def invalidate_program_graphql_cache(program_key: str) -> None:
     """Invalidate GraphQL cache for a specific program.
 
     Args:
         program_key: The program's unique key.
 
     """
+    if not program_key:
+        logger.warning("Cannot invalidate GraphQL cache: program_key is empty")
+        return
+
     queries_to_invalidate = [
         ("getProgram", {"programKey": program_key}),
         ("getProgramModules", {"programKey": program_key}),
     ]
 
-    for field_name, field_args in queries_to_invalidate:
-        key = f"{field_name}:{json.dumps(field_args, sort_keys=True)}"
-        cache_key = (
-            f"{settings.GRAPHQL_RESOLVER_CACHE_PREFIX}-{hashlib.sha256(key.encode()).hexdigest()}"
-        )
-        cache.delete(cache_key)
-        logger.info("Invalidated GraphQL cache for %s with key: %s", field_name, program_key)
+    try:
+        for field_name, field_args in queries_to_invalidate:
+            key = f"{field_name}:{json.dumps(field_args, sort_keys=True)}"
+            cache_key = (
+                f"{settings.GRAPHQL_RESOLVER_CACHE_PREFIX}-{hashlib.sha256(key.encode()).hexdigest()}"
+            )
+            cache.delete(cache_key)
+            logger.info("Invalidated GraphQL cache for %s with key: %s", field_name, program_key)
+    except Exception as e:
+        logger.error("Failed to invalidate GraphQL cache for program %s: %s", program_key, e)
📜 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 e624c5e and c352495.

📒 Files selected for processing (3)
  • backend/apps/core/utils/index.py
  • backend/apps/mentorship/api/internal/queries/module.py
  • backend/apps/mentorship/signals/program.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/apps/mentorship/signals/program.py
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: Rajgupta36
Repo: OWASP/Nest PR: 1717
File: frontend/src/components/ModuleForm.tsx:112-134
Timestamp: 2025-07-14T16:18:07.287Z
Learning: In the OWASP Nest mentorship system, date validation for modules (ensuring start date precedes end date) is handled on the backend in the module GraphQL mutations via the `_validate_module_dates` helper function in backend/apps/mentorship/graphql/mutations/module.py, which prevents invalid date ranges from being stored in the database.
📚 Learning: 2025-12-31T05:17:39.659Z
Learnt from: kart-u
Repo: OWASP/Nest PR: 3101
File: backend/apps/common/extensions.py:92-98
Timestamp: 2025-12-31T05:17:39.659Z
Learning: In this codebase, import OperationType for GraphQL operations from the graphql-core package rather than from strawberry. Use 'from graphql import OperationType'. Strawberry re-exports via graphql-core internally, so relying on strawberry's API may be brittle. Apply this rule to all Python files that deal with GraphQL operation types; ensure imports come from graphql (graphql-core) and not from strawberry packages. This improves compatibility and avoids coupling to strawberry's internals.

Applied to files:

  • backend/apps/core/utils/index.py
  • backend/apps/mentorship/api/internal/queries/module.py
📚 Learning: 2025-08-31T13:47:15.861Z
Learnt from: rudransh-shrivastava
Repo: OWASP/Nest PR: 2155
File: frontend/src/server/queries/programsQueries.ts:81-81
Timestamp: 2025-08-31T13:47:15.861Z
Learning: In frontend/src/server/queries/programsQueries.ts, GET_PROGRAM_DETAILS is actively used in frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx for program editing functionality and cannot be removed. It serves a different purpose than GET_PROGRAM_ADMIN_DETAILS, providing comprehensive program information needed for editing.

Applied to files:

  • backend/apps/mentorship/api/internal/queries/module.py
📚 Learning: 2025-07-11T15:57:56.648Z
Learnt from: Rajgupta36
Repo: OWASP/Nest PR: 1717
File: backend/apps/mentorship/graphql/queries/module.py:39-50
Timestamp: 2025-07-11T15:57:56.648Z
Learning: In the OWASP Nest mentorship GraphQL queries, Strawberry GraphQL automatically converts between Django Module instances and ModuleNode types, so methods can return Module instances directly without manual conversion even when typed as ModuleNode.

Applied to files:

  • backend/apps/mentorship/api/internal/queries/module.py
📚 Learning: 2025-07-13T05:55:46.436Z
Learnt from: Rajgupta36
Repo: OWASP/Nest PR: 1717
File: backend/apps/mentorship/graphql/mutations/program.py:166-166
Timestamp: 2025-07-13T05:55:46.436Z
Learning: In the OWASP Nest mentorship GraphQL mutations, Strawberry GraphQL automatically converts between Django Program instances and ProgramNode types, so mutations can return Program instances directly without manual conversion even when typed as ProgramNode, similar to the Module/ModuleNode pattern.

Applied to files:

  • backend/apps/mentorship/api/internal/queries/module.py
🧬 Code graph analysis (2)
backend/apps/core/utils/index.py (2)
backend/apps/common/index.py (1)
  • register (104-123)
backend/apps/owasp/api/internal/nodes/project.py (1)
  • key (68-70)
backend/apps/mentorship/api/internal/queries/module.py (3)
backend/apps/mentorship/models/program.py (2)
  • Program (18-87)
  • ProgramStatus (25-28)
backend/apps/mentorship/models/module.py (1)
  • Module (17-99)
backend/apps/mentorship/api/internal/nodes/module.py (1)
  • ModuleNode (19-159)
🔇 Additional comments (2)
backend/apps/mentorship/api/internal/queries/module.py (1)

45-57: LGTM! Clean use of the published_modules manager.

The implementation correctly leverages the PublishedModuleManager to filter modules from published programs (via program__status=PUBLISHED), keeping the query code clean and maintainable.

backend/apps/core/utils/index.py (1)

4-5: Imports correctly added.

The missing imports (hashlib, json, and settings) flagged in the previous review have been properly added. This resolves the runtime NameError issue.

Also applies to: 11-11

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 31, 2025
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 1, 2026
@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 2, 2026

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

🧹 Nitpick comments (1)
frontend/src/app/mentorship/programs/[programKey]/page.tsx (1)

82-93: Refactor dependencies to avoid fragile state cycle.

Including hasError, hasAttemptedLoad, and program in the dependencies array creates a pattern where the effect can re-trigger itself after updating these state variables. While the current guards (lines 57, 67, 73) prevent infinite loops, this is fragile:

  • Future modifications to the guards could introduce infinite re-renders
  • ESLint exhaustive-deps rules may flag this pattern
  • The intent is harder to understand for maintainers
🔎 Suggested approaches to simplify

Option 1: Use refs for values that don't need to trigger effects

+ const hasErrorRef = useRef(false)
+ const hasAttemptedLoadRef = useRef(false)

  useEffect(() => {
    const processResult = async () => {
      // ...
      if (error) {
-       if (!hasError) {
+       if (!hasErrorRef.current) {
-         setHasError(true)
+         hasErrorRef.current = true
+         setHasError(true)
          setProgram(null)
          setModules([])
-         setHasAttemptedLoad(true)
+         hasAttemptedLoadRef.current = true
+         setHasAttemptedLoad(true)
        }
        return
      }
      // similar changes for other branches...
    }
    processResult()
- }, [shouldRefresh, data, error, refetch, router, searchParams, programKey, hasError, hasAttemptedLoad, program])
+ }, [shouldRefresh, data, error, refetch, router, searchParams, programKey])

Option 2: Use a reducer to manage related state

Consider consolidating hasError, hasAttemptedLoad, program, and modules into a single reducer state to eliminate interdependencies.

📜 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 9e403f5 and 4528b64.

📒 Files selected for processing (1)
  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: rudransh-shrivastava
Repo: OWASP/Nest PR: 2155
File: frontend/src/server/queries/programsQueries.ts:81-81
Timestamp: 2025-08-31T13:47:15.861Z
Learning: In frontend/src/server/queries/programsQueries.ts, GET_PROGRAM_DETAILS is actively used in frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx for program editing functionality and cannot be removed. It serves a different purpose than GET_PROGRAM_ADMIN_DETAILS, providing comprehensive program information needed for editing.
📚 Learning: 2025-08-31T13:47:15.861Z
Learnt from: rudransh-shrivastava
Repo: OWASP/Nest PR: 2155
File: frontend/src/server/queries/programsQueries.ts:81-81
Timestamp: 2025-08-31T13:47:15.861Z
Learning: In frontend/src/server/queries/programsQueries.ts, GET_PROGRAM_DETAILS is actively used in frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx for program editing functionality and cannot be removed. It serves a different purpose than GET_PROGRAM_ADMIN_DETAILS, providing comprehensive program information needed for editing.

Applied to files:

  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
📚 Learning: 2025-08-31T13:47:15.861Z
Learnt from: rudransh-shrivastava
Repo: OWASP/Nest PR: 2155
File: frontend/src/server/queries/programsQueries.ts:81-81
Timestamp: 2025-08-31T13:47:15.861Z
Learning: In frontend/src/server/queries/programsQueries.ts, GET_PROGRAM_DETAILS and GET_PROGRAM_ADMIN_DETAILS are two separate queries serving different purposes: GET_PROGRAM_DETAILS fetches comprehensive program information while GET_PROGRAM_ADMIN_DETAILS fetches only admin-related details. These queries cannot be removed or merged as they serve different use cases in the application.

Applied to files:

  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
📚 Learning: 2025-08-31T13:47:15.861Z
Learnt from: rudransh-shrivastava
Repo: OWASP/Nest PR: 2155
File: frontend/src/server/queries/programsQueries.ts:81-81
Timestamp: 2025-08-31T13:47:15.861Z
Learning: In frontend/src/server/queries/programsQueries.ts, GET_PROGRAM_DETAILS and GET_PROGRAM_ADMIN_DETAILS are two separate queries serving different purposes: GET_PROGRAM_DETAILS fetches comprehensive program information while GET_PROGRAM_ADMIN_DETAILS fetches only admin-related details.

Applied to files:

  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
📚 Learning: 2025-07-12T17:14:28.536Z
Learnt from: Rajgupta36
Repo: OWASP/Nest PR: 1717
File: frontend/src/app/mentorship/programs/[programKey]/edit/page.tsx:90-128
Timestamp: 2025-07-12T17:14:28.536Z
Learning: Both ProgramForm (programCard.tsx) and ModuleForm (mainmoduleCard.tsx) components already implement HTML validation using the `required` attribute on form fields. The browser's native validation prevents form submission and displays error messages when required fields are empty, eliminating the need for additional JavaScript validation before GraphQL mutations.

Applied to files:

  • frontend/src/app/mentorship/programs/[programKey]/page.tsx
🧬 Code graph analysis (1)
frontend/src/app/mentorship/programs/[programKey]/page.tsx (1)
frontend/src/app/global-error.tsx (1)
  • ErrorDisplay (28-52)
🔇 Additional comments (7)
frontend/src/app/mentorship/programs/[programKey]/page.tsx (7)

20-22: LGTM!

The error field is correctly extracted from the query result to enable error handling for unpublished or unavailable programs.


34-35: State flags enable proper loading/error UX.

The hasError and hasAttemptedLoad flags correctly track whether data loading has been attempted and whether an error occurred, ensuring the UI doesn't prematurely show an error state before the first load attempt completes.


53-54: LGTM!

The explicit early return after handling the refresh prevents further processing during the refresh cycle, which is the correct control flow.


56-64: LGTM!

The error handling correctly processes GraphQL errors by clearing program data and setting error flags. The guard if (!hasError) prevents redundant state updates when the error state is already established.


66-78: LGTM! Backend null response correctly handled.

The logic correctly implements the PR objective:

  • Lines 66-72: Loads program data only when necessary (initial load or programKey change)
  • Lines 73-78: Treats backend returning null for getProgram as a "not found" error, which correctly handles unpublished programs being inaccessible

95-97: LGTM!

The loading condition correctly ensures the spinner displays until at least one data load attempt has completed, preventing a premature error state from being shown.


99-107: LGTM! Error messaging aligns with PR objectives.

The error display condition and message correctly handle both error states and missing program data, effectively treating unpublished programs as "not available" with a 404 response, which aligns with the PR objective of hiding unpublished programs from public view.

Copy link
Collaborator

@kasya kasya left a comment

Choose a reason for hiding this comment

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

hi @mrkeshav-05 ! Left a request ⬇️

Comment on lines +31 to +39
if program.status != Program.ProgramStatus.PUBLISHED:
msg = f"Program with key '{program_key}' not found."
logger.warning(
"Attempted public access to unpublished program '%s' (status: %s)",
program_key,
program.status,
)
raise ObjectDoesNotExist(msg)

Copy link
Collaborator

Choose a reason for hiding this comment

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

@mrkeshav-05 programs and modules should still be available from admin view (/my/mentorship/) even if they are not published. Otherwise, how would mentors edit them? 🤔
Right now I'm not able to access any unpublished program in my admin view:

Image

We only need to hide these from public view - /mentorship path. Same for modules.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also please don't forget to run make check-test before pushing changes 👌🏼

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hide Unpublished Programs and Modules From Public View

2 participants