Skip to content

Commit 1f634b1

Browse files
Implement Verified Author Logic for Publishing Without Review (#1276) (#1303)
* Add verified author functionality - Add verified_author_at column to users table with migration - Add methods to check and manage verified author status in User model - Add admin interface to verify/unverify authors in UsersController - Add publishing limits for verified authors in ArticlesController - Add VerifyAuthor and UnVerifyAuthor jobs for queue processing - Add user policies for verified author management - Add admin view updates for verified author management - Add tests for verified author functionality * fix maybeFlashSuccessMessage * wip * wip * wip * wip * wip * wip --------- Co-authored-by: Dries Vints <[email protected]>
1 parent 6183bca commit 1f634b1

27 files changed

+375
-98
lines changed

app/Console/Commands/SyncArticleImages.php

Lines changed: 0 additions & 67 deletions
This file was deleted.

app/Http/Controllers/Admin/UsersController.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use App\Jobs\DeleteUser;
1010
use App\Jobs\DeleteUserThreads;
1111
use App\Jobs\UnbanUser;
12+
use App\Jobs\UnVerifyAuthor;
13+
use App\Jobs\VerifyAuthor;
1214
use App\Models\User;
1315
use App\Policies\UserPolicy;
1416
use App\Queries\SearchUsers;
@@ -60,6 +62,28 @@ public function unban(User $user): RedirectResponse
6062
return redirect()->route('profile', $user->username());
6163
}
6264

65+
public function verifyAuthor(User $user)
66+
{
67+
$this->authorize(UserPolicy::ADMIN, $user);
68+
69+
$this->dispatchSync(new VerifyAuthor($user));
70+
71+
$this->success($user->name() . ' was verified!');
72+
73+
return redirect()->route('admin.users');
74+
}
75+
76+
public function unverifyAuthor(User $user)
77+
{
78+
$this->authorize(UserPolicy::ADMIN, $user);
79+
80+
$this->dispatchSync(new UnverifyAuthor($user));
81+
82+
$this->success($user->name() . ' was unverified!');
83+
84+
return redirect()->route('admin.users');
85+
}
86+
6387
public function delete(User $user): RedirectResponse
6488
{
6589
$this->authorize(UserPolicy::DELETE, $user);

app/Http/Controllers/Articles/ArticlesController.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,13 @@ public function store(ArticleRequest $request)
115115

116116
$article = Article::findByUuidOrFail($uuid);
117117

118-
$this->success(
119-
$request->shouldBeSubmitted()
120-
? 'Thank you for submitting, unfortunately we can\'t accept every submission. You\'ll only hear back from us when we accept your article.'
121-
: 'Article successfully created!'
122-
);
118+
if ($article->isNotApproved()) {
119+
$this->success(
120+
$request->shouldBeSubmitted()
121+
? 'Thank you for submitting, unfortunately we can\'t accept every submission. You\'ll only hear back from us when we accept your article.'
122+
: 'Article successfully created!'
123+
);
124+
}
123125

124126
return $request->wantsJson()
125127
? ArticleResource::make($article)

app/Http/Requests/ArticleRequest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Models\User;
66
use App\Rules\HttpImageRule;
77
use Illuminate\Http\Concerns\InteractsWithInput;
8+
use Illuminate\Validation\Rules\RequiredIf;
89

910
class ArticleRequest extends Request
1011
{
@@ -14,6 +15,7 @@ public function rules(): array
1415
{
1516
return [
1617
'title' => ['required', 'max:100'],
18+
'hero_image_id' => ['nullable', new RequiredIf(auth()->user()->isVerifiedAuthor())],
1719
'body' => ['required', new HttpImageRule],
1820
'tags' => 'array|nullable',
1921
'tags.*' => 'exists:tags,id',
@@ -58,4 +60,9 @@ public function shouldBeSubmitted(): bool
5860
{
5961
return $this->boolean('submitted');
6062
}
63+
64+
public function heroImageId(): ?string
65+
{
66+
return $this->get('hero_image_id');
67+
}
6168
}

app/Jobs/CreateArticle.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public function __construct(
2020
private string $body,
2121
private User $author,
2222
private bool $shouldBeSubmitted,
23+
private ?string $heroImageId = null,
2324
array $options = []
2425
) {
2526
$this->originalUrl = $options['original_url'] ?? null;
@@ -34,6 +35,7 @@ public static function fromRequest(ArticleRequest $request, UuidInterface $uuid)
3435
$request->body(),
3536
$request->author(),
3637
$request->shouldBeSubmitted(),
38+
$request->heroImageId(),
3739
[
3840
'original_url' => $request->originalUrl(),
3941
'tags' => $request->tags(),
@@ -46,16 +48,27 @@ public function handle(): void
4648
$article = new Article([
4749
'uuid' => $this->uuid->toString(),
4850
'title' => $this->title,
51+
'hero_image_id' => $this->heroImageId,
4952
'body' => $this->body,
5053
'original_url' => $this->originalUrl,
5154
'slug' => $this->title,
5255
'submitted_at' => $this->shouldBeSubmitted ? now() : null,
56+
'approved_at' => $this->canBeAutoApproved() ? now() : null,
5357
]);
5458
$article->authoredBy($this->author);
5559
$article->syncTags($this->tags);
5660

61+
if ($article->hero_image_id) {
62+
SyncArticleImage::dispatch($article);
63+
}
64+
5765
if ($article->isAwaitingApproval()) {
5866
event(new ArticleWasSubmittedForApproval($article));
5967
}
6068
}
69+
70+
private function canBeAutoApproved(): bool
71+
{
72+
return $this->shouldBeSubmitted && $this->author->canVerifiedAuthorPublishMoreArticleToday();
73+
}
6174
}

app/Jobs/SyncArticleImage.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\Models\Article;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Foundation\Bus\Dispatchable;
8+
use Illuminate\Foundation\Queue\Queueable;
9+
use Illuminate\Queue\InteractsWithQueue;
10+
use Illuminate\Queue\SerializesModels;
11+
use Illuminate\Support\Facades\Http;
12+
13+
final class SyncArticleImage implements ShouldQueue
14+
{
15+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
16+
17+
public function __construct(public Article $article)
18+
{
19+
//
20+
}
21+
22+
public function handle(): void
23+
{
24+
$imageData = $this->fetchUnsplashImageDataFromId($this->article);
25+
26+
if (! is_null($imageData)) {
27+
$this->article->hero_image_url = $imageData['image_url'];
28+
$this->article->hero_image_author_name = $imageData['author_name'];
29+
$this->article->hero_image_author_url = $imageData['author_url'];
30+
$this->article->save();
31+
}
32+
}
33+
34+
protected function fetchUnsplashImageDataFromId(Article $article): ?array
35+
{
36+
$response = Http::retry(3, 100, throw: false)
37+
->withToken(config('services.unsplash.access_key'), 'Client-ID')
38+
->get("https://api.unsplash.com/photos/{$article->hero_image_id}");
39+
40+
if ($response->failed()) {
41+
$article->hero_image_id = null;
42+
$article->save();
43+
44+
return null;
45+
}
46+
47+
$response = $response->json();
48+
49+
// Trigger as Unsplash download...
50+
Http::retry(3, 100, throw: false)
51+
->withToken(config('services.unsplash.access_key'), 'Client-ID')
52+
->get($response['links']['download_location']);
53+
54+
return [
55+
'image_url' => $response['urls']['raw'],
56+
'author_name' => $response['user']['name'],
57+
'author_url' => $response['user']['links']['html'],
58+
];
59+
}
60+
}

app/Jobs/UnVerifyAuthor.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\Models\User;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Foundation\Queue\Queueable;
8+
9+
class UnVerifyAuthor implements ShouldQueue
10+
{
11+
use Queueable;
12+
13+
/**
14+
* Create a new job instance.
15+
*/
16+
public function __construct(private User $user)
17+
{
18+
//
19+
}
20+
21+
/**
22+
* Execute the job.
23+
*/
24+
public function handle(): void
25+
{
26+
$this->user->author_verified_at = null;
27+
$this->user->save();
28+
}
29+
}

app/Jobs/UpdateArticle.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public function __construct(
1717
private string $title,
1818
private string $body,
1919
private bool $shouldBeSubmitted,
20+
private ?string $heroImageId = null,
2021
array $options = []
2122
) {
2223
$this->originalUrl = $options['original_url'] ?? null;
@@ -30,6 +31,7 @@ public static function fromRequest(Article $article, ArticleRequest $request): s
3031
$request->title(),
3132
$request->body(),
3233
$request->shouldBeSubmitted(),
34+
$request->heroImageId(),
3335
[
3436
'original_url' => $request->originalUrl(),
3537
'tags' => $request->tags(),
@@ -39,9 +41,12 @@ public static function fromRequest(Article $article, ArticleRequest $request): s
3941

4042
public function handle(): void
4143
{
44+
$originalImage = $this->article->hero_image_id;
45+
4246
$this->article->update([
4347
'title' => $this->title,
4448
'body' => $this->body,
49+
'hero_image_id' => $this->heroImageId,
4550
'original_url' => $this->originalUrl,
4651
'slug' => $this->title,
4752
]);
@@ -54,6 +59,10 @@ public function handle(): void
5459
}
5560

5661
$this->article->syncTags($this->tags);
62+
63+
if ($this->article->hero_image_id !== $originalImage) {
64+
SyncArticleImage::dispatch($this->article);
65+
}
5766
}
5867

5968
private function shouldUpdateSubmittedAt(): bool

app/Jobs/VerifyAuthor.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\Models\User;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Foundation\Queue\Queueable;
8+
9+
class VerifyAuthor implements ShouldQueue
10+
{
11+
use Queueable;
12+
13+
/**
14+
* Create a new job instance.
15+
*/
16+
public function __construct(private User $user)
17+
{
18+
//
19+
}
20+
21+
/**
22+
* Execute the job.
23+
*/
24+
public function handle(): void
25+
{
26+
$this->user->author_verified_at = now();
27+
$this->user->save();
28+
}
29+
}

app/Models/Article.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ public function isShared(): bool
196196

197197
public function isAwaitingApproval(): bool
198198
{
199-
return $this->isSubmitted() && $this->isNotApproved() && $this->isNotDeclined();
199+
return $this->isSubmitted() && $this->isNotApproved() && $this->isNotDeclined() && ! $this->author()->canVerifiedAuthorPublishMoreArticleToday();
200200
}
201201

202202
public function isNotAwaitingApproval(): bool

0 commit comments

Comments
 (0)