Course Module Navigation

In Progress Generated: 2026-07-20 Module: Course

Overview

What

Adds a navigation block to the module-detail API response (GET /api/v1/courses/{courseId}/modules/{moduleId}) carrying the previous and next reachable activity in whole-course order. Each link exposes the module's id, name, and modname so the frontend can render prev/next controls.

When

Runs synchronously during every module-detail request. The ModuleNavigationService is invoked after the module is resolved and authorized, walking outward from the current module to find the nearest visible and available neighbours. Best-effort: failures yield null links, never a 500.

Why

The PM requested the "previous / next" component on course module pages (matching Moodle's activity navigation). The endpoint previously returned no navigation data, leaving the frontend with nothing to render the control from. This is a backend-only change enabling the frontend team to build the UI.

📁 Where

Lives in app/Modules/Course/. New files: DTOs/ModuleNavLinkDTO, DTOs/ModuleNavigationDTO, Services/ModuleNavigationService. Modified: Controllers/ModuleController, Resources/ModuleDetailResource. Reuses the shared CourseModinfoService and Availability module.

🛠 How

The controller calls ModuleNavigationService::resolve(), which fetches the cached whole-course structure via CourseModinfoService, locates the current module's index, then walks outward in both directions (-1 for previous, +1 for next). Each candidate is tested for reachability: visible, not deleted, not a skip-type (label, attendance), and passes the AvailabilityService evaluation. Course-wide completion/section maps are pre-computed once and shared across all evaluations so cm=-1 resolution works correctly across section boundaries. The result is attached to ModuleDetailResource via withNavigation() and serialized as a navigation JSON block.

Sequence Diagram

sequenceDiagram
    participant B as Browser
    participant C as ModuleController
    participant R as ShowModuleRequest
    participant MS as ModuleService
    participant NS as ModuleNavigationService
    participant MI as CourseModinfoService
    participant AS as AvailabilityService
    participant DB as Database
    participant RES as ModuleDetailResource

    B->>C: GET /api/v1/courses/{courseId}/modules/{moduleId}
    C->>R: validate & authorize
    R-->>C: ShowModuleDTO

    C->>MS: show(dto)
    MS->>DB: query course_modules + relations
    DB-->>MS: MoodleCourseModule
    MS-->>C: module

    C->>NS: resolve(courseId, cmId, studentId)
    NS->>MI: get(courseId)
    MI->>DB: query course_sections, course_modules (cached)
    DB-->>MI: CourseModinfoDTO
    MI-->>NS: modinfo

    NS->>DB: query course_modules_completion (bulk)
    DB-->>NS: completionMap

    Note over NS: Walk backward from current index
    loop Find previous
        NS->>AS: evaluate(availability, context)
        AS-->>NS: AvailabilityResult
    end

    Note over NS: Walk forward from current index
    loop Find next
        NS->>AS: evaluate(availability, context)
        AS-->>NS: AvailabilityResult
    end

    NS-->>C: ModuleNavigationDTO(previous, next)

    C->>RES: new ModuleDetailResource(module)
    C->>RES: withNavigation(navigation)
    RES-->>C: JSON response with navigation block
    C-->>B: 200 { data: { ..., navigation: { previous, next } } }
      

Flowchart

flowchart TD
    A[GET /courses/courseId/modules/moduleId] --> B{FormRequest valid?}
    B -- No --> B1[422 Validation Error]
    B -- Yes --> C[ModuleService::show]
    C --> D{Module found?}
    D -- No --> D1[404 Not Found]
    D -- Yes --> E{Module available?}
    E -- No --> E1[423 Locked]
    E -- Yes --> F[Dispatch ModuleViewed event]
    F --> G[Get completion record]
    G --> H[ModuleNavigationService::resolve]
    H --> I[Fetch modinfo from cache]
    I --> J{Current module in list?}
    J -- No --> K[Return empty navigation]
    J -- Yes --> L[Build shared context maps]
    L --> M[Walk backward for previous]
    M --> N{Candidate reachable?}
    N -- "deleted / hidden / label" --> M
    N -- "availability locked" --> M
    N -- "boundary reached" --> O[previous = null]
    N -- Yes --> P[previous = NavLinkDTO]
    O --> Q[Walk forward for next]
    P --> Q
    Q --> R{Candidate reachable?}
    R -- "deleted / hidden / label" --> Q
    R -- "availability locked" --> Q
    R -- "boundary reached" --> S[next = null]
    R -- Yes --> T[next = NavLinkDTO]
    S --> U[Build ModuleDetailResource]
    T --> U
    K --> U
    U --> V[Attach withNavigation + withFilters]
    V --> W[200 JSON Response]

    style B1 fill:#ef4444,color:#fff
    style D1 fill:#ef4444,color:#fff
    style E1 fill:#f59e0b,color:#fff
    style W fill:#10b981,color:#fff
    style K fill:#64748b,color:#fff
      

Files Changed

File PathLayerDescription
app/Modules/Course/DTOs/ModuleNavLinkDTO.php DTO New — final readonly DTO carrying a single nav link (id, name, modname)
app/Modules/Course/DTOs/ModuleNavigationDTO.php DTO New — carries previous + next (nullable) nav links; has empty() factory
app/Modules/Course/Services/ModuleNavigationService.php Service New — resolves nearest reachable neighbours via outward walk, builds shared availability context, evaluates visibility + availability per candidate
app/Modules/Course/Resources/ModuleDetailResource.php Resource Modified — added withNavigation() method and navigation block in toArray()
app/Modules/Course/Controllers/ModuleController.php Controller Modified — injects ModuleNavigationService, calls resolve(), passes result via withNavigation()
tests/Feature/Course/ModuleNavigationTest.php Test New — 7 feature tests covering course order, boundaries, hidden/label skip, availability lock, cross-section navigation
docs/openapi.yaml Docs Modified — documents the navigation block in the module response schema
docs/postman_collection.json Docs Modified — updated example response and test script to include navigation

Rules Applied