optimizations
This commit is contained in:
673
app/Models/Collection.php
Normal file
673
app/Models/Collection.php
Normal file
@@ -0,0 +1,673 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\CollectionComment;
|
||||
use App\Models\CollectionDailyStat;
|
||||
use App\Models\CollectionHistory;
|
||||
use App\Models\CollectionMember;
|
||||
use App\Models\CollectionSave;
|
||||
use App\Models\CollectionSurfacePlacement;
|
||||
use App\Models\CollectionSubmission;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Collection extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const LIFECYCLE_DRAFT = 'draft';
|
||||
public const LIFECYCLE_SCHEDULED = 'scheduled';
|
||||
public const LIFECYCLE_PUBLISHED = 'published';
|
||||
public const LIFECYCLE_FEATURED = 'featured';
|
||||
public const LIFECYCLE_ARCHIVED = 'archived';
|
||||
public const LIFECYCLE_HIDDEN = 'hidden';
|
||||
public const LIFECYCLE_RESTRICTED = 'restricted';
|
||||
public const LIFECYCLE_UNDER_REVIEW = 'under_review';
|
||||
public const LIFECYCLE_EXPIRED = 'expired';
|
||||
|
||||
public const WORKFLOW_DRAFT = 'draft';
|
||||
public const WORKFLOW_IN_REVIEW = 'in_review';
|
||||
public const WORKFLOW_APPROVED = 'approved';
|
||||
public const WORKFLOW_PROGRAMMED = 'programmed';
|
||||
public const WORKFLOW_ARCHIVED = 'archived';
|
||||
|
||||
public const READINESS_READY = 'ready';
|
||||
public const READINESS_NEEDS_WORK = 'needs_work';
|
||||
public const READINESS_BLOCKED = 'blocked';
|
||||
|
||||
public const HEALTH_HEALTHY = 'healthy';
|
||||
public const HEALTH_NEEDS_METADATA = 'needs_metadata';
|
||||
public const HEALTH_STALE = 'stale';
|
||||
public const HEALTH_LOW_CONTENT = 'low_content';
|
||||
public const HEALTH_BROKEN_ITEMS = 'broken_items';
|
||||
public const HEALTH_WEAK_COVER = 'weak_cover';
|
||||
public const HEALTH_LOW_ENGAGEMENT = 'low_engagement';
|
||||
public const HEALTH_ATTRIBUTION_INCOMPLETE = 'attribution_incomplete';
|
||||
public const HEALTH_NEEDS_REVIEW = 'needs_review';
|
||||
public const HEALTH_DUPLICATE_RISK = 'duplicate_risk';
|
||||
public const HEALTH_MERGE_CANDIDATE = 'merge_candidate';
|
||||
|
||||
public const TYPE_PERSONAL = 'personal';
|
||||
public const TYPE_COMMUNITY = 'community';
|
||||
public const TYPE_EDITORIAL = 'editorial';
|
||||
|
||||
public const EDITORIAL_OWNER_CREATOR = 'creator';
|
||||
public const EDITORIAL_OWNER_STAFF_ACCOUNT = 'staff_account';
|
||||
public const EDITORIAL_OWNER_SYSTEM = 'system';
|
||||
|
||||
public const COLLABORATION_CLOSED = 'closed';
|
||||
public const COLLABORATION_INVITE_ONLY = 'invite_only';
|
||||
public const COLLABORATION_OPEN = 'open';
|
||||
|
||||
public const MODERATION_ACTIVE = 'active';
|
||||
public const MODERATION_UNDER_REVIEW = 'under_review';
|
||||
public const MODERATION_REVIEW = self::MODERATION_UNDER_REVIEW;
|
||||
public const MODERATION_RESTRICTED = 'restricted';
|
||||
public const MODERATION_HIDDEN = 'hidden';
|
||||
|
||||
public const MEMBER_ROLE_OWNER = 'owner';
|
||||
public const MEMBER_ROLE_EDITOR = 'editor';
|
||||
public const MEMBER_ROLE_CONTRIBUTOR = 'contributor';
|
||||
public const MEMBER_ROLE_VIEWER = 'viewer';
|
||||
|
||||
public const MEMBER_STATUS_PENDING = 'pending';
|
||||
public const MEMBER_STATUS_ACTIVE = 'active';
|
||||
public const MEMBER_STATUS_REVOKED = 'revoked';
|
||||
|
||||
public const SUBMISSION_PENDING = 'pending';
|
||||
public const SUBMISSION_APPROVED = 'approved';
|
||||
public const SUBMISSION_REJECTED = 'rejected';
|
||||
public const SUBMISSION_WITHDRAWN = 'withdrawn';
|
||||
|
||||
public const COMMENT_VISIBLE = 'visible';
|
||||
public const COMMENT_HIDDEN = 'hidden';
|
||||
public const COMMENT_FLAGGED = 'flagged';
|
||||
|
||||
public const VISIBILITY_PUBLIC = 'public';
|
||||
public const VISIBILITY_UNLISTED = 'unlisted';
|
||||
public const VISIBILITY_PRIVATE = 'private';
|
||||
|
||||
public const MODE_MANUAL = 'manual';
|
||||
public const MODE_SMART = 'smart';
|
||||
|
||||
public const SORT_MANUAL = 'manual';
|
||||
public const SORT_NEWEST = 'newest';
|
||||
public const SORT_OLDEST = 'oldest';
|
||||
public const SORT_POPULAR = 'popular';
|
||||
|
||||
public const SPOTLIGHT_STYLE_DEFAULT = 'default';
|
||||
public const SPOTLIGHT_STYLE_EDITORIAL = 'editorial';
|
||||
public const SPOTLIGHT_STYLE_SEASONAL = 'seasonal';
|
||||
public const SPOTLIGHT_STYLE_CHALLENGE = 'challenge';
|
||||
public const SPOTLIGHT_STYLE_COMMUNITY = 'community';
|
||||
|
||||
public const PRESENTATION_STANDARD = 'standard';
|
||||
public const PRESENTATION_EDITORIAL_GRID = 'editorial_grid';
|
||||
public const PRESENTATION_HERO_GRID = 'hero_grid';
|
||||
public const PRESENTATION_MASONRY = 'masonry';
|
||||
|
||||
public const EMPHASIS_COVER_HEAVY = 'cover_heavy';
|
||||
public const EMPHASIS_BALANCED = 'balanced';
|
||||
public const EMPHASIS_ARTWORK_FIRST = 'artwork_first';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'managed_by_user_id',
|
||||
'title',
|
||||
'slug',
|
||||
'lifecycle_state',
|
||||
'workflow_state',
|
||||
'readiness_state',
|
||||
'health_state',
|
||||
'health_flags_json',
|
||||
'canonical_collection_id',
|
||||
'duplicate_cluster_key',
|
||||
'program_key',
|
||||
'partner_key',
|
||||
'trust_tier',
|
||||
'experiment_key',
|
||||
'experiment_treatment',
|
||||
'placement_variant',
|
||||
'ranking_mode_variant',
|
||||
'collection_pool_version',
|
||||
'test_label',
|
||||
'recommendation_tier',
|
||||
'ranking_bucket',
|
||||
'search_boost_tier',
|
||||
'type',
|
||||
'editorial_owner_mode',
|
||||
'editorial_owner_user_id',
|
||||
'editorial_owner_label',
|
||||
'description',
|
||||
'subtitle',
|
||||
'summary',
|
||||
'collaboration_mode',
|
||||
'allow_submissions',
|
||||
'allow_comments',
|
||||
'allow_saves',
|
||||
'moderation_status',
|
||||
'published_at',
|
||||
'unpublished_at',
|
||||
'event_key',
|
||||
'event_label',
|
||||
'season_key',
|
||||
'banner_text',
|
||||
'badge_label',
|
||||
'spotlight_style',
|
||||
'quality_score',
|
||||
'ranking_score',
|
||||
'metadata_completeness_score',
|
||||
'editorial_readiness_score',
|
||||
'freshness_score',
|
||||
'engagement_score',
|
||||
'health_score',
|
||||
'last_health_check_at',
|
||||
'last_recommendation_refresh_at',
|
||||
'placement_eligibility',
|
||||
'analytics_enabled',
|
||||
'presentation_style',
|
||||
'emphasis_mode',
|
||||
'theme_token',
|
||||
'series_key',
|
||||
'series_title',
|
||||
'series_description',
|
||||
'series_order',
|
||||
'campaign_key',
|
||||
'campaign_label',
|
||||
'commercial_eligibility',
|
||||
'promotion_tier',
|
||||
'sponsorship_label',
|
||||
'sponsorship_state',
|
||||
'partner_label',
|
||||
'ownership_domain',
|
||||
'commercial_review_state',
|
||||
'legal_review_state',
|
||||
'monetization_ready_status',
|
||||
'brand_safe_status',
|
||||
'editorial_notes',
|
||||
'staff_commercial_notes',
|
||||
'archived_at',
|
||||
'expired_at',
|
||||
'history_count',
|
||||
'cover_artwork_id',
|
||||
'visibility',
|
||||
'mode',
|
||||
'sort_mode',
|
||||
'artworks_count',
|
||||
'comments_count',
|
||||
'saves_count',
|
||||
'collaborators_count',
|
||||
'is_featured',
|
||||
'profile_order',
|
||||
'views_count',
|
||||
'likes_count',
|
||||
'followers_count',
|
||||
'shares_count',
|
||||
'smart_rules_json',
|
||||
'layout_modules_json',
|
||||
'last_activity_at',
|
||||
'featured_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'artworks_count' => 'integer',
|
||||
'comments_count' => 'integer',
|
||||
'saves_count' => 'integer',
|
||||
'collaborators_count' => 'integer',
|
||||
'is_featured' => 'boolean',
|
||||
'profile_order' => 'integer',
|
||||
'views_count' => 'integer',
|
||||
'likes_count' => 'integer',
|
||||
'followers_count' => 'integer',
|
||||
'shares_count' => 'integer',
|
||||
'allow_submissions' => 'boolean',
|
||||
'allow_comments' => 'boolean',
|
||||
'allow_saves' => 'boolean',
|
||||
'analytics_enabled' => 'boolean',
|
||||
'commercial_eligibility' => 'boolean',
|
||||
'smart_rules_json' => 'array',
|
||||
'layout_modules_json' => 'array',
|
||||
'health_flags_json' => 'array',
|
||||
'quality_score' => 'decimal:2',
|
||||
'ranking_score' => 'decimal:2',
|
||||
'metadata_completeness_score' => 'decimal:2',
|
||||
'editorial_readiness_score' => 'decimal:2',
|
||||
'freshness_score' => 'decimal:2',
|
||||
'engagement_score' => 'decimal:2',
|
||||
'health_score' => 'decimal:2',
|
||||
'placement_eligibility' => 'boolean',
|
||||
'series_order' => 'integer',
|
||||
'history_count' => 'integer',
|
||||
'last_activity_at' => 'datetime',
|
||||
'featured_at' => 'datetime',
|
||||
'last_health_check_at' => 'datetime',
|
||||
'last_recommendation_refresh_at' => 'datetime',
|
||||
'published_at' => 'datetime',
|
||||
'unpublished_at' => 'datetime',
|
||||
'archived_at' => 'datetime',
|
||||
'expired_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected ?bool $cachedVisibilityWindow = null;
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function managedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'managed_by_user_id');
|
||||
}
|
||||
|
||||
public function editorialOwnerUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'editorial_owner_user_id');
|
||||
}
|
||||
|
||||
public function coverArtwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class, 'cover_artwork_id');
|
||||
}
|
||||
|
||||
public function likes(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionLike::class);
|
||||
}
|
||||
|
||||
public function saves(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionSave::class);
|
||||
}
|
||||
|
||||
public function follows(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionFollow::class);
|
||||
}
|
||||
|
||||
public function members(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionMember::class);
|
||||
}
|
||||
|
||||
public function submissions(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionSubmission::class);
|
||||
}
|
||||
|
||||
public function comments(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionComment::class);
|
||||
}
|
||||
|
||||
public function historyEntries(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionHistory::class);
|
||||
}
|
||||
|
||||
public function canonicalCollection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'canonical_collection_id');
|
||||
}
|
||||
|
||||
public function dailyStats(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionDailyStat::class);
|
||||
}
|
||||
|
||||
public function programAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionProgramAssignment::class);
|
||||
}
|
||||
|
||||
public function qualitySnapshots(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionQualitySnapshot::class);
|
||||
}
|
||||
|
||||
public function recommendationSnapshots(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionRecommendationSnapshot::class);
|
||||
}
|
||||
|
||||
public function mergeActionsAsSource(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionMergeAction::class, 'source_collection_id');
|
||||
}
|
||||
|
||||
public function mergeActionsAsTarget(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionMergeAction::class, 'target_collection_id');
|
||||
}
|
||||
|
||||
public function entityLinks(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionEntityLink::class);
|
||||
}
|
||||
|
||||
public function savedNotes(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionSavedNote::class);
|
||||
}
|
||||
|
||||
public function placements(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionSurfacePlacement::class);
|
||||
}
|
||||
|
||||
public function artworks(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Artwork::class, 'collection_artwork', 'collection_id', 'artwork_id')
|
||||
->withPivot(['order_num'])
|
||||
->withTimestamps()
|
||||
->orderByPivot('order_num');
|
||||
}
|
||||
|
||||
public function publicArtworks(): BelongsToMany
|
||||
{
|
||||
return $this->artworks()
|
||||
->whereNull('artworks.deleted_at')
|
||||
->where('artworks.is_public', true)
|
||||
->where('artworks.is_approved', true)
|
||||
->whereNotNull('artworks.published_at')
|
||||
->where('artworks.published_at', '<=', now());
|
||||
}
|
||||
|
||||
public function manualRelatedCollections(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(self::class, 'collection_related_links', 'collection_id', 'related_collection_id')
|
||||
->withPivot(['sort_order', 'created_by_user_id'])
|
||||
->withTimestamps()
|
||||
->orderBy('collection_related_links.sort_order')
|
||||
->orderBy('collection_related_links.id');
|
||||
}
|
||||
|
||||
public function scopePublic(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->where('visibility', self::VISIBILITY_PUBLIC)
|
||||
->where('moderation_status', self::MODERATION_ACTIVE)
|
||||
->whereNotIn('lifecycle_state', [
|
||||
self::LIFECYCLE_DRAFT,
|
||||
self::LIFECYCLE_EXPIRED,
|
||||
self::LIFECYCLE_HIDDEN,
|
||||
self::LIFECYCLE_RESTRICTED,
|
||||
self::LIFECYCLE_UNDER_REVIEW,
|
||||
])
|
||||
->where(function (Builder $builder): void {
|
||||
$builder->whereNull('published_at')
|
||||
->orWhere('published_at', '<=', now());
|
||||
})
|
||||
->where(function (Builder $builder): void {
|
||||
$builder->whereNull('unpublished_at')
|
||||
->orWhere('unpublished_at', '>', now());
|
||||
})
|
||||
->where(function (Builder $builder): void {
|
||||
$builder->whereNull('expired_at')
|
||||
->orWhere('expired_at', '>', now());
|
||||
});
|
||||
}
|
||||
|
||||
public function scopePublicEligible(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->public()
|
||||
->where('placement_eligibility', true)
|
||||
->whereIn('lifecycle_state', [
|
||||
self::LIFECYCLE_PUBLISHED,
|
||||
self::LIFECYCLE_FEATURED,
|
||||
]);
|
||||
}
|
||||
|
||||
public function scopeFeaturedPublic(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->publicEligible()
|
||||
->where('is_featured', true);
|
||||
}
|
||||
|
||||
public function scopeVisibleOnProfile(Builder $query): Builder
|
||||
{
|
||||
return $query->public();
|
||||
}
|
||||
|
||||
public function scopeOwnedBy(Builder $query, int $userId): Builder
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
public function isOwnedBy(User|int|null $user): bool
|
||||
{
|
||||
$userId = $user instanceof User ? $user->id : $user;
|
||||
|
||||
return $userId !== null && (int) $userId === (int) $this->user_id;
|
||||
}
|
||||
|
||||
public function isPubliclyAccessible(): bool
|
||||
{
|
||||
if ($this->moderation_status !== self::MODERATION_ACTIVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->published_at && $this->published_at->isFuture()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->unpublished_at && $this->unpublished_at->lte(now())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->expired_at && $this->expired_at->lte(now()) && $this->lifecycle_state === self::LIFECYCLE_EXPIRED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! in_array($this->lifecycle_state, [
|
||||
self::LIFECYCLE_SCHEDULED,
|
||||
self::LIFECYCLE_PUBLISHED,
|
||||
self::LIFECYCLE_FEATURED,
|
||||
self::LIFECYCLE_ARCHIVED,
|
||||
], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($this->visibility, [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED], true);
|
||||
}
|
||||
|
||||
public function isPubliclyEngageable(): bool
|
||||
{
|
||||
return $this->visibility === self::VISIBILITY_PUBLIC;
|
||||
}
|
||||
|
||||
public function displayOwnerName(): string
|
||||
{
|
||||
if ($this->type === self::TYPE_EDITORIAL && $this->editorial_owner_mode === self::EDITORIAL_OWNER_SYSTEM) {
|
||||
return (string) ($this->editorial_owner_label ?: config('collections.editorial.system_owner_label', 'Skinbase Editorial'));
|
||||
}
|
||||
|
||||
$owner = $this->relationLoaded('user') ? $this->user : $this->user()->first();
|
||||
|
||||
return (string) ($owner?->name ?: $owner?->username ?: 'Skinbase Curator');
|
||||
}
|
||||
|
||||
public function displayOwnerUsername(): ?string
|
||||
{
|
||||
if ($this->type === self::TYPE_EDITORIAL && $this->editorial_owner_mode === self::EDITORIAL_OWNER_SYSTEM) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$owner = $this->relationLoaded('user') ? $this->user : $this->user()->first();
|
||||
|
||||
return $owner?->username;
|
||||
}
|
||||
|
||||
public function hasSystemEditorialOwner(): bool
|
||||
{
|
||||
return $this->type === self::TYPE_EDITORIAL && $this->editorial_owner_mode === self::EDITORIAL_OWNER_SYSTEM;
|
||||
}
|
||||
|
||||
public function isCollaborative(): bool
|
||||
{
|
||||
return $this->type !== self::TYPE_PERSONAL || $this->collaboration_mode !== self::COLLABORATION_CLOSED;
|
||||
}
|
||||
|
||||
public function activeMemberRoleFor(User|int|null $user): ?string
|
||||
{
|
||||
$userId = $user instanceof User ? $user->id : $user;
|
||||
|
||||
if ($userId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isOwnedBy($userId)) {
|
||||
return self::MEMBER_ROLE_OWNER;
|
||||
}
|
||||
|
||||
$members = $this->relationLoaded('members')
|
||||
? $this->members
|
||||
: $this->members()->where('status', self::MEMBER_STATUS_ACTIVE)->get();
|
||||
|
||||
return $members
|
||||
->first(fn (CollectionMember $member) => (int) $member->user_id === (int) $userId && $member->status === self::MEMBER_STATUS_ACTIVE)
|
||||
?->role;
|
||||
}
|
||||
|
||||
public function hasActiveMember(User|int|null $user): bool
|
||||
{
|
||||
return $this->activeMemberRoleFor($user) !== null;
|
||||
}
|
||||
|
||||
public function canBeManagedBy(User $user): bool
|
||||
{
|
||||
return in_array($this->activeMemberRoleFor($user), [
|
||||
self::MEMBER_ROLE_OWNER,
|
||||
self::MEMBER_ROLE_EDITOR,
|
||||
], true);
|
||||
}
|
||||
|
||||
public function canManageArtworks(User $user): bool
|
||||
{
|
||||
return $this->canBeManagedBy($user);
|
||||
}
|
||||
|
||||
public function canManageMembers(User $user): bool
|
||||
{
|
||||
return $this->isCollaborative() && $this->canBeManagedBy($user);
|
||||
}
|
||||
|
||||
public function canReceiveSubmissionsFrom(?User $user): bool
|
||||
{
|
||||
if (! $user || ! $this->allow_submissions || ! $this->isPubliclyAccessible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->type === self::TYPE_EDITORIAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->collaboration_mode === self::COLLABORATION_CLOSED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! $this->hasActiveMember($user);
|
||||
}
|
||||
|
||||
public function canReceiveCommentsFrom(?User $user): bool
|
||||
{
|
||||
return $user !== null && $this->allow_comments && $this->canBeViewedBy($user);
|
||||
}
|
||||
|
||||
public function canBeSavedBy(?User $user): bool
|
||||
{
|
||||
return $user !== null && $this->allow_saves && $this->visibility === self::VISIBILITY_PUBLIC && ! $this->isOwnedBy($user);
|
||||
}
|
||||
|
||||
public function isFeatureablePublicly(): bool
|
||||
{
|
||||
return $this->visibility === self::VISIBILITY_PUBLIC
|
||||
&& $this->moderation_status === self::MODERATION_ACTIVE
|
||||
&& (bool) $this->placement_eligibility
|
||||
&& in_array($this->lifecycle_state, [self::LIFECYCLE_PUBLISHED, self::LIFECYCLE_FEATURED], true);
|
||||
}
|
||||
|
||||
public function isSmart(): bool
|
||||
{
|
||||
return $this->mode === self::MODE_SMART;
|
||||
}
|
||||
|
||||
public function canBeViewedBy(?User $viewer): bool
|
||||
{
|
||||
if ($viewer && ($this->isOwnedBy($viewer) || $this->hasActiveMember($viewer))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->isPubliclyAccessible();
|
||||
}
|
||||
|
||||
public function resolvedCoverArtwork(bool $publicOnly = false): ?Artwork
|
||||
{
|
||||
$cover = $this->relationLoaded('coverArtwork') ? $this->coverArtwork : $this->coverArtwork()->first();
|
||||
if ($cover && (! $publicOnly || $this->artworkIsPubliclyVisible($cover))) {
|
||||
return $cover;
|
||||
}
|
||||
|
||||
$relation = $publicOnly ? 'publicArtworks' : 'artworks';
|
||||
$artworks = $this->relationLoaded($relation)
|
||||
? $this->getRelation($relation)
|
||||
: $this->{$relation}()->limit(1)->get();
|
||||
|
||||
return $artworks->first();
|
||||
}
|
||||
|
||||
public function syncArtworksCount(): void
|
||||
{
|
||||
$this->forceFill([
|
||||
'artworks_count' => $this->isSmart()
|
||||
? (int) $this->artworks_count
|
||||
: $this->artworks()->count(),
|
||||
])->save();
|
||||
}
|
||||
|
||||
public function inSeries(): bool
|
||||
{
|
||||
return filled($this->series_key);
|
||||
}
|
||||
|
||||
public function hasCanonicalTarget(): bool
|
||||
{
|
||||
return $this->canonical_collection_id !== null;
|
||||
}
|
||||
|
||||
public function isPlacementEligible(): bool
|
||||
{
|
||||
return (bool) $this->placement_eligibility;
|
||||
}
|
||||
|
||||
public function supportsAnalytics(): bool
|
||||
{
|
||||
return (bool) $this->analytics_enabled;
|
||||
}
|
||||
|
||||
public function usesPremiumPresentation(): bool
|
||||
{
|
||||
return $this->presentation_style !== self::PRESENTATION_STANDARD
|
||||
|| $this->emphasis_mode !== self::EMPHASIS_BALANCED
|
||||
|| filled($this->theme_token);
|
||||
}
|
||||
|
||||
private function artworkIsPubliclyVisible(Artwork $artwork): bool
|
||||
{
|
||||
return ! $artwork->trashed()
|
||||
&& (bool) $artwork->is_public
|
||||
&& (bool) $artwork->is_approved
|
||||
&& $artwork->published_at !== null
|
||||
&& $artwork->published_at->lte(now());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user