Review, publish, preview and undo changes through the API
Revisions now call publishing only what publishes: a project's status is whether the work is active, and a service is published by its flag. The fake identity server answers by token, so a test can sign in a second person. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
187
app/Http/Controllers/Api/V1/ReviewController.php
Normal file
187
app/Http/Controllers/Api/V1/ReviewController.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Api\V1\Concerns\ResolvesContent;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ReviewCommentResource;
|
||||
use App\Models\User;
|
||||
use App\Support\AdminContent;
|
||||
use App\Support\ContentTypes;
|
||||
use App\Support\Publisher;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* The handover between writing something and putting it on the site — the
|
||||
* panel's review buttons, as calls. Pages, projects and services only: the
|
||||
* kinds with a separate `publish` permission.
|
||||
*/
|
||||
class ReviewController extends Controller
|
||||
{
|
||||
use ResolvesContent;
|
||||
|
||||
/**
|
||||
* What is waiting for the caller: everything awaiting review they could
|
||||
* answer, either addressed to them or to nobody in particular.
|
||||
*/
|
||||
public function queue(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$items = collect();
|
||||
|
||||
foreach (array_keys(AdminContent::REVIEWABLE) as $resource) {
|
||||
$model = AdminContent::model($resource);
|
||||
|
||||
if ($user->cannot("{$resource}.publish")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$model::query()
|
||||
->awaitingReviewBy($user)
|
||||
->with(['reviewRequester', 'reviewer'])
|
||||
->get()
|
||||
->each(fn (Model $record) => $items->push($this->summary($resource, $record)));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => $items->sortBy('requested_at')->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The thread: requests, returns, publications and comments, newest first.
|
||||
*/
|
||||
public function thread(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$record = $this->record($request);
|
||||
|
||||
$this->authorize('view', $record);
|
||||
|
||||
return ReviewCommentResource::collection(
|
||||
$record->reviewComments()->with('user')->paginate(perPage: min($request->integer('per_page', 25), 100)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for it to be reviewed — the only way somebody without the publish
|
||||
* permission has of putting their work live.
|
||||
*/
|
||||
public function request(Request $request): JsonResponse
|
||||
{
|
||||
$record = $this->record($request);
|
||||
|
||||
$this->authorize('update', $record);
|
||||
|
||||
$data = $request->validate([
|
||||
'reviewer_id' => ['sometimes', 'nullable', 'integer', 'exists:users,id'],
|
||||
'note' => ['sometimes', 'nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
if ($record->isAwaitingReview() || $record->isPublishedNow()) {
|
||||
throw ValidationException::withMessages(['review' => [__('rules.reviews.not_requestable')]]);
|
||||
}
|
||||
|
||||
$reviewer = isset($data['reviewer_id']) ? User::find($data['reviewer_id']) : null;
|
||||
|
||||
if ($reviewer !== null && $reviewer->cannot($record->getTable().'.publish')) {
|
||||
throw ValidationException::withMessages(['reviewer_id' => [__('rules.reviews.not_a_reviewer')]]);
|
||||
}
|
||||
|
||||
$record->requestReview($reviewer, $data['note'] ?? null, $request->user());
|
||||
|
||||
return $this->answer($record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send it back to whoever asked, saying why.
|
||||
*/
|
||||
public function return(Request $request): JsonResponse
|
||||
{
|
||||
$record = $this->record($request);
|
||||
|
||||
$this->authorize($record->getTable().'.publish');
|
||||
|
||||
$data = $request->validate(['note' => ['required', 'string', 'max:2000']]);
|
||||
|
||||
if (! $record->isAwaitingReview()) {
|
||||
throw ValidationException::withMessages(['review' => [__('rules.reviews.not_awaiting')]]);
|
||||
}
|
||||
|
||||
$record->returnForChanges($data['note'], $request->user());
|
||||
|
||||
return $this->answer($record);
|
||||
}
|
||||
|
||||
public function comment(Request $request): JsonResponse
|
||||
{
|
||||
$record = $this->record($request);
|
||||
|
||||
$this->authorize('view', $record);
|
||||
|
||||
$data = $request->validate(['body' => ['required', 'string', 'max:2000']]);
|
||||
|
||||
return ReviewCommentResource::make($record->comment($data['body'], $request->user())->load('user'))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve: on the site now, or from `at`. Either way the review, if there
|
||||
* was one, is answered.
|
||||
*/
|
||||
public function publish(Request $request): JsonResponse
|
||||
{
|
||||
$record = $this->record($request);
|
||||
|
||||
$this->authorize($record->getTable().'.publish');
|
||||
|
||||
$data = $request->validate(['at' => ['sometimes', 'nullable', 'date']]);
|
||||
|
||||
Publisher::publish($record, filled($data['at'] ?? null) ? CarbonImmutable::parse($data['at']) : null);
|
||||
|
||||
return $this->answer($record);
|
||||
}
|
||||
|
||||
public function unpublish(Request $request): JsonResponse
|
||||
{
|
||||
$record = $this->record($request);
|
||||
|
||||
$this->authorize($record->getTable().'.publish');
|
||||
|
||||
Publisher::unpublish($record);
|
||||
|
||||
return $this->answer($record);
|
||||
}
|
||||
|
||||
protected function answer(Model $record): JsonResponse
|
||||
{
|
||||
$record->refresh()->load(['reviewRequester', 'reviewer']);
|
||||
|
||||
return response()->json(['data' => $this->summary(AdminContent::nameOf($record), $record)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function summary(string $resource, Model $record): array
|
||||
{
|
||||
return [
|
||||
'type' => $resource,
|
||||
'id' => $record->getKey(),
|
||||
'title' => ContentTypes::title($record),
|
||||
'published' => $record->isPublishedNow(),
|
||||
'published_at' => $record->published_at?->toIso8601String(),
|
||||
'awaiting_review' => $record->isAwaitingReview(),
|
||||
'requested_at' => $record->review_requested_at?->toIso8601String(),
|
||||
'requested_by' => $record->reviewRequester ? ['id' => $record->reviewRequester->id, 'name' => $record->reviewRequester->name] : null,
|
||||
'reviewer' => $record->reviewer ? ['id' => $record->reviewer->id, 'name' => $record->reviewer->name] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user