Allow artworks to have one primary category and extra secondaries.
Keep artwork_category as membership, store a deterministic primary_category_id, and let upload plus studio pick up to five additional same-type categories without changing canonical URLs.
This commit is contained in:
+58
-11
@@ -66,6 +66,9 @@ class Artwork extends Model
|
||||
'description',
|
||||
'file_name',
|
||||
'file_path',
|
||||
'download_status',
|
||||
'download_source',
|
||||
'download_checked_at',
|
||||
'hash',
|
||||
'file_ext',
|
||||
'thumb_ext',
|
||||
@@ -81,6 +84,10 @@ class Artwork extends Model
|
||||
'is_public',
|
||||
'visibility',
|
||||
'is_approved',
|
||||
'approval_source',
|
||||
'moderated_at',
|
||||
'moderated_by',
|
||||
'moderation_note',
|
||||
'is_mature',
|
||||
'maturity_level',
|
||||
'maturity_source',
|
||||
@@ -117,6 +124,7 @@ class Artwork extends Model
|
||||
'description_source',
|
||||
'tags_source',
|
||||
'category_source',
|
||||
'primary_category_id',
|
||||
// Versioning
|
||||
'current_version_id',
|
||||
'version_count',
|
||||
@@ -132,6 +140,7 @@ class Artwork extends Model
|
||||
'is_public' => 'boolean',
|
||||
'visibility' => 'string',
|
||||
'is_approved' => 'boolean',
|
||||
'moderated_at' => 'datetime',
|
||||
'is_mature' => 'boolean',
|
||||
'maturity_level' => 'string',
|
||||
'maturity_source' => 'string',
|
||||
@@ -171,6 +180,7 @@ class Artwork extends Model
|
||||
'category_source' => 'string',
|
||||
'version_updated_at' => 'datetime',
|
||||
'requires_reapproval' => 'boolean',
|
||||
'download_checked_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -449,6 +459,32 @@ class Artwork extends Model
|
||||
return $this->belongsToMany(Category::class, 'artwork_category', 'artwork_id', 'category_id');
|
||||
}
|
||||
|
||||
public function primaryCategory(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class, 'primary_category_id');
|
||||
}
|
||||
|
||||
public function resolvedPrimaryCategory(): ?Category
|
||||
{
|
||||
if ($this->relationLoaded('primaryCategory') && $this->getRelation('primaryCategory') instanceof Category) {
|
||||
return $this->getRelation('primaryCategory');
|
||||
}
|
||||
|
||||
$primaryId = $this->primary_category_id !== null ? (int) $this->primary_category_id : 0;
|
||||
if ($primaryId > 0 && $this->relationLoaded('categories')) {
|
||||
$match = $this->categories->firstWhere('id', $primaryId);
|
||||
if ($match instanceof Category) {
|
||||
return $match;
|
||||
}
|
||||
}
|
||||
|
||||
if ($primaryId > 0) {
|
||||
return $this->primaryCategory;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function collections(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Collection::class, 'collection_artwork', 'artwork_id', 'collection_id')
|
||||
@@ -559,19 +595,25 @@ class Artwork extends Model
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$this->loadMissing(['user', 'group', 'tags', 'categories.contentType', 'stats', 'awardStat']);
|
||||
$this->loadMissing(['user', 'group', 'tags', 'categories.contentType', 'primaryCategory.contentType', 'stats', 'awardStat']);
|
||||
|
||||
$stat = $this->stats;
|
||||
$awardStat = $this->awardStat;
|
||||
$publishedSortAt = $this->published_at ?? $this->created_at;
|
||||
$sortedCategories = $this->categories->sortBy(
|
||||
fn ($category) => sprintf(
|
||||
'%010d|%s|%010d',
|
||||
(int) ($category->sort_order ?? 999999999),
|
||||
strtolower((string) ($category->name ?? '')),
|
||||
(int) ($category->id ?? 0)
|
||||
)
|
||||
)->values();
|
||||
$primaryCategory = $this->resolvedPrimaryCategory();
|
||||
$sortedCategories = $this->categories
|
||||
->sortBy(function ($category) use ($primaryCategory) {
|
||||
$isPrimary = $primaryCategory && (int) $category->id === (int) $primaryCategory->id;
|
||||
|
||||
return sprintf(
|
||||
'%d|%010d|%s|%010d',
|
||||
$isPrimary ? 0 : 1,
|
||||
(int) ($category->sort_order ?? 999999999),
|
||||
strtolower((string) ($category->name ?? '')),
|
||||
(int) ($category->id ?? 0)
|
||||
);
|
||||
})
|
||||
->values();
|
||||
|
||||
// Orientation derived from pixel dimensions
|
||||
$orientation = 'square';
|
||||
@@ -588,8 +630,6 @@ class Artwork extends Model
|
||||
? $this->width.'x'.$this->height
|
||||
: '';
|
||||
|
||||
// Primary category slug follows the same sort_order-first semantics used by page presenters.
|
||||
$primaryCategory = $sortedCategories->first();
|
||||
$categorySlugs = $sortedCategories
|
||||
->pluck('slug')
|
||||
->filter()
|
||||
@@ -619,6 +659,13 @@ class Artwork extends Model
|
||||
'author_name' => $this->group?->name ?? $this->user?->name ?? 'Skinbase',
|
||||
'published_as_type' => $this->publishedAsType(),
|
||||
'category' => $category,
|
||||
'primary_category_id' => $primaryCategory?->id ? (int) $primaryCategory->id : null,
|
||||
'category_ids' => $this->categories
|
||||
->pluck('id')
|
||||
->map(static fn ($id): int => (int) $id)
|
||||
->unique()
|
||||
->values()
|
||||
->all(),
|
||||
'categories' => $categorySlugs,
|
||||
'content_type' => $content_type,
|
||||
'content_types' => $contentTypeSlugs,
|
||||
|
||||
Reference in New Issue
Block a user