Article Approval
This commit is contained in:
parent
8c5bccae9e
commit
3ee9e342e6
11 changed files with 234 additions and 3 deletions
17
app/Events/ArticleApproved.php
Normal file
17
app/Events/ArticleApproved.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Article;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ArticleApproved
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
public function __construct(public Article $article)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,9 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ArticlesController extends Controller
|
||||
|
|
@ -14,6 +16,22 @@ public function __invoke(Request $request): View
|
|||
->orderBy('created_at', 'desc')
|
||||
->paginate(15);
|
||||
|
||||
return view('pages.articles.index', compact('articles'));
|
||||
$publishingApprovalsEnabled = Setting::isPublishingApprovalsEnabled();
|
||||
|
||||
return view('pages.articles.index', compact('articles', 'publishingApprovalsEnabled'));
|
||||
}
|
||||
|
||||
public function approve(Article $article): RedirectResponse
|
||||
{
|
||||
$article->approve('manual');
|
||||
|
||||
return redirect()->back()->with('success', 'Article approved and queued for publishing.');
|
||||
}
|
||||
|
||||
public function reject(Article $article): RedirectResponse
|
||||
{
|
||||
$article->reject('manual');
|
||||
|
||||
return redirect()->back()->with('success', 'Article rejected.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,17 +12,20 @@ class SettingsController extends Controller
|
|||
public function index(): View
|
||||
{
|
||||
$articleProcessingEnabled = Setting::isArticleProcessingEnabled();
|
||||
$publishingApprovalsEnabled = Setting::isPublishingApprovalsEnabled();
|
||||
|
||||
return view('pages.settings.index', compact('articleProcessingEnabled'));
|
||||
return view('pages.settings.index', compact('articleProcessingEnabled', 'publishingApprovalsEnabled'));
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'article_processing_enabled' => 'boolean',
|
||||
'enable_publishing_approvals' => 'boolean',
|
||||
]);
|
||||
|
||||
Setting::setArticleProcessingEnabled($request->boolean('article_processing_enabled'));
|
||||
Setting::setPublishingApprovalsEnabled($request->boolean('enable_publishing_approvals'));
|
||||
|
||||
// If redirected from onboarding, go to dashboard
|
||||
if ($request->get('from') === 'onboarding') {
|
||||
|
|
|
|||
27
app/Listeners/PublishApprovedArticle.php
Normal file
27
app/Listeners/PublishApprovedArticle.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\ArticleApproved;
|
||||
use App\Events\ArticleReadyToPublish;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class PublishApprovedArticle implements ShouldQueue
|
||||
{
|
||||
public string $queue = 'default';
|
||||
|
||||
public function handle(ArticleApproved $event): void
|
||||
{
|
||||
$article = $event->article;
|
||||
|
||||
// Skip if already has publication (prevents duplicate processing)
|
||||
if ($article->articlePublication()->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only publish if the article is valid and approved
|
||||
if ($article->isValid() && $article->isApproved()) {
|
||||
event(new ArticleReadyToPublish($article));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Events\NewArticleFetched;
|
||||
use App\Events\ArticleReadyToPublish;
|
||||
use App\Models\Setting;
|
||||
use App\Services\Article\ValidationService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
|
|
@ -32,7 +33,17 @@ public function handle(NewArticleFetched $event): void
|
|||
return;
|
||||
}
|
||||
|
||||
event(new ArticleReadyToPublish($article));
|
||||
// Check if approval system is enabled
|
||||
if (Setting::isPublishingApprovalsEnabled()) {
|
||||
// If approvals are enabled, only proceed if article is approved
|
||||
if ($article->isApproved()) {
|
||||
event(new ArticleReadyToPublish($article));
|
||||
}
|
||||
// If not approved, article will wait for manual approval
|
||||
} else {
|
||||
// If approvals are disabled, proceed with publishing
|
||||
event(new ArticleReadyToPublish($article));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Events\ArticleApproved;
|
||||
use App\Events\NewArticleFetched;
|
||||
use Database\Factories\ArticleFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -36,6 +37,9 @@ class Article extends Model
|
|||
'description',
|
||||
'is_valid',
|
||||
'is_duplicate',
|
||||
'approval_status',
|
||||
'approved_at',
|
||||
'approved_by',
|
||||
'fetched_at',
|
||||
'validated_at',
|
||||
];
|
||||
|
|
@ -48,6 +52,8 @@ public function casts(): array
|
|||
return [
|
||||
'is_valid' => 'boolean',
|
||||
'is_duplicate' => 'boolean',
|
||||
'approval_status' => 'string',
|
||||
'approved_at' => 'datetime',
|
||||
'fetched_at' => 'datetime',
|
||||
'validated_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
|
|
@ -68,6 +74,57 @@ public function isValid(): bool
|
|||
return $this->is_valid;
|
||||
}
|
||||
|
||||
public function isApproved(): bool
|
||||
{
|
||||
return $this->approval_status === 'approved';
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->approval_status === 'pending';
|
||||
}
|
||||
|
||||
public function isRejected(): bool
|
||||
{
|
||||
return $this->approval_status === 'rejected';
|
||||
}
|
||||
|
||||
public function approve(string $approvedBy = null): void
|
||||
{
|
||||
$this->update([
|
||||
'approval_status' => 'approved',
|
||||
'approved_at' => now(),
|
||||
'approved_by' => $approvedBy,
|
||||
]);
|
||||
|
||||
// Fire event to trigger publishing
|
||||
event(new ArticleApproved($this));
|
||||
}
|
||||
|
||||
public function reject(string $rejectedBy = null): void
|
||||
{
|
||||
$this->update([
|
||||
'approval_status' => 'rejected',
|
||||
'approved_at' => now(),
|
||||
'approved_by' => $rejectedBy,
|
||||
]);
|
||||
}
|
||||
|
||||
public function canBePublished(): bool
|
||||
{
|
||||
if (!$this->isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If approval system is disabled, auto-approve valid articles
|
||||
if (!\App\Models\Setting::isPublishingApprovalsEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If approval system is enabled, only approved articles can be published
|
||||
return $this->isApproved();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasOne<ArticlePublication, $this>
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -41,4 +41,14 @@ public static function setArticleProcessingEnabled(bool $enabled): void
|
|||
{
|
||||
static::setBool('article_processing_enabled', $enabled);
|
||||
}
|
||||
|
||||
public static function isPublishingApprovalsEnabled(): bool
|
||||
{
|
||||
return static::getBool('enable_publishing_approvals', false);
|
||||
}
|
||||
|
||||
public static function setPublishingApprovalsEnabled(bool $enabled): void
|
||||
{
|
||||
static::setBool('enable_publishing_approvals', $enabled);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('articles', function (Blueprint $table) {
|
||||
$table->enum('approval_status', ['pending', 'approved', 'rejected'])
|
||||
->default('pending')
|
||||
->after('is_duplicate');
|
||||
$table->timestamp('approved_at')->nullable()->after('approval_status');
|
||||
$table->string('approved_by')->nullable()->after('approved_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('articles', function (Blueprint $table) {
|
||||
$table->dropColumn(['approval_status', 'approved_at', 'approved_by']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -15,7 +15,9 @@
|
|||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Approval</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
|
|
@ -33,7 +35,35 @@
|
|||
{{ $article->articlePublication ? 'Published' : 'Pending' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
|
||||
@if($article->approval_status === 'approved') bg-green-100 text-green-800
|
||||
@elseif($article->approval_status === 'rejected') bg-red-100 text-red-800
|
||||
@else bg-yellow-100 text-yellow-800 @endif">
|
||||
{{ ucfirst($article->approval_status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $article->created_at->format('Y-m-d H:i') }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
@if($publishingApprovalsEnabled && $article->isValid() && $article->isPending() && !$article->articlePublication)
|
||||
<div class="flex space-x-2">
|
||||
<form method="POST" action="{{ route('articles.approve', $article) }}" class="inline">
|
||||
@csrf
|
||||
<button type="submit" class="text-green-600 hover:text-green-900 font-medium">
|
||||
Approve & Publish
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ route('articles.reject', $article) }}" class="inline">
|
||||
@csrf
|
||||
<button type="submit" class="text-red-600 hover:text-red-900 font-medium">
|
||||
Reject
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@elseif($article->isValid() && !$article->articlePublication)
|
||||
<span class="text-gray-500">Auto-publishing enabled</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,30 @@ class="form-checkbox h-5 w-5 text-blue-600">
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Publishing Control</h3>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<label class="text-sm font-medium text-gray-700">Enable Publishing Approvals</label>
|
||||
<p class="text-sm text-gray-500 mt-1">When enabled, articles will require manual approval before being published to platforms.</p>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="hidden" name="enable_publishing_approvals" value="0">
|
||||
<input type="checkbox"
|
||||
name="enable_publishing_approvals"
|
||||
value="1"
|
||||
{{ $publishingApprovalsEnabled ? 'checked' : '' }}
|
||||
class="form-checkbox h-5 w-5 text-blue-600">
|
||||
<span class="ml-2 text-sm text-gray-700">
|
||||
{{ $publishingApprovalsEnabled ? 'Enabled' : 'Disabled' }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Save Settings
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
Route::get('/onboarding/complete', [OnboardingController::class, 'complete'])->name('onboarding.complete');
|
||||
|
||||
Route::get('/articles', ArticlesController::class)->name('articles');
|
||||
Route::post('/articles/{article}/approve', [ArticlesController::class, 'approve'])->name('articles.approve');
|
||||
Route::post('/articles/{article}/reject', [ArticlesController::class, 'reject'])->name('articles.reject');
|
||||
Route::get('/logs', LogsController::class)->name('logs');
|
||||
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');
|
||||
Route::put('/settings', [SettingsController::class, 'update'])->name('settings.update');
|
||||
|
|
|
|||
Loading…
Reference in a new issue