From a22afcb8daf27b94e40cff2bc5edda9cf2683f95 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Thu, 5 Feb 2026 10:53:21 +0100 Subject: [PATCH] Update release reporting --- .../Admin/AdminReleaseReportController.php | 29 ++- app/Http/Controllers/DetailsController.php | 2 +- app/Services/MovieBrowseService.php | 2 +- .../Releases/ReleaseBrowseService.php | 2 +- .../Releases/ReleaseSearchService.php | 2 +- resources/js/csp-safe.js | 53 +++++- .../admin/release-reports/index.blade.php | 53 ++++++ routes/web.php | 1 + .../AdminReleaseReportControllerTest.php | 170 ++++++++++++++++++ 9 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 tests/Feature/AdminReleaseReportControllerTest.php diff --git a/app/Http/Controllers/Admin/AdminReleaseReportController.php b/app/Http/Controllers/Admin/AdminReleaseReportController.php index b3e49a1d6..50d8f7914 100644 --- a/app/Http/Controllers/Admin/AdminReleaseReportController.php +++ b/app/Http/Controllers/Admin/AdminReleaseReportController.php @@ -96,13 +96,33 @@ class AdminReleaseReportController extends BasePageController return redirect()->back()->with('success', 'Report dismissed successfully.'); } + /** + * Revert a resolved or dismissed report back to reviewed status. + */ + public function revert(int $id): RedirectResponse + { + $report = ReleaseReport::findOrFail($id); + + if (! in_array($report->status, ['resolved', 'dismissed'])) { + return redirect()->back()->with('error', 'Only resolved or dismissed reports can be reverted.'); + } + + $report->update([ + 'status' => 'reviewed', + 'reviewed_by' => Auth::id(), + 'reviewed_at' => now(), + ]); + + return redirect()->back()->with('success', 'Report reverted to reviewed status successfully.'); + } + /** * Bulk update report statuses. */ public function bulkAction(Request $request): RedirectResponse { $request->validate([ - 'action' => 'required|in:dismiss,resolve,reviewed,delete', + 'action' => 'required|in:dismiss,resolve,reviewed,delete,revert', 'report_ids' => 'required|array', 'report_ids.*' => 'integer', ]); @@ -145,6 +165,12 @@ class AdminReleaseReportController extends BasePageController 'reviewed_by' => Auth::id(), 'reviewed_at' => now(), ]); + } elseif ($action === 'revert' && in_array($report->status, ['resolved', 'dismissed'])) { + $report->update([ + 'status' => 'reviewed', + 'reviewed_by' => Auth::id(), + 'reviewed_at' => now(), + ]); } $count++; @@ -155,6 +181,7 @@ class AdminReleaseReportController extends BasePageController 'dismiss' => 'dismissed', 'resolve' => 'resolved', 'reviewed' => 'marked as reviewed', + 'revert' => 'reverted to reviewed', default => 'processed', }; diff --git a/app/Http/Controllers/DetailsController.php b/app/Http/Controllers/DetailsController.php index 8ef2edcb3..506095b12 100644 --- a/app/Http/Controllers/DetailsController.php +++ b/app/Http/Controllers/DetailsController.php @@ -76,7 +76,7 @@ class DetailsController extends BasePageController $similars = $this->releaseSearchService->searchSimilar($data['id'], $data['searchname'], $this->userdata->categoryexclusions); $failed = DnzbFailure::getFailedCount($data['id']); $reportData = ReleaseReport::where('releases_id', $data['id']) - ->whereIn('status', ['pending', 'reviewed']) + ->whereIn('status', ['pending', 'reviewed', 'resolved']) ->get(); $reportCount = $reportData->count(); $reportReasons = ReleaseReport::reasonKeysToLabels($reportData->pluck('reason')->unique()->implode(', ')); diff --git a/app/Services/MovieBrowseService.php b/app/Services/MovieBrowseService.php index f267d97c8..efcabf1cf 100644 --- a/app/Services/MovieBrowseService.php +++ b/app/Services/MovieBrowseService.php @@ -84,7 +84,7 @@ class MovieBrowseService .'LEFT OUTER JOIN usenet_groups g ON g.id = r.groups_id ' .'LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id ' .'LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id ' - .'LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS report_count, GROUP_CONCAT(DISTINCT reason SEPARATOR \', \') AS report_reasons FROM release_reports WHERE status IN (\'pending\', \'reviewed\') GROUP BY releases_id) rr ON rr.releases_id = r.id ' + .'LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS report_count, GROUP_CONCAT(DISTINCT reason SEPARATOR \', \') AS report_reasons FROM release_reports WHERE status IN (\'pending\', \'reviewed\', \'resolved\') GROUP BY releases_id) rr ON rr.releases_id = r.id ' .'LEFT OUTER JOIN categories c ON c.id = r.categories_id ' .'LEFT OUTER JOIN root_categories cp ON cp.id = c.root_categories_id ' .'INNER JOIN movieinfo m ON m.imdbid = r.imdbid ' diff --git a/app/Services/Releases/ReleaseBrowseService.php b/app/Services/Releases/ReleaseBrowseService.php index 5b76cc6c5..d57f5ecf4 100644 --- a/app/Services/Releases/ReleaseBrowseService.php +++ b/app/Services/Releases/ReleaseBrowseService.php @@ -87,7 +87,7 @@ class ReleaseBrowseService LEFT OUTER JOIN video_data re ON re.releases_id = r.id LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id - LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS report_count, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS report_reasons FROM release_reports WHERE status IN ('pending', 'reviewed') GROUP BY releases_id) rr ON rr.releases_id = r.id + LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS report_count, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS report_reasons FROM release_reports WHERE status IN ('pending', 'reviewed', 'resolved') GROUP BY releases_id) rr ON rr.releases_id = r.id GROUP BY r.id ORDER BY %8\$s %9\$s", $this->showPasswords(), diff --git a/app/Services/Releases/ReleaseSearchService.php b/app/Services/Releases/ReleaseSearchService.php index e1af88119..9b0b8977e 100644 --- a/app/Services/Releases/ReleaseSearchService.php +++ b/app/Services/Releases/ReleaseSearchService.php @@ -1138,7 +1138,7 @@ class ReleaseSearchService LEFT JOIN categories c ON c.id = r.categories_id LEFT JOIN root_categories cp ON cp.id = c.root_categories_id LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id - LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS report_count, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS report_reasons FROM release_reports WHERE status IN ('pending', 'reviewed') GROUP BY releases_id) rr ON rr.releases_id = r.id + LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS report_count, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS report_reasons FROM release_reports WHERE status IN ('pending', 'reviewed', 'resolved') GROUP BY releases_id) rr ON rr.releases_id = r.id %s", $whereSql ); diff --git a/resources/js/csp-safe.js b/resources/js/csp-safe.js index 4878fc7f6..9db2785b1 100644 --- a/resources/js/csp-safe.js +++ b/resources/js/csp-safe.js @@ -5906,7 +5906,7 @@ document.addEventListener('contentLoaded', initReleaseReportButtons); /** * Initialize admin release reports page functionality - * Handles description modal, select all, and bulk actions + * Handles description modal, revert confirmation modal, select all, and bulk actions */ function initAdminReleaseReports() { // Report Description Modal @@ -5918,6 +5918,57 @@ function initAdminReleaseReports() { var descCloseButtons = document.querySelectorAll('.report-desc-modal-close'); var descBackdrop = document.querySelector('.report-desc-modal-backdrop'); + // Revert Confirmation Modal + var revertModal = document.getElementById('revertConfirmModal'); + var revertButtons = document.querySelectorAll('.revert-report-btn'); + var revertForm = document.getElementById('revertConfirmForm'); + var revertStatusSpan = document.getElementById('revertReportStatus'); + var revertCloseButtons = document.querySelectorAll('.revert-modal-close'); + var revertBackdrop = document.querySelector('.revert-modal-backdrop'); + + // Open revert confirmation modal + revertButtons.forEach(function(btn) { + if (btn.hasAttribute('data-revert-initialized')) return; + btn.setAttribute('data-revert-initialized', 'true'); + + btn.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + var actionUrl = this.getAttribute('data-action-url'); + var status = this.getAttribute('data-report-status'); + + if (revertForm) revertForm.setAttribute('action', actionUrl); + if (revertStatusSpan) revertStatusSpan.textContent = status; + + if (revertModal) revertModal.classList.remove('hidden'); + }); + }); + + // Close revert modal + function closeRevertModal() { + if (revertModal) revertModal.classList.add('hidden'); + } + + revertCloseButtons.forEach(function(btn) { + btn.addEventListener('click', function(e) { + e.preventDefault(); + closeRevertModal(); + }); + }); + + if (revertBackdrop) { + revertBackdrop.addEventListener('click', closeRevertModal); + } + + // Close revert modal on Escape key + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeRevertModal(); + closeDescModal(); + } + }); + // Open description modal descButtons.forEach(function(btn) { if (btn.hasAttribute('data-desc-initialized')) return; diff --git a/resources/views/admin/release-reports/index.blade.php b/resources/views/admin/release-reports/index.blade.php index 071d4ba11..0348c3cec 100644 --- a/resources/views/admin/release-reports/index.blade.php +++ b/resources/views/admin/release-reports/index.blade.php @@ -75,6 +75,7 @@ + + {{ ucfirst($report->status) }} @else {{ ucfirst($report->status) }} @endif @@ -319,5 +331,46 @@ + + + @endsection diff --git a/routes/web.php b/routes/web.php index 9805b8224..94b30cbb7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -249,6 +249,7 @@ Route::middleware('role:Admin', '2fa')->prefix('admin')->group(function () { Route::post('release-reports/{id}/status', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'updateStatus'])->name('admin.release-reports.update-status'); Route::post('release-reports/{id}/delete-release', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'deleteRelease'])->name('admin.release-reports.delete-release'); Route::post('release-reports/{id}/dismiss', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'dismiss'])->name('admin.release-reports.dismiss'); + Route::post('release-reports/{id}/revert', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'revert'])->name('admin.release-reports.revert'); Route::post('release-reports/bulk', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'bulkAction'])->name('admin.release-reports.bulk'); Route::get('show-list', [AdminShowsController::class, 'index'])->name('admin.show-list'); diff --git a/tests/Feature/AdminReleaseReportControllerTest.php b/tests/Feature/AdminReleaseReportControllerTest.php new file mode 100644 index 000000000..59cb436ab --- /dev/null +++ b/tests/Feature/AdminReleaseReportControllerTest.php @@ -0,0 +1,170 @@ +assertFileExists($controllerPath); + + $content = file_get_contents($controllerPath); + + // Check that the controller contains the revert method + $this->assertStringContainsString('public function revert', $content); + $this->assertStringContainsString("'resolved', 'dismissed'", $content); + $this->assertStringContainsString('Report reverted to reviewed status', $content); + } + + /** + * Test that bulk action supports revert option. + */ + public function test_bulk_action_supports_revert(): void + { + $controllerPath = app_path('Http/Controllers/Admin/AdminReleaseReportController.php'); + + $this->assertFileExists($controllerPath); + + $content = file_get_contents($controllerPath); + + // Check that bulk action validation includes revert + $this->assertStringContainsString("'action' => 'required|in:dismiss,resolve,reviewed,delete,revert'", $content); + $this->assertStringContainsString("'revert' => 'reverted to reviewed'", $content); + } + + /** + * Test that the revert route is defined. + */ + public function test_revert_route_is_defined(): void + { + $routesPath = base_path('routes/web.php'); + + $this->assertFileExists($routesPath); + + $content = file_get_contents($routesPath); + + // Check that the revert route exists + $this->assertStringContainsString('release-reports/{id}/revert', $content); + $this->assertStringContainsString("'revert'])->name('admin.release-reports.revert')", $content); + } + + /** + * Test that the admin view includes revert button for resolved/dismissed reports. + */ + public function test_admin_view_has_revert_button(): void + { + $viewPath = resource_path('views/admin/release-reports/index.blade.php'); + + $this->assertFileExists($viewPath); + + $content = file_get_contents($viewPath); + + // Check that the view contains the revert button with proper data attributes + $this->assertStringContainsString('revert-report-btn', $content); + $this->assertStringContainsString('data-action-url', $content); + $this->assertStringContainsString('admin.release-reports.revert', $content); + $this->assertStringContainsString('fa-undo', $content); + $this->assertStringContainsString('Revert', $content); + $this->assertStringContainsString("in_array(\$report->status, ['resolved', 'dismissed'])", $content); + } + + /** + * Test that the admin view has revert confirmation modal. + */ + public function test_admin_view_has_revert_confirmation_modal(): void + { + $viewPath = resource_path('views/admin/release-reports/index.blade.php'); + + $this->assertFileExists($viewPath); + + $content = file_get_contents($viewPath); + + // Check that the view contains the revert confirmation modal + $this->assertStringContainsString('revertConfirmModal', $content); + $this->assertStringContainsString('revertConfirmForm', $content); + $this->assertStringContainsString('Confirm Revert', $content); + $this->assertStringContainsString('revert-modal-close', $content); + } + + /** + * Test that bulk action dropdown includes revert option. + */ + public function test_bulk_action_dropdown_has_revert_option(): void + { + $viewPath = resource_path('views/admin/release-reports/index.blade.php'); + + $this->assertFileExists($viewPath); + + $content = file_get_contents($viewPath); + + // Check that the bulk action dropdown contains revert option + $this->assertStringContainsString('', $content); + } + + /** + * Test that ReleaseBrowseService includes resolved status in report query. + */ + public function test_browse_service_includes_resolved_status(): void + { + $servicePath = app_path('Services/Releases/ReleaseBrowseService.php'); + + $this->assertFileExists($servicePath); + + $content = file_get_contents($servicePath); + + // Check that the query includes resolved status + $this->assertStringContainsString("WHERE status IN ('pending', 'reviewed', 'resolved')", $content); + } + + /** + * Test that ReleaseSearchService includes resolved status in report query. + */ + public function test_search_service_includes_resolved_status(): void + { + $servicePath = app_path('Services/Releases/ReleaseSearchService.php'); + + $this->assertFileExists($servicePath); + + $content = file_get_contents($servicePath); + + // Check that the query includes resolved status + $this->assertStringContainsString("WHERE status IN ('pending', 'reviewed', 'resolved')", $content); + } + + /** + * Test that MovieBrowseService includes resolved status in report query. + */ + public function test_movie_browse_service_includes_resolved_status(): void + { + $servicePath = app_path('Services/MovieBrowseService.php'); + + $this->assertFileExists($servicePath); + + $content = file_get_contents($servicePath); + + // Check that the query includes resolved status + $this->assertStringContainsString("WHERE status IN (\\'pending\\', \\'reviewed\\', \\'resolved\\')", $content); + } + + /** + * Test that DetailsController includes resolved status in report query. + */ + public function test_details_controller_includes_resolved_status(): void + { + $controllerPath = app_path('Http/Controllers/DetailsController.php'); + + $this->assertFileExists($controllerPath); + + $content = file_get_contents($controllerPath); + + // Check that the query includes resolved status + $this->assertStringContainsString("whereIn('status', ['pending', 'reviewed', 'resolved'])", $content); + } +}