Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved authorization, filtering, category-ladder, and test setup issues remain.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds centralized mentoring-report access control for students, TGIs, mentors, permissions, and active training places.
Changes:
- Adds report visibility and category-ladder logic.
- Updates mentoring policies and history filtering.
- Adds report permissions and unit/feature coverage.
File summaries
| File | Summary |
|---|---|
tests/Unit/Training/Mentoring/MentoringReportAccessServiceTest.php |
Tests access-service rules. |
tests/Unit/Training/Mentoring/MentoringPolicyTest.php |
Tests policy authorization. |
tests/Feature/TrainingPanel/Mentor/ViewMentoringReportTest.php |
Tests report-page access. |
tests/Feature/TrainingPanel/Mentor/MentoringHistoryTest.php |
Tests filtered history visibility. |
database/seeders/RolesAndPermissionsSeeder.php |
Registers report permissions. |
app/Services/Training/MentoringReportAccessService.php |
Implements report visibility and scoped queries. |
app/Policies/Training/Mentoring/MentoringPolicy.php |
Delegates mentoring authorization. |
app/Filament/Training/Pages/Mentor/MentoringHistory.php |
Applies visibility filtering to report history. |
Review details
Suppressed comments (5)
app/Services/Training/MentoringReportAccessService.php:232
- The view-all early return bypasses the same validity checks used by
canViewReport(). A filed session with an unknown position will therefore appear in the history table for a view-all user, but its visible View Report action then leads to a 403 becausecanViewReport()rejects that position. Apply the recognized-category/student constraints before returning the view-all query, or centralize a shared valid-session scope.
if ($this->canViewAll($user)) {
return Session::query();
tests/Unit/Training/Mentoring/MentoringPolicyTest.php:166
- Once the role fixture is present, this TGI case still cannot reach the ladder check: ATC category resolution is built from
TrainingPositionrows, but the test only creates a session withEGLL_TWR. Without an S2 training position,resolveCategoryForCtsCallsign()returnsnulland the policy denies access. Seed the matching training position before creating the session.
$s2Session = Session::factory()->create([
'position' => 'EGLL_TWR',
'filed' => now(),
]);
tests/Unit/Training/Mentoring/MentoringPolicyTest.php:180
- This new view-all test creates an
EGLL_APPsession but noTrainingPositionrow. The access service resolves the session category before checking the view-all permission, so it returns false for the unregistered callsign and the test fails. Seed the matching S3 training position (as the mentor tests do) before invoking the policy.
$session = Session::factory()->create([
'position' => 'EGLL_APP',
'filed' => now(),
]);
tests/Unit/Training/Mentoring/MentoringReportAccessServiceTest.php:555
- This helper stores only the session callsign; it does not create a
TrainingPosition. Category resolution for ATC callsigns is derived fromTrainingPositionrows, so the new TGI and view-all cases using this helper resolvenulland fail before exercising their access rules (the default test seeder does not add training positions). Seed the corresponding position in each relevant test while leaving the unknown-position case unseeded.
return Session::factory()->create([
'student_id' => $studentMember->id,
'position' => $this->callsignFor($category),
'filed' => now(),
]);
tests/Unit/Training/Mentoring/MentoringReportAccessServiceTest.php:130
- The first
canViewReport()call builds MentorPermissionService's cached callsign map while noTrainingPositionforEGLL_APPexists. Creating the student's place afterward cannot update that same cached map, so the second assertion remains false. Seed the S3 training position before the first assertion while leaving the place absent, or recreate/reset the service between assertions.
$this->assertFalse($this->service->canViewReport($mentorAccount, $session));
- Files reviewed: 8/8 changed files
- Comments generated: 12
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved authorization gaps and query performance/filtering issues remain.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
app/Filament/Training/Pages/Mentor/MentoringHistory.php:122
- The previous history queries eager-loaded
studentandmentor, but this replacement returns aSessionquery without either relation.BaseMentoringHistoryPagedereferences both relations for every table row, so this introduces N+1 CTS queries on each history page; eager-load them on the query returned here (and any other caller that renders these rows).
$query = $accessService->visibleSessionsQueryFor(auth()->user())
->where('taken_date', '<', now());
app/Services/Training/MentoringReportAccessService.php:264
visibleSessionsQueryFor()returns a freshSessionbuilder from the union without eager-loading the relations used byBaseMentoringHistoryPage(student,mentor, andcancelReason). Rendering a page of 100 rows will therefore issue per-row CTS queries for these values, whereas the previous repository queries eager-loaded at least the student and mentor relations. Add eager loading to the returned builder.
return Session::query()
->fromSub($union, 'sessions')
->select('sessions.*');
app/Services/Training/MentoringReportAccessService.php:328
- This loads every active training place and its trainable record, then resolves CTS members for every place holder, on each mentoring-history query. The result is only needed for students in the sessions being listed, and
activePlaceGroups()is invoked again for the conducted-session arm, so the cost grows with the entire training-place population rather than the current user's history. Scope this to relevant student accounts (or express it as joins/subqueries) and reuse the computed groups within the request.
return TrainingPlace::query()
->whereNull('deleted_at')
->with('trainable')
->get()
- Files reviewed: 9/9 changed files
- Comments generated: 3
- Review effort level: Lite
Summary of changes
New rules on who can access or see a mentoring report:
training.mentoring.view.*ortraining.mentoring.reports.view-allsee everything.Screenshots (if necessary)