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:
test
2026-09-20 14:50:12 +02:00
parent 4d60a876af
commit 232880e01f
43 changed files with 2896 additions and 565 deletions
+58 -11
View File
@@ -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,