optimizations
This commit is contained in:
@@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use App\Services\ThumbnailService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Scout\Searchable;
|
||||
@@ -27,6 +26,10 @@ class Artwork extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, Searchable;
|
||||
|
||||
public const VISIBILITY_PUBLIC = 'public';
|
||||
public const VISIBILITY_UNLISTED = 'unlisted';
|
||||
public const VISIBILITY_PRIVATE = 'private';
|
||||
|
||||
protected $table = 'artworks';
|
||||
|
||||
protected $fillable = [
|
||||
@@ -44,11 +47,23 @@ class Artwork extends Model
|
||||
'width',
|
||||
'height',
|
||||
'is_public',
|
||||
'visibility',
|
||||
'is_approved',
|
||||
'is_mature',
|
||||
'published_at',
|
||||
'hash',
|
||||
'thumb_ext',
|
||||
'file_ext',
|
||||
'clip_tags_json',
|
||||
'blip_caption',
|
||||
'yolo_objects_json',
|
||||
'vision_metadata_updated_at',
|
||||
'last_vector_indexed_at',
|
||||
'ai_status',
|
||||
'title_source',
|
||||
'description_source',
|
||||
'tags_source',
|
||||
'category_source',
|
||||
// Versioning
|
||||
'current_version_id',
|
||||
'version_count',
|
||||
@@ -62,9 +77,20 @@ class Artwork extends Model
|
||||
|
||||
protected $casts = [
|
||||
'is_public' => 'boolean',
|
||||
'visibility' => 'string',
|
||||
'is_approved' => 'boolean',
|
||||
'is_mature' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
'publish_at' => 'datetime',
|
||||
'clip_tags_json' => 'array',
|
||||
'yolo_objects_json' => 'array',
|
||||
'vision_metadata_updated_at' => 'datetime',
|
||||
'last_vector_indexed_at' => 'datetime',
|
||||
'ai_status' => 'string',
|
||||
'title_source' => 'string',
|
||||
'description_source' => 'string',
|
||||
'tags_source' => 'string',
|
||||
'category_source' => 'string',
|
||||
'version_updated_at' => 'datetime',
|
||||
'requires_reapproval' => 'boolean',
|
||||
];
|
||||
@@ -156,6 +182,14 @@ class Artwork extends Model
|
||||
return $this->belongsToMany(Category::class, 'artwork_category', 'artwork_id', 'category_id');
|
||||
}
|
||||
|
||||
public function collections(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Collection::class, 'collection_artwork', 'artwork_id', 'collection_id')
|
||||
->withPivot(['order_num'])
|
||||
->withTimestamps()
|
||||
->orderByPivot('order_num');
|
||||
}
|
||||
|
||||
public function tags(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Tag::class, 'artwork_tag', 'artwork_id', 'tag_id')
|
||||
@@ -218,6 +252,11 @@ class Artwork extends Model
|
||||
return $this->belongsTo(ArtworkVersion::class, 'current_version_id');
|
||||
}
|
||||
|
||||
public function artworkAiAssist(): HasOne
|
||||
{
|
||||
return $this->hasOne(ArtworkAiAssist::class, 'artwork_id');
|
||||
}
|
||||
|
||||
public function awardStat(): HasOne
|
||||
{
|
||||
return $this->hasOne(ArtworkAwardStat::class);
|
||||
@@ -267,6 +306,17 @@ class Artwork extends Model
|
||||
'category' => $category,
|
||||
'content_type' => $content_type,
|
||||
'tags' => $tags,
|
||||
'ai_clip_tags' => collect((array) ($this->clip_tags_json ?? []))
|
||||
->map(static fn ($row) => is_array($row) ? (string) ($row['tag'] ?? '') : '')
|
||||
->filter()
|
||||
->values()
|
||||
->all(),
|
||||
'ai_blip_caption' => (string) ($this->blip_caption ?? ''),
|
||||
'ai_yolo_objects' => collect((array) ($this->yolo_objects_json ?? []))
|
||||
->map(static fn ($row) => is_array($row) ? (string) ($row['tag'] ?? '') : '')
|
||||
->filter()
|
||||
->values()
|
||||
->all(),
|
||||
'resolution' => $resolution,
|
||||
'orientation' => $orientation,
|
||||
'downloads' => (int) ($stat?->downloads ?? 0),
|
||||
@@ -276,6 +326,7 @@ class Artwork extends Model
|
||||
'is_public' => (bool) $this->is_public,
|
||||
'is_approved' => (bool) $this->is_approved,
|
||||
// ── Trending / discovery fields ────────────────────────────────────
|
||||
'trending_score_1h' => (float) ($this->trending_score_1h ?? 0),
|
||||
'trending_score_24h' => (float) ($this->trending_score_24h ?? 0),
|
||||
'trending_score_7d' => (float) ($this->trending_score_7d ?? 0),
|
||||
'favorites_count' => (int) ($stat?->favorites ?? 0),
|
||||
|
||||
48
app/Models/ArtworkAiAssist.php
Normal file
48
app/Models/ArtworkAiAssist.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class ArtworkAiAssist extends Model
|
||||
{
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_QUEUED = 'queued';
|
||||
public const STATUS_PROCESSING = 'processing';
|
||||
public const STATUS_READY = 'ready';
|
||||
public const STATUS_FAILED = 'failed';
|
||||
|
||||
protected $fillable = [
|
||||
'artwork_id',
|
||||
'status',
|
||||
'mode',
|
||||
'title_suggestions_json',
|
||||
'description_suggestions_json',
|
||||
'tag_suggestions_json',
|
||||
'category_suggestions_json',
|
||||
'similar_candidates_json',
|
||||
'raw_response_json',
|
||||
'action_log_json',
|
||||
'error_message',
|
||||
'processed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'title_suggestions_json' => 'array',
|
||||
'description_suggestions_json' => 'array',
|
||||
'tag_suggestions_json' => 'array',
|
||||
'category_suggestions_json' => 'array',
|
||||
'similar_candidates_json' => 'array',
|
||||
'raw_response_json' => 'array',
|
||||
'action_log_json' => 'array',
|
||||
'processed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class);
|
||||
}
|
||||
}
|
||||
38
app/Models/ArtworkAiAssistEvent.php
Normal file
38
app/Models/ArtworkAiAssistEvent.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class ArtworkAiAssistEvent extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'artwork_ai_assist_id',
|
||||
'artwork_id',
|
||||
'user_id',
|
||||
'event_type',
|
||||
'meta',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
public function assist(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ArtworkAiAssist::class, 'artwork_ai_assist_id');
|
||||
}
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
45
app/Models/CollectionComment.php
Normal file
45
app/Models/CollectionComment.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class CollectionComment extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'user_id',
|
||||
'parent_id',
|
||||
'body',
|
||||
'rendered_body',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function replies(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id')->orderBy('created_at');
|
||||
}
|
||||
}
|
||||
42
app/Models/CollectionDailyStat.php
Normal file
42
app/Models/CollectionDailyStat.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionDailyStat extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'stat_date',
|
||||
'views_count',
|
||||
'likes_count',
|
||||
'follows_count',
|
||||
'saves_count',
|
||||
'comments_count',
|
||||
'shares_count',
|
||||
'submissions_count',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'views_count' => 'integer',
|
||||
'likes_count' => 'integer',
|
||||
'follows_count' => 'integer',
|
||||
'saves_count' => 'integer',
|
||||
'comments_count' => 'integer',
|
||||
'shares_count' => 'integer',
|
||||
'submissions_count' => 'integer',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
}
|
||||
31
app/Models/CollectionEntityLink.php
Normal file
31
app/Models/CollectionEntityLink.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionEntityLink extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'linked_type',
|
||||
'linked_id',
|
||||
'relationship_type',
|
||||
'metadata_json',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata_json' => 'array',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
}
|
||||
36
app/Models/CollectionFollow.php
Normal file
36
app/Models/CollectionFollow.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionFollow extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'user_id',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
44
app/Models/CollectionHistory.php
Normal file
44
app/Models/CollectionHistory.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionHistory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'collection_history';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'actor_user_id',
|
||||
'action_type',
|
||||
'summary',
|
||||
'before_json',
|
||||
'after_json',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'before_json' => 'array',
|
||||
'after_json' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
|
||||
public function actor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'actor_user_id');
|
||||
}
|
||||
}
|
||||
36
app/Models/CollectionLike.php
Normal file
36
app/Models/CollectionLike.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionLike extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'user_id',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
49
app/Models/CollectionMember.php
Normal file
49
app/Models/CollectionMember.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionMember extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'user_id',
|
||||
'invited_by_user_id',
|
||||
'role',
|
||||
'status',
|
||||
'note',
|
||||
'invited_at',
|
||||
'expires_at',
|
||||
'accepted_at',
|
||||
'revoked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'invited_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
'accepted_at' => 'datetime',
|
||||
'revoked_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function invitedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'invited_by_user_id');
|
||||
}
|
||||
}
|
||||
37
app/Models/CollectionMergeAction.php
Normal file
37
app/Models/CollectionMergeAction.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionMergeAction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'source_collection_id',
|
||||
'target_collection_id',
|
||||
'action_type',
|
||||
'actor_user_id',
|
||||
'summary',
|
||||
];
|
||||
|
||||
public function sourceCollection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class, 'source_collection_id');
|
||||
}
|
||||
|
||||
public function targetCollection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class, 'target_collection_id');
|
||||
}
|
||||
|
||||
public function actor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'actor_user_id');
|
||||
}
|
||||
}
|
||||
42
app/Models/CollectionProgramAssignment.php
Normal file
42
app/Models/CollectionProgramAssignment.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionProgramAssignment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'program_key',
|
||||
'campaign_key',
|
||||
'placement_scope',
|
||||
'starts_at',
|
||||
'ends_at',
|
||||
'priority',
|
||||
'notes',
|
||||
'created_by_user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'starts_at' => 'datetime',
|
||||
'ends_at' => 'datetime',
|
||||
'priority' => 'integer',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||
}
|
||||
}
|
||||
36
app/Models/CollectionQualitySnapshot.php
Normal file
36
app/Models/CollectionQualitySnapshot.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionQualitySnapshot extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'snapshot_date',
|
||||
'quality_score',
|
||||
'health_score',
|
||||
'metadata_completeness_score',
|
||||
'freshness_score',
|
||||
'engagement_score',
|
||||
'readiness_score',
|
||||
'flags_json',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'snapshot_date' => 'date',
|
||||
'flags_json' => 'array',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
}
|
||||
32
app/Models/CollectionRecommendationSnapshot.php
Normal file
32
app/Models/CollectionRecommendationSnapshot.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionRecommendationSnapshot extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'context_key',
|
||||
'recommendation_score',
|
||||
'rationale_json',
|
||||
'snapshot_date',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'rationale_json' => 'array',
|
||||
'snapshot_date' => 'date',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
}
|
||||
41
app/Models/CollectionSave.php
Normal file
41
app/Models/CollectionSave.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionSave extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'user_id',
|
||||
'created_at',
|
||||
'last_viewed_at',
|
||||
'save_context',
|
||||
'save_context_meta_json',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'last_viewed_at' => 'datetime',
|
||||
'save_context_meta_json' => 'array',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
31
app/Models/CollectionSavedList.php
Normal file
31
app/Models/CollectionSavedList.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class CollectionSavedList extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'title',
|
||||
'slug',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionSavedListItem::class, 'saved_list_id')->orderBy('order_num');
|
||||
}
|
||||
}
|
||||
38
app/Models/CollectionSavedListItem.php
Normal file
38
app/Models/CollectionSavedListItem.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionSavedListItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'saved_list_id',
|
||||
'collection_id',
|
||||
'order_num',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'order_num' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function list(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CollectionSavedList::class, 'saved_list_id');
|
||||
}
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
}
|
||||
30
app/Models/CollectionSavedNote.php
Normal file
30
app/Models/CollectionSavedNote.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionSavedNote extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'collection_id',
|
||||
'note',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
}
|
||||
48
app/Models/CollectionSubmission.php
Normal file
48
app/Models/CollectionSubmission.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionSubmission extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'artwork_id',
|
||||
'user_id',
|
||||
'message',
|
||||
'status',
|
||||
'reviewed_by_user_id',
|
||||
'reviewed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'reviewed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function reviewedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'reviewed_by_user_id');
|
||||
}
|
||||
}
|
||||
43
app/Models/CollectionSurfaceDefinition.php
Normal file
43
app/Models/CollectionSurfaceDefinition.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class CollectionSurfaceDefinition extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'collection_surface_definitions';
|
||||
|
||||
protected $fillable = [
|
||||
'surface_key',
|
||||
'title',
|
||||
'description',
|
||||
'mode',
|
||||
'rules_json',
|
||||
'ranking_mode',
|
||||
'max_items',
|
||||
'is_active',
|
||||
'starts_at',
|
||||
'ends_at',
|
||||
'fallback_surface_key',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'rules_json' => 'array',
|
||||
'max_items' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'starts_at' => 'datetime',
|
||||
'ends_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function placements(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionSurfacePlacement::class, 'surface_key', 'surface_key');
|
||||
}
|
||||
}
|
||||
46
app/Models/CollectionSurfacePlacement.php
Normal file
46
app/Models/CollectionSurfacePlacement.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CollectionSurfacePlacement extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'collection_surface_placements';
|
||||
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'surface_key',
|
||||
'placement_type',
|
||||
'priority',
|
||||
'starts_at',
|
||||
'ends_at',
|
||||
'is_active',
|
||||
'campaign_key',
|
||||
'notes',
|
||||
'created_by_user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'priority' => 'integer',
|
||||
'starts_at' => 'datetime',
|
||||
'ends_at' => 'datetime',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Collection::class);
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||
}
|
||||
}
|
||||
301
app/Models/NovaCard.php
Normal file
301
app/Models/NovaCard.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
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;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class NovaCard extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const FORMAT_SQUARE = 'square';
|
||||
public const FORMAT_PORTRAIT = 'portrait';
|
||||
public const FORMAT_STORY = 'story';
|
||||
public const FORMAT_LANDSCAPE = 'landscape';
|
||||
|
||||
public const VISIBILITY_PUBLIC = 'public';
|
||||
public const VISIBILITY_UNLISTED = 'unlisted';
|
||||
public const VISIBILITY_PRIVATE = 'private';
|
||||
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
public const STATUS_PROCESSING = 'processing';
|
||||
public const STATUS_PUBLISHED = 'published';
|
||||
public const STATUS_HIDDEN = 'hidden';
|
||||
public const STATUS_REJECTED = 'rejected';
|
||||
|
||||
public const MOD_PENDING = 'pending';
|
||||
public const MOD_APPROVED = 'approved';
|
||||
public const MOD_FLAGGED = 'flagged';
|
||||
public const MOD_REJECTED = 'rejected';
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'user_id',
|
||||
'category_id',
|
||||
'title',
|
||||
'slug',
|
||||
'quote_text',
|
||||
'quote_author',
|
||||
'quote_source',
|
||||
'description',
|
||||
'format',
|
||||
'project_json',
|
||||
'schema_version',
|
||||
'render_version',
|
||||
'preview_path',
|
||||
'preview_width',
|
||||
'preview_height',
|
||||
'background_type',
|
||||
'background_image_id',
|
||||
'original_card_id',
|
||||
'root_card_id',
|
||||
'template_id',
|
||||
'visibility',
|
||||
'status',
|
||||
'moderation_status',
|
||||
'featured',
|
||||
'allow_download',
|
||||
'allow_remix',
|
||||
'views_count',
|
||||
'shares_count',
|
||||
'downloads_count',
|
||||
'likes_count',
|
||||
'favorites_count',
|
||||
'saves_count',
|
||||
'remixes_count',
|
||||
'comments_count',
|
||||
'challenge_entries_count',
|
||||
'trending_score',
|
||||
'featured_score',
|
||||
'style_family',
|
||||
'palette_family',
|
||||
'density_score',
|
||||
'editor_mode_last_used',
|
||||
'allow_background_reuse',
|
||||
'allow_export',
|
||||
'original_creator_id',
|
||||
'published_at',
|
||||
'last_engaged_at',
|
||||
'last_ranked_at',
|
||||
'last_rendered_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'project_json' => 'array',
|
||||
'schema_version' => 'integer',
|
||||
'render_version' => 'integer',
|
||||
'preview_width' => 'integer',
|
||||
'preview_height' => 'integer',
|
||||
'featured' => 'boolean',
|
||||
'allow_download' => 'boolean',
|
||||
'allow_remix' => 'boolean',
|
||||
'views_count' => 'integer',
|
||||
'shares_count' => 'integer',
|
||||
'downloads_count' => 'integer',
|
||||
'likes_count' => 'integer',
|
||||
'favorites_count' => 'integer',
|
||||
'saves_count' => 'integer',
|
||||
'remixes_count' => 'integer',
|
||||
'comments_count' => 'integer',
|
||||
'challenge_entries_count' => 'integer',
|
||||
'trending_score' => 'float',
|
||||
'featured_score' => 'float',
|
||||
'density_score' => 'integer',
|
||||
'allow_background_reuse' => 'boolean',
|
||||
'allow_export' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
'last_engaged_at' => 'datetime',
|
||||
'last_ranked_at' => 'datetime',
|
||||
'last_rendered_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (self $card): void {
|
||||
if (! $card->uuid) {
|
||||
$card->uuid = (string) Str::uuid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCardCategory::class, 'category_id');
|
||||
}
|
||||
|
||||
public function template(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCardTemplate::class, 'template_id');
|
||||
}
|
||||
|
||||
public function backgroundImage(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCardBackground::class, 'background_image_id');
|
||||
}
|
||||
|
||||
public function originalCard(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'original_card_id');
|
||||
}
|
||||
|
||||
public function rootCard(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'root_card_id');
|
||||
}
|
||||
|
||||
public function originalCreator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'original_creator_id');
|
||||
}
|
||||
|
||||
public function remixes(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'original_card_id');
|
||||
}
|
||||
|
||||
public function tags(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(NovaCardTag::class, 'nova_card_tag_relation', 'card_id', 'tag_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function reactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCardReaction::class, 'card_id');
|
||||
}
|
||||
|
||||
public function versions(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCardVersion::class, 'card_id');
|
||||
}
|
||||
|
||||
public function collectionItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCardCollectionItem::class, 'card_id');
|
||||
}
|
||||
|
||||
public function challengeEntries(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCardChallengeEntry::class, 'card_id');
|
||||
}
|
||||
|
||||
public function comments(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCardComment::class, 'card_id');
|
||||
}
|
||||
|
||||
public function scopePublished(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->where('status', self::STATUS_PUBLISHED)
|
||||
->where('moderation_status', '!=', self::MOD_REJECTED)
|
||||
->whereIn('visibility', [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED])
|
||||
->whereNotNull('published_at');
|
||||
}
|
||||
|
||||
public function scopePubliclyVisible(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->published()
|
||||
->where('visibility', self::VISIBILITY_PUBLIC)
|
||||
->whereNotIn('moderation_status', [self::MOD_FLAGGED, self::MOD_REJECTED])
|
||||
->where('status', self::STATUS_PUBLISHED);
|
||||
}
|
||||
|
||||
public function scopeRising(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->publiclyVisible()
|
||||
->where('published_at', '>=', now()->subHours(96))
|
||||
->where(function (Builder $q): void {
|
||||
$q->where('saves_count', '>', 0)
|
||||
->orWhere('remixes_count', '>', 0)
|
||||
->orWhere('likes_count', '>', 1);
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeFeaturedEditorial(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->publiclyVisible()
|
||||
->where(function (Builder $q): void {
|
||||
$q->whereNotNull('featured_score')
|
||||
->orWhere('featured', true);
|
||||
});
|
||||
}
|
||||
|
||||
public function previewUrl(): ?string
|
||||
{
|
||||
if (! $this->preview_path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Storage::disk((string) config('nova_cards.storage.public_disk', 'public'))->url($this->preview_path);
|
||||
}
|
||||
|
||||
public function ogPreviewUrl(): ?string
|
||||
{
|
||||
if (! $this->preview_path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ogPath = preg_replace('/\.webp$/', '-og.jpg', $this->preview_path) ?: null;
|
||||
if (! $ogPath) {
|
||||
return $this->previewUrl();
|
||||
}
|
||||
|
||||
return Storage::disk((string) config('nova_cards.storage.public_disk', 'public'))->url($ogPath);
|
||||
}
|
||||
|
||||
public function publicUrl(): string
|
||||
{
|
||||
return route('cards.show', ['slug' => $this->slug, 'id' => $this->id]);
|
||||
}
|
||||
|
||||
public function isOwnedBy(?User $user): bool
|
||||
{
|
||||
return $user !== null && (int) $user->id === (int) $this->user_id;
|
||||
}
|
||||
|
||||
public function canBeViewedBy(?User $user): bool
|
||||
{
|
||||
if ($this->isOwnedBy($user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->status !== self::STATUS_PUBLISHED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->moderation_status === self::MOD_REJECTED || $this->moderation_status === self::MOD_FLAGGED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($this->visibility, [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED], true);
|
||||
}
|
||||
|
||||
public function isRemix(): bool
|
||||
{
|
||||
return $this->original_card_id !== null;
|
||||
}
|
||||
|
||||
public function canReceiveCommentsFrom(?User $user): bool
|
||||
{
|
||||
return $user !== null && $this->canBeViewedBy($user);
|
||||
}
|
||||
}
|
||||
35
app/Models/NovaCardAsset.php
Normal file
35
app/Models/NovaCardAsset.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class NovaCardAsset extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'asset_pack_id',
|
||||
'asset_key',
|
||||
'label',
|
||||
'type',
|
||||
'preview_image',
|
||||
'data_json',
|
||||
'official',
|
||||
'active',
|
||||
'order_num',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data_json' => 'array',
|
||||
'official' => 'boolean',
|
||||
'active' => 'boolean',
|
||||
'order_num' => 'integer',
|
||||
];
|
||||
|
||||
public function pack(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCardAssetPack::class, 'asset_pack_id');
|
||||
}
|
||||
}
|
||||
38
app/Models/NovaCardAssetPack.php
Normal file
38
app/Models/NovaCardAssetPack.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class NovaCardAssetPack extends Model
|
||||
{
|
||||
public const TYPE_ASSET = 'asset';
|
||||
public const TYPE_TEMPLATE = 'template';
|
||||
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'preview_image',
|
||||
'manifest_json',
|
||||
'official',
|
||||
'active',
|
||||
'order_num',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'manifest_json' => 'array',
|
||||
'official' => 'boolean',
|
||||
'active' => 'boolean',
|
||||
'order_num' => 'integer',
|
||||
];
|
||||
|
||||
public function assets(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCardAsset::class, 'asset_pack_id');
|
||||
}
|
||||
}
|
||||
54
app/Models/NovaCardBackground.php
Normal file
54
app/Models/NovaCardBackground.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class NovaCardBackground extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'original_path',
|
||||
'processed_path',
|
||||
'width',
|
||||
'height',
|
||||
'mime_type',
|
||||
'file_size',
|
||||
'sha256',
|
||||
'visibility',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'width' => 'integer',
|
||||
'height' => 'integer',
|
||||
'file_size' => 'integer',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function cards(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCard::class, 'background_image_id');
|
||||
}
|
||||
|
||||
public function processedUrl(): ?string
|
||||
{
|
||||
if (! $this->processed_path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Storage::disk((string) config('nova_cards.storage.public_disk', 'public'))->url($this->processed_path);
|
||||
}
|
||||
}
|
||||
33
app/Models/NovaCardCategory.php
Normal file
33
app/Models/NovaCardCategory.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class NovaCardCategory extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'name',
|
||||
'description',
|
||||
'active',
|
||||
'order_num',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'active' => 'boolean',
|
||||
'order_num' => 'integer',
|
||||
];
|
||||
|
||||
public function cards(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCard::class, 'category_id');
|
||||
}
|
||||
}
|
||||
65
app/Models/NovaCardChallenge.php
Normal file
65
app/Models/NovaCardChallenge.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class NovaCardChallenge extends Model
|
||||
{
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
public const STATUS_ARCHIVED = 'archived';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'slug',
|
||||
'title',
|
||||
'description',
|
||||
'prompt',
|
||||
'rules_json',
|
||||
'status',
|
||||
'official',
|
||||
'featured',
|
||||
'winner_card_id',
|
||||
'entries_count',
|
||||
'starts_at',
|
||||
'ends_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'rules_json' => 'array',
|
||||
'official' => 'boolean',
|
||||
'featured' => 'boolean',
|
||||
'entries_count' => 'integer',
|
||||
'starts_at' => 'datetime',
|
||||
'ends_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function winnerCard(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCard::class, 'winner_card_id');
|
||||
}
|
||||
|
||||
public function entries(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCardChallengeEntry::class, 'challenge_id');
|
||||
}
|
||||
|
||||
public function cards(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(NovaCard::class, 'nova_card_challenge_entries', 'challenge_id', 'card_id')
|
||||
->withPivot(['user_id', 'status', 'note'])
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
42
app/Models/NovaCardChallengeEntry.php
Normal file
42
app/Models/NovaCardChallengeEntry.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class NovaCardChallengeEntry extends Model
|
||||
{
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
public const STATUS_HIDDEN = 'hidden';
|
||||
public const STATUS_REJECTED = 'rejected';
|
||||
|
||||
public const STATUS_SUBMITTED = 'submitted';
|
||||
public const STATUS_FEATURED = 'featured';
|
||||
public const STATUS_WINNER = 'winner';
|
||||
|
||||
protected $fillable = [
|
||||
'challenge_id',
|
||||
'card_id',
|
||||
'user_id',
|
||||
'status',
|
||||
'note',
|
||||
];
|
||||
|
||||
public function challenge(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCardChallenge::class, 'challenge_id');
|
||||
}
|
||||
|
||||
public function card(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCard::class, 'card_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
68
app/Models/NovaCardCollection.php
Normal file
68
app/Models/NovaCardCollection.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
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\Support\Facades\Route;
|
||||
|
||||
class NovaCardCollection extends Model
|
||||
{
|
||||
public const VISIBILITY_PRIVATE = 'private';
|
||||
public const VISIBILITY_PUBLIC = 'public';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'slug',
|
||||
'name',
|
||||
'description',
|
||||
'visibility',
|
||||
'official',
|
||||
'featured',
|
||||
'cards_count',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'official' => 'boolean',
|
||||
'featured' => 'boolean',
|
||||
'cards_count' => 'integer',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCardCollectionItem::class, 'collection_id');
|
||||
}
|
||||
|
||||
public function cards(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(NovaCard::class, 'nova_card_collection_items', 'collection_id', 'card_id')
|
||||
->withPivot(['note', 'sort_order'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function isPubliclyVisible(): bool
|
||||
{
|
||||
return $this->official || $this->visibility === self::VISIBILITY_PUBLIC;
|
||||
}
|
||||
|
||||
public function publicUrl(): ?string
|
||||
{
|
||||
if (! $this->exists || ! Route::has('cards.collections.show')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return route('cards.collections.show', [
|
||||
'slug' => $this->slug,
|
||||
'id' => $this->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
32
app/Models/NovaCardCollectionItem.php
Normal file
32
app/Models/NovaCardCollectionItem.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class NovaCardCollectionItem extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'collection_id',
|
||||
'card_id',
|
||||
'note',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function collection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCardCollection::class, 'collection_id');
|
||||
}
|
||||
|
||||
public function card(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCard::class, 'card_id');
|
||||
}
|
||||
}
|
||||
45
app/Models/NovaCardComment.php
Normal file
45
app/Models/NovaCardComment.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class NovaCardComment extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'card_id',
|
||||
'user_id',
|
||||
'parent_id',
|
||||
'body',
|
||||
'rendered_body',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function card(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCard::class, 'card_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function replies(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id')->orderBy('created_at');
|
||||
}
|
||||
}
|
||||
47
app/Models/NovaCardCreatorPreset.php
Normal file
47
app/Models/NovaCardCreatorPreset.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class NovaCardCreatorPreset extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const TYPE_STYLE = 'style';
|
||||
public const TYPE_LAYOUT = 'layout';
|
||||
public const TYPE_BACKGROUND = 'background';
|
||||
public const TYPE_TYPOGRAPHY = 'typography';
|
||||
public const TYPE_STARTER = 'starter';
|
||||
|
||||
public const TYPES = [
|
||||
self::TYPE_STYLE,
|
||||
self::TYPE_LAYOUT,
|
||||
self::TYPE_BACKGROUND,
|
||||
self::TYPE_TYPOGRAPHY,
|
||||
self::TYPE_STARTER,
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
'preset_type',
|
||||
'config_json',
|
||||
'is_default',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'config_json' => 'array',
|
||||
'is_default' => 'boolean',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
75
app/Models/NovaCardExport.php
Normal file
75
app/Models/NovaCardExport.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class NovaCardExport extends Model
|
||||
{
|
||||
public const TYPE_PREVIEW = 'preview';
|
||||
public const TYPE_HIRES = 'hires';
|
||||
public const TYPE_SQUARE = 'square';
|
||||
public const TYPE_STORY = 'story';
|
||||
public const TYPE_WALLPAPER = 'wallpaper';
|
||||
public const TYPE_OG = 'og';
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_PROCESSING = 'processing';
|
||||
public const STATUS_READY = 'ready';
|
||||
public const STATUS_FAILED = 'failed';
|
||||
|
||||
protected $fillable = [
|
||||
'card_id',
|
||||
'user_id',
|
||||
'export_type',
|
||||
'status',
|
||||
'output_path',
|
||||
'width',
|
||||
'height',
|
||||
'format',
|
||||
'options_json',
|
||||
'ready_at',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'options_json' => 'array',
|
||||
'width' => 'integer',
|
||||
'height' => 'integer',
|
||||
'ready_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function card(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCard::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function outputUrl(): ?string
|
||||
{
|
||||
if (! $this->output_path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Storage::disk((string) config('nova_cards.storage.public_disk', 'public'))->url($this->output_path);
|
||||
}
|
||||
|
||||
public function isReady(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_READY;
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at !== null && $this->expires_at->isPast();
|
||||
}
|
||||
}
|
||||
30
app/Models/NovaCardReaction.php
Normal file
30
app/Models/NovaCardReaction.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class NovaCardReaction extends Model
|
||||
{
|
||||
public const TYPE_LIKE = 'like';
|
||||
public const TYPE_FAVORITE = 'favorite';
|
||||
|
||||
protected $fillable = [
|
||||
'card_id',
|
||||
'user_id',
|
||||
'type',
|
||||
];
|
||||
|
||||
public function card(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCard::class, 'card_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
25
app/Models/NovaCardTag.php
Normal file
25
app/Models/NovaCardTag.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class NovaCardTag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'name',
|
||||
];
|
||||
|
||||
public function cards(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(NovaCard::class, 'nova_card_tag_relation', 'tag_id', 'card_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
40
app/Models/NovaCardTemplate.php
Normal file
40
app/Models/NovaCardTemplate.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class NovaCardTemplate extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'name',
|
||||
'description',
|
||||
'preview_image',
|
||||
'config_json',
|
||||
'supported_formats',
|
||||
'active',
|
||||
'official',
|
||||
'order_num',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'config_json' => 'array',
|
||||
'supported_formats' => 'array',
|
||||
'active' => 'boolean',
|
||||
'official' => 'boolean',
|
||||
'order_num' => 'integer',
|
||||
];
|
||||
|
||||
public function cards(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCard::class, 'template_id');
|
||||
}
|
||||
}
|
||||
35
app/Models/NovaCardVersion.php
Normal file
35
app/Models/NovaCardVersion.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class NovaCardVersion extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'card_id',
|
||||
'user_id',
|
||||
'version_number',
|
||||
'label',
|
||||
'snapshot_hash',
|
||||
'snapshot_json',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'version_number' => 'integer',
|
||||
'snapshot_json' => 'array',
|
||||
];
|
||||
|
||||
public function card(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NovaCard::class, 'card_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
use App\Models\ReportHistory;
|
||||
|
||||
class Report extends Model
|
||||
{
|
||||
@@ -17,10 +20,27 @@ class Report extends Model
|
||||
'reason',
|
||||
'details',
|
||||
'status',
|
||||
'moderator_note',
|
||||
'last_moderated_by_id',
|
||||
'last_moderated_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'last_moderated_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function reporter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'reporter_id');
|
||||
}
|
||||
|
||||
public function lastModeratedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'last_moderated_by_id');
|
||||
}
|
||||
|
||||
public function historyEntries(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReportHistory::class)->latest('id');
|
||||
}
|
||||
}
|
||||
|
||||
45
app/Models/ReportHistory.php
Normal file
45
app/Models/ReportHistory.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReportHistory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $table = 'report_history';
|
||||
|
||||
protected $fillable = [
|
||||
'report_id',
|
||||
'actor_user_id',
|
||||
'action_type',
|
||||
'summary',
|
||||
'note',
|
||||
'before_json',
|
||||
'after_json',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'before_json' => 'array',
|
||||
'after_json' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Report::class);
|
||||
}
|
||||
|
||||
public function actor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'actor_user_id');
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use App\Models\Message;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Achievement;
|
||||
use App\Models\UserAchievement;
|
||||
use App\Models\UserActivity;
|
||||
use App\Models\UserXpLog;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@@ -25,7 +26,10 @@ use Laravel\Scout\Searchable;
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable, SoftDeletes, Searchable;
|
||||
use HasFactory, Notifiable, SoftDeletes;
|
||||
use Searchable {
|
||||
Searchable::bootSearchable as private bootScoutSearchable;
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -59,6 +63,7 @@ class User extends Authenticatable
|
||||
'rank',
|
||||
'password',
|
||||
'role',
|
||||
'nova_featured_creator',
|
||||
'allow_messages_from',
|
||||
];
|
||||
|
||||
@@ -81,6 +86,7 @@ class User extends Authenticatable
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'last_visit_at' => 'datetime',
|
||||
'last_verification_sent_at' => 'datetime',
|
||||
'verification_send_window_started_at' => 'datetime',
|
||||
'verification_send_count_24h' => 'integer',
|
||||
@@ -98,16 +104,32 @@ class User extends Authenticatable
|
||||
'xp' => 'integer',
|
||||
'level' => 'integer',
|
||||
'rank' => 'string',
|
||||
'nova_featured_creator' => 'boolean',
|
||||
'password' => 'hashed',
|
||||
'allow_messages_from' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
public function novaCards(): HasMany
|
||||
{
|
||||
return $this->hasMany(NovaCard::class);
|
||||
}
|
||||
|
||||
public function artworks(): HasMany
|
||||
{
|
||||
return $this->hasMany(Artwork::class);
|
||||
}
|
||||
|
||||
public function collections(): HasMany
|
||||
{
|
||||
return $this->hasMany(Collection::class)->latest('updated_at');
|
||||
}
|
||||
|
||||
public function savedCollectionLists(): HasMany
|
||||
{
|
||||
return $this->hasMany(CollectionSavedList::class, 'user_id')->orderBy('title');
|
||||
}
|
||||
|
||||
public function socialAccounts(): HasMany
|
||||
{
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
@@ -170,6 +192,11 @@ class User extends Authenticatable
|
||||
return $this->hasMany(UserAchievement::class, 'user_id');
|
||||
}
|
||||
|
||||
public function userActivities(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserActivity::class, 'user_id');
|
||||
}
|
||||
|
||||
public function achievements(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Achievement::class, 'user_achievements', 'user_id', 'achievement_id')
|
||||
@@ -298,6 +325,14 @@ class User extends Authenticatable
|
||||
|
||||
// ─── Meilisearch ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* User indexing is handled explicitly through IndexUserJob so unrelated
|
||||
* model writes do not enqueue Scout sync jobs implicitly.
|
||||
*/
|
||||
protected static function bootSearchable(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Only index active users (not soft-deleted, is_active = true).
|
||||
*/
|
||||
|
||||
69
app/Models/UserActivity.php
Normal file
69
app/Models/UserActivity.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserActivity extends Model
|
||||
{
|
||||
protected $table = 'user_activities';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public const CREATED_AT = 'created_at';
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
public const TYPE_UPLOAD = 'upload';
|
||||
public const TYPE_COMMENT = 'comment';
|
||||
public const TYPE_REPLY = 'reply';
|
||||
public const TYPE_LIKE = 'like';
|
||||
public const TYPE_FAVOURITE = 'favourite';
|
||||
public const TYPE_FOLLOW = 'follow';
|
||||
public const TYPE_ACHIEVEMENT = 'achievement';
|
||||
public const TYPE_FORUM_POST = 'forum_post';
|
||||
public const TYPE_FORUM_REPLY = 'forum_reply';
|
||||
|
||||
public const ENTITY_ARTWORK = 'artwork';
|
||||
public const ENTITY_ARTWORK_COMMENT = 'artwork_comment';
|
||||
public const ENTITY_USER = 'user';
|
||||
public const ENTITY_ACHIEVEMENT = 'achievement';
|
||||
public const ENTITY_FORUM_THREAD = 'forum_thread';
|
||||
public const ENTITY_FORUM_POST = 'forum_post';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'type',
|
||||
'entity_type',
|
||||
'entity_id',
|
||||
'meta',
|
||||
'created_at',
|
||||
'hidden_at',
|
||||
'hidden_by',
|
||||
'hidden_reason',
|
||||
'flagged_at',
|
||||
'flagged_by',
|
||||
'flag_reason',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'integer',
|
||||
'entity_id' => 'integer',
|
||||
'meta' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'hidden_at' => 'datetime',
|
||||
'hidden_by' => 'integer',
|
||||
'flagged_at' => 'datetime',
|
||||
'flagged_by' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
37
app/Models/UserNegativeSignal.php
Normal file
37
app/Models/UserNegativeSignal.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserNegativeSignal extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'user_negative_signals';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class);
|
||||
}
|
||||
|
||||
public function tag(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tag::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user