Add tests for featured thumbnail generation; apply Pint formatting and related edits
This commit is contained in:
@@ -11,11 +11,15 @@ use App\Models\GroupChallenge;
|
||||
use App\Models\GroupEvent;
|
||||
use App\Models\GroupProject;
|
||||
use App\Models\GroupRelease;
|
||||
use App\Models\NewsArticleComment;
|
||||
use App\Models\User;
|
||||
use App\Support\News\NewsCoverImage;
|
||||
use App\Support\AvatarUrl;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use cPad\Plugins\News\Models\NewsArticle;
|
||||
@@ -25,6 +29,8 @@ use cPad\Plugins\News\Models\NewsTag;
|
||||
|
||||
final class NewsService
|
||||
{
|
||||
private const PUBLIC_CACHE_VERSION_KEY = 'news.public.cache.version';
|
||||
|
||||
public const RELATION_GROUP = 'group';
|
||||
public const RELATION_ARTWORK = 'artwork';
|
||||
public const RELATION_COLLECTION = 'collection';
|
||||
@@ -45,6 +51,8 @@ final class NewsService
|
||||
self::RELATION_USER => 'Profile',
|
||||
];
|
||||
|
||||
private ?bool $artworkStatsViewsColumnExists = null;
|
||||
|
||||
public function articleTypeOptions(): array
|
||||
{
|
||||
return \collect(NewsArticle::TYPE_LABELS)
|
||||
@@ -98,15 +106,23 @@ final class NewsService
|
||||
|
||||
public function sidebarData(): array
|
||||
{
|
||||
return [
|
||||
'categories' => NewsCategory::active()->withCount('publishedArticles')->ordered()->get(),
|
||||
'trending' => NewsArticle::published()
|
||||
->with('category')
|
||||
->orderByDesc('views')
|
||||
->limit(config('news.trending_limit', 5))
|
||||
->get(['id', 'title', 'slug', 'views', 'published_at', 'category_id', 'type']),
|
||||
'tags' => NewsTag::whereHas('articles', fn ($query) => $query->published())->orderBy('name')->get(),
|
||||
];
|
||||
return Cache::remember($this->publicCacheKey('sidebar'), $this->publicCacheTtl(), function (): array {
|
||||
return [
|
||||
'categories' => NewsCategory::active()->withCount('publishedArticles')->ordered()->get(),
|
||||
'trending' => NewsArticle::published()
|
||||
->with('category')
|
||||
->orderByDesc('views')
|
||||
->limit(config('news.trending_limit', 5))
|
||||
->get(['id', 'title', 'slug', 'views', 'published_at', 'category_id', 'type']),
|
||||
'tags' => NewsTag::query()
|
||||
->whereHas('articles', fn ($query) => $query->published())
|
||||
->withCount(['articles as published_articles_count' => fn ($query) => $query->published()])
|
||||
->orderByDesc('published_articles_count')
|
||||
->orderBy('name')
|
||||
->limit((int) config('news.sidebar_tags_limit', 18))
|
||||
->get(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function studioListing(array $filters = []): array
|
||||
@@ -169,6 +185,9 @@ final class NewsService
|
||||
'content' => (string) ($article->content ?? ''),
|
||||
'cover_image' => (string) ($article->cover_image ?? ''),
|
||||
'cover_url' => $article->cover_url,
|
||||
'cover_mobile_url' => $article->cover_mobile_url,
|
||||
'cover_desktop_url' => $article->cover_desktop_url,
|
||||
'cover_srcset' => $article->cover_srcset,
|
||||
'type' => (string) ($article->type ?? NewsArticle::TYPE_ANNOUNCEMENT),
|
||||
'editorial_status' => (string) ($article->editorial_status ?? NewsArticle::EDITORIAL_STATUS_DRAFT),
|
||||
'published_at' => \optional($article->published_at)?->toIso8601String(),
|
||||
@@ -214,6 +233,8 @@ final class NewsService
|
||||
public function deleteArticle(NewsArticle $article): void
|
||||
{
|
||||
$article->delete();
|
||||
|
||||
$this->invalidatePublicCache();
|
||||
}
|
||||
|
||||
public function publish(NewsArticle $article): NewsArticle
|
||||
@@ -224,6 +245,8 @@ final class NewsService
|
||||
'published_at' => $article->published_at ?? \now(),
|
||||
])->save();
|
||||
|
||||
$this->invalidatePublicCache();
|
||||
|
||||
return $article->fresh(['author', 'category', 'tags', 'relatedEntities']);
|
||||
}
|
||||
|
||||
@@ -234,6 +257,8 @@ final class NewsService
|
||||
'status' => 'draft',
|
||||
])->save();
|
||||
|
||||
$this->invalidatePublicCache();
|
||||
|
||||
return $article->fresh(['author', 'category', 'tags', 'relatedEntities']);
|
||||
}
|
||||
|
||||
@@ -241,6 +266,8 @@ final class NewsService
|
||||
{
|
||||
$article->forceFill(['is_featured' => ! $article->is_featured])->save();
|
||||
|
||||
$this->invalidatePublicCache();
|
||||
|
||||
return $article->fresh(['author', 'category', 'tags', 'relatedEntities']);
|
||||
}
|
||||
|
||||
@@ -248,6 +275,8 @@ final class NewsService
|
||||
{
|
||||
$article->forceFill(['is_pinned' => ! (bool) $article->is_pinned])->save();
|
||||
|
||||
$this->invalidatePublicCache();
|
||||
|
||||
return $article->fresh(['author', 'category', 'tags', 'relatedEntities']);
|
||||
}
|
||||
|
||||
@@ -280,6 +309,61 @@ final class NewsService
|
||||
->all();
|
||||
}
|
||||
|
||||
public function publicArticleShowData(NewsArticle $article, ?User $viewer = null): array
|
||||
{
|
||||
if ($viewer !== null) {
|
||||
return $this->buildPublicArticleShowData($article, $viewer);
|
||||
}
|
||||
|
||||
return Cache::remember(
|
||||
$this->publicCacheKey('article.show.' . $article->id),
|
||||
$this->publicCacheTtl(),
|
||||
fn (): array => $this->buildPublicArticleShowData($article, null),
|
||||
);
|
||||
}
|
||||
|
||||
private function buildPublicArticleShowData(NewsArticle $article, ?User $viewer = null): array
|
||||
{
|
||||
$related = NewsArticle::with('author', 'category')
|
||||
->published()
|
||||
->when($article->category_id, fn ($query) => $query->where('category_id', $article->category_id))
|
||||
->where('id', '!=', $article->id)
|
||||
->editorialOrder()
|
||||
->limit(config('news.related_limit', 4))
|
||||
->get();
|
||||
|
||||
$comments = collect();
|
||||
$commentsCount = 0;
|
||||
|
||||
if ($article->commentsAreEnabled()) {
|
||||
$comments = NewsArticleComment::query()
|
||||
->where('article_id', $article->id)
|
||||
->whereNull('parent_id')
|
||||
->where('status', 'visible')
|
||||
->with(['user.profile'])
|
||||
->orderBy('created_at')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$commentsCount = (int) NewsArticleComment::query()
|
||||
->where('article_id', $article->id)
|
||||
->where('status', 'visible')
|
||||
->count();
|
||||
}
|
||||
|
||||
return [
|
||||
'related' => $related,
|
||||
'relatedEntities' => $this->resolveRelatedEntities($article, $viewer),
|
||||
'comments' => $comments,
|
||||
'commentsCount' => $commentsCount,
|
||||
];
|
||||
}
|
||||
|
||||
public function invalidatePublicCache(): void
|
||||
{
|
||||
Cache::forever(self::PUBLIC_CACHE_VERSION_KEY, $this->publicCacheVersion() + 1);
|
||||
}
|
||||
|
||||
public function syncRelations(NewsArticle $article, array $relations): void
|
||||
{
|
||||
$normalized = \collect($relations)
|
||||
@@ -311,6 +395,23 @@ final class NewsService
|
||||
'sort_order' => $index,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->invalidatePublicCache();
|
||||
}
|
||||
|
||||
private function publicCacheKey(string $suffix): string
|
||||
{
|
||||
return 'news.public.v' . $this->publicCacheVersion() . '.' . $suffix;
|
||||
}
|
||||
|
||||
private function publicCacheTtl(): int
|
||||
{
|
||||
return max(60, (int) config('news.public_cache_ttl', 120));
|
||||
}
|
||||
|
||||
private function publicCacheVersion(): int
|
||||
{
|
||||
return (int) Cache::get(self::PUBLIC_CACHE_VERSION_KEY, 1);
|
||||
}
|
||||
|
||||
private function persistArticle(NewsArticle $article, User $editor, array $data): NewsArticle
|
||||
@@ -362,6 +463,8 @@ final class NewsService
|
||||
$article->tags()->sync($this->resolveArticleTagIds($data));
|
||||
$this->syncRelations($article, $data['relations'] ?? []);
|
||||
|
||||
$this->invalidatePublicCache();
|
||||
|
||||
return $article->fresh(['author.profile', 'category', 'tags', 'relatedEntities']);
|
||||
}
|
||||
|
||||
@@ -376,6 +479,7 @@ final class NewsService
|
||||
'editorial_status' => (string) ($article->editorial_status ?? NewsArticle::EDITORIAL_STATUS_DRAFT),
|
||||
'published_at' => \optional($article->published_at)?->toIso8601String(),
|
||||
'cover_url' => $article->cover_url,
|
||||
'cover_srcset' => $article->cover_srcset,
|
||||
'author_name' => (string) ($article->author?->name ?? 'Skinbase'),
|
||||
'category_name' => (string) ($article->category?->name ?? ''),
|
||||
'is_featured' => (bool) $article->is_featured,
|
||||
@@ -485,13 +589,13 @@ final class NewsService
|
||||
|
||||
private function deleteManagedCoverImage(string $path): void
|
||||
{
|
||||
$trimmed = ltrim(trim($path), '/');
|
||||
$paths = NewsCoverImage::managedPaths($path);
|
||||
|
||||
if ($trimmed === '' || ! Str::startsWith($trimmed, 'news/covers/')) {
|
||||
if ($paths === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
Storage::disk((string) config('uploads.object_storage.disk', 's3'))->delete($trimmed);
|
||||
Storage::disk((string) config('uploads.object_storage.disk', 's3'))->delete($paths);
|
||||
}
|
||||
|
||||
private function searchGroups(string $query, ?User $viewer): array
|
||||
@@ -517,8 +621,9 @@ final class NewsService
|
||||
|
||||
private function searchArtworks(string $query): array
|
||||
{
|
||||
return Artwork::query()
|
||||
$queryBuilder = Artwork::query()
|
||||
->with(['user.profile'])
|
||||
->select('artworks.*')
|
||||
->where('artwork_status', 'published')
|
||||
->where('visibility', Artwork::VISIBILITY_PUBLIC)
|
||||
->when($query !== '', function (Builder $builder) use ($query): void {
|
||||
@@ -528,8 +633,17 @@ final class NewsService
|
||||
->orWhere('description', 'like', '%' . $query . '%');
|
||||
});
|
||||
})
|
||||
->orderByDesc('views')
|
||||
->limit(8)
|
||||
->limit(8);
|
||||
|
||||
if ($this->artworkStatsViewsAvailable()) {
|
||||
$queryBuilder->leftJoin('artwork_stats as stats', 'stats.artwork_id', '=', 'artworks.id')
|
||||
->orderByRaw('COALESCE(stats.views, 0) DESC');
|
||||
} else {
|
||||
$queryBuilder->orderByDesc('published_at');
|
||||
}
|
||||
|
||||
return $queryBuilder
|
||||
->orderByDesc('artworks.id')
|
||||
->get()
|
||||
->map(fn (Artwork $artwork): ?array => $this->resolveArtworkPreview((int) $artwork->id, ''))
|
||||
->filter()
|
||||
@@ -537,6 +651,16 @@ final class NewsService
|
||||
->all();
|
||||
}
|
||||
|
||||
private function artworkStatsViewsAvailable(): bool
|
||||
{
|
||||
if ($this->artworkStatsViewsColumnExists === null) {
|
||||
$this->artworkStatsViewsColumnExists = Schema::hasTable('artwork_stats')
|
||||
&& Schema::hasColumn('artwork_stats', 'views');
|
||||
}
|
||||
|
||||
return $this->artworkStatsViewsColumnExists;
|
||||
}
|
||||
|
||||
private function searchCollections(string $query, ?User $viewer): array
|
||||
{
|
||||
return Collection::query()
|
||||
|
||||
Reference in New Issue
Block a user