feat: report reviews etc ...
This commit is contained in:
parent
49419c1e7b
commit
4af28f3828
|
|
@ -6,6 +6,7 @@ use App\Enums\ModerationCaseStatus;
|
|||
use App\Enums\ReportStatus;
|
||||
use App\Models\MealPosts;
|
||||
use App\Models\ModerationCase;
|
||||
use App\Models\PostReviews;
|
||||
use App\Models\User;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\EditAction;
|
||||
|
|
@ -36,6 +37,7 @@ class ModerationCasesTable
|
|||
return $query->where(function (Builder $query) use ($search): void {
|
||||
$query
|
||||
->whereHasMorph('caseable', [MealPosts::class], fn (Builder $query): Builder => $query->where('title', 'like', "%{$search}%"))
|
||||
->orWhereHasMorph('caseable', [PostReviews::class], fn (Builder $query): Builder => $query->where('comment', 'like', "%{$search}%"))
|
||||
->orWhereHasMorph('caseable', [User::class], fn (Builder $query): Builder => $query->where('name', 'like', "%{$search}%"));
|
||||
});
|
||||
}),
|
||||
|
|
@ -80,6 +82,7 @@ class ModerationCasesTable
|
|||
->label(__('admin.moderation_cases.filters.target_type'))
|
||||
->options([
|
||||
MealPosts::class => __('admin.moderation_cases.targets.meal_post'),
|
||||
PostReviews::class => __('admin.moderation_cases.targets.post_review'),
|
||||
User::class => __('admin.moderation_cases.targets.user'),
|
||||
]),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -58,6 +58,22 @@ class PostReviewsController extends Controller
|
|||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function mine(Request $request, MealPosts $mealPost): JsonResponse|PostReviewsResource
|
||||
{
|
||||
$this->abortIfUnavailable($mealPost);
|
||||
|
||||
$postReview = $mealPost->reviews()
|
||||
->where('user_id', $request->user()->getKey())
|
||||
->with('user:id,name,avatar_url')
|
||||
->first();
|
||||
|
||||
if (! $postReview) {
|
||||
return response()->json(['data' => null]);
|
||||
}
|
||||
|
||||
return new PostReviewsResource($postReview);
|
||||
}
|
||||
|
||||
public function show(PostReviews $postReview): PostReviewsResource
|
||||
{
|
||||
$postReview->loadMissing('mealPost.user');
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use App\Enums\ReportReason;
|
|||
use App\Http\Requests\StoreReportRequest;
|
||||
use App\Http\Resources\ReportResource;
|
||||
use App\Models\MealPosts;
|
||||
use App\Models\PostReviews;
|
||||
use App\Models\User;
|
||||
use App\Services\ModerationReportService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
|
@ -47,4 +48,22 @@ class ReportController extends Controller
|
|||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function storePostReview(
|
||||
StoreReportRequest $request,
|
||||
PostReviews $postReview,
|
||||
ModerationReportService $reports,
|
||||
): JsonResponse {
|
||||
$report = $reports->report(
|
||||
reportable: $postReview,
|
||||
reporter: $request->user(),
|
||||
reason: ReportReason::from($request->validated('reason')),
|
||||
details: $request->validated('details'),
|
||||
);
|
||||
|
||||
return (new ReportResource($report))
|
||||
->additional(['message' => __('api.reports.created')])
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\MealPosts;
|
||||
use App\Models\PostReviews;
|
||||
use App\Models\Report;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -31,6 +32,7 @@ class ReportResource extends JsonResource
|
|||
{
|
||||
return match ($this->reportable_type) {
|
||||
MealPosts::class => 'meal_post',
|
||||
PostReviews::class => 'post_review',
|
||||
User::class => 'user',
|
||||
default => 'unknown',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class ModerationCase extends Model
|
|||
{
|
||||
return match ($this->caseable_type) {
|
||||
MealPosts::class => 'meal_post',
|
||||
PostReviews::class => 'post_review',
|
||||
User::class => 'user',
|
||||
default => 'unknown',
|
||||
};
|
||||
|
|
@ -61,6 +62,7 @@ class ModerationCase extends Model
|
|||
{
|
||||
return match ($this->caseable_type) {
|
||||
MealPosts::class => __('admin.moderation_cases.targets.meal_post'),
|
||||
PostReviews::class => __('admin.moderation_cases.targets.post_review'),
|
||||
User::class => __('admin.moderation_cases.targets.user'),
|
||||
default => __('admin.moderation_cases.targets.unknown'),
|
||||
};
|
||||
|
|
@ -72,6 +74,9 @@ class ModerationCase extends Model
|
|||
|
||||
return match (true) {
|
||||
$target instanceof MealPosts => $target->title,
|
||||
$target instanceof PostReviews => filled($target->comment)
|
||||
? str($target->comment)->limit(80)->toString()
|
||||
: __('admin.moderation_cases.targets.post_review'),
|
||||
$target instanceof User => $target->name,
|
||||
default => (string) $this->caseable_id,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ namespace App\Models;
|
|||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
|
||||
class PostReviews extends Model
|
||||
{
|
||||
|
|
@ -27,6 +29,16 @@ class PostReviews extends Model
|
|||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function reports(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Report::class, 'reportable');
|
||||
}
|
||||
|
||||
public function moderationCase(): MorphOne
|
||||
{
|
||||
return $this->morphOne(ModerationCase::class, 'caseable');
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use App\Enums\ReportStatus;
|
|||
use App\Enums\UserRole;
|
||||
use App\Models\MealPosts;
|
||||
use App\Models\ModerationCase;
|
||||
use App\Models\PostReviews;
|
||||
use App\Models\Report;
|
||||
use App\Models\User;
|
||||
use Filament\Notifications\Notification as FilamentNotification;
|
||||
|
|
@ -135,6 +136,12 @@ class ModerationReportService
|
|||
422,
|
||||
__('api.reports.cannot_report_own_meal'),
|
||||
);
|
||||
|
||||
abort_if(
|
||||
$reportable instanceof PostReviews && $reportable->user_id === $reporter->getKey(),
|
||||
422,
|
||||
__('api.reports.cannot_report_own_review'),
|
||||
);
|
||||
}
|
||||
|
||||
private function isDuplicateReportException(QueryException $exception): bool
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\MealPosts;
|
||||
use App\Models\PostReviews;
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
|
@ -8,6 +9,7 @@ return [
|
|||
|
||||
'report_thresholds' => [
|
||||
MealPosts::class => 3,
|
||||
PostReviews::class => 3,
|
||||
User::class => 5,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ return [
|
|||
],
|
||||
'targets' => [
|
||||
'meal_post' => 'Meal',
|
||||
'post_review' => 'Review',
|
||||
'user' => 'User',
|
||||
'unknown' => 'Unknown',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -41,8 +41,10 @@ return [
|
|||
'already_exists' => 'You have already reported this content.',
|
||||
'cannot_report_self' => 'You cannot report your own profile.',
|
||||
'cannot_report_own_meal' => 'You cannot report your own meal.',
|
||||
'cannot_report_own_review' => 'You cannot report your own review.',
|
||||
'targets' => [
|
||||
'meal_post' => 'a meal',
|
||||
'post_review' => 'a review',
|
||||
'user' => 'a user',
|
||||
'unknown' => 'content',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ return [
|
|||
],
|
||||
'targets' => [
|
||||
'meal_post' => 'Plat',
|
||||
'post_review' => 'Avis',
|
||||
'user' => 'Utilisateur',
|
||||
'unknown' => 'Inconnu',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -41,8 +41,10 @@ return [
|
|||
'already_exists' => 'Vous avez déjà signalé ce contenu.',
|
||||
'cannot_report_self' => 'Vous ne pouvez pas signaler votre propre profil.',
|
||||
'cannot_report_own_meal' => 'Vous ne pouvez pas signaler votre propre plat.',
|
||||
'cannot_report_own_review' => 'Vous ne pouvez pas signaler votre propre avis.',
|
||||
'targets' => [
|
||||
'meal_post' => 'un plat',
|
||||
'post_review' => 'un avis',
|
||||
'user' => 'un utilisateur',
|
||||
'unknown' => 'un contenu',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -76,12 +76,16 @@ Route::middleware(['auth:sanctum', 'verified', 'not_suspended'])->group(function
|
|||
Route::post('meal-posts/{mealPost}/reports', [ReportController::class, 'storeMealPost'])
|
||||
->middleware('throttle:reports')
|
||||
->name('meal-posts.reports.store');
|
||||
Route::get('meal-posts/{mealPost}/my-review', [PostReviewsController::class, 'mine'])->name('meal-posts.reviews.mine');
|
||||
Route::get('meal-posts/{mealPost}/reviews', [PostReviewsController::class, 'index'])->name('meal-posts.reviews.index');
|
||||
Route::post('meal-posts/{mealPost}/reviews', [PostReviewsController::class, 'store'])->middleware('throttle:content-write')->name('meal-posts.reviews.store');
|
||||
Route::get('post-reviews/{postReview}', [PostReviewsController::class, 'show'])->name('post-reviews.show');
|
||||
Route::put('post-reviews/{postReview}', [PostReviewsController::class, 'update'])->middleware('throttle:content-write')->name('post-reviews.update');
|
||||
Route::patch('post-reviews/{postReview}', [PostReviewsController::class, 'update'])->middleware('throttle:content-write')->name('post-reviews.patch');
|
||||
Route::delete('post-reviews/{postReview}', [PostReviewsController::class, 'destroy'])->middleware('throttle:content-write')->name('post-reviews.destroy');
|
||||
Route::post('post-reviews/{postReview}/reports', [ReportController::class, 'storePostReview'])
|
||||
->middleware('throttle:reports')
|
||||
->name('post-reviews.reports.store');
|
||||
Route::apiResource('meal-posts', MealPostController::class)
|
||||
->except(['update'])
|
||||
->middlewareFor(['store', 'destroy'], 'throttle:content-write');
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use App\Enums\ReportReason;
|
|||
use App\Enums\UserRole;
|
||||
use App\Models\MealPosts;
|
||||
use App\Models\ModerationCase;
|
||||
use App\Models\PostReviews;
|
||||
use App\Models\Report;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
|
@ -96,6 +97,40 @@ it('reports users and prevents reporting own profile', function () {
|
|||
->assertJsonPath('message', __('api.reports.cannot_report_self'));
|
||||
});
|
||||
|
||||
it('reports post reviews and prevents reporting own review', function () {
|
||||
$reviewAuthor = User::factory()->create();
|
||||
$reporter = User::factory()->create();
|
||||
$review = PostReviews::query()->create([
|
||||
'meal_post_id' => MealPosts::factory()->create()->id,
|
||||
'user_id' => $reviewAuthor->id,
|
||||
'rating' => 2,
|
||||
'comment' => 'Commentaire problématique.',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($reporter);
|
||||
|
||||
$this->postJson("/api/post-reviews/{$review->id}/reports", [
|
||||
'reason' => ReportReason::INAPPROPRIATE->value,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.reportableType', 'post_review')
|
||||
->assertJsonPath('data.reportableId', $review->id);
|
||||
|
||||
$this->assertDatabaseHas('moderation_cases', [
|
||||
'caseable_type' => PostReviews::class,
|
||||
'caseable_id' => $review->id,
|
||||
'threshold' => 3,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($reviewAuthor);
|
||||
|
||||
$this->postJson("/api/post-reviews/{$review->id}/reports", [
|
||||
'reason' => ReportReason::OTHER->value,
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonPath('message', __('api.reports.cannot_report_own_review'));
|
||||
});
|
||||
|
||||
it('notifies moderators once when a report threshold is reached', function () {
|
||||
$admin = User::factory()->create(['role' => UserRole::ADMIN]);
|
||||
$moderator = User::factory()->create(['role' => UserRole::MODERATOR]);
|
||||
|
|
|
|||
|
|
@ -156,6 +156,41 @@ it('lists reviews for a single meal post', function () {
|
|||
->assertJsonMissing(['id' => $otherReview->id]);
|
||||
});
|
||||
|
||||
it('shows the authenticated users review for a meal post', function () {
|
||||
$user = User::factory()->create();
|
||||
$mealPost = MealPosts::factory()->create();
|
||||
$review = PostReviews::query()->create([
|
||||
'meal_post_id' => $mealPost->id,
|
||||
'user_id' => $user->id,
|
||||
'rating' => 4,
|
||||
'comment' => 'Mon avis.',
|
||||
]);
|
||||
PostReviews::query()->create([
|
||||
'meal_post_id' => $mealPost->id,
|
||||
'user_id' => User::factory()->create()->id,
|
||||
'rating' => 5,
|
||||
'comment' => 'Un autre avis.',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$this->getJson("/api/meal-posts/{$mealPost->id}/my-review")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.id', $review->id)
|
||||
->assertJsonPath('data.user_id', $user->id);
|
||||
});
|
||||
|
||||
it('returns null when the authenticated user has not reviewed a meal post', function () {
|
||||
$user = User::factory()->create();
|
||||
$mealPost = MealPosts::factory()->create();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$this->getJson("/api/meal-posts/{$mealPost->id}/my-review")
|
||||
->assertOk()
|
||||
->assertJsonPath('data', null);
|
||||
});
|
||||
|
||||
it('lets the review author update and delete their review', function () {
|
||||
$user = User::factory()->create();
|
||||
$review = PostReviews::query()->create([
|
||||
|
|
|
|||
Loading…
Reference in New Issue