more fixes

This commit is contained in:
2026-03-12 07:22:38 +01:00
parent 547215cbe8
commit 4f576ceb04
226 changed files with 14380 additions and 4453 deletions

View File

@@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
@@ -14,11 +17,18 @@ class ArtworkDownload extends Model
{
protected $table = 'artwork_downloads';
public $timestamps = false;
const CREATED_AT = 'created_at';
const UPDATED_AT = null;
protected $fillable = [
'artwork_id',
'user_id',
'ip',
'ip_address',
'user_agent',
'referer',
];
public function artwork(): BelongsTo

View File

@@ -87,7 +87,7 @@ class ForumCategory extends Model
}
if ($slug !== '') {
return '/images/forum/defaults/' . $slug . '.jpg';
return '/images/forum/' . $slug . '.webp';
}
return $default;

View File

@@ -15,7 +15,18 @@ class ForumPost extends Model
protected $table = 'forum_posts';
protected $fillable = [
'id','thread_id','user_id','content','is_edited','edited_at'
'id',
'thread_id',
'topic_id',
'user_id',
'content',
'is_edited',
'edited_at',
'spam_score',
'quality_score',
'flagged',
'flagged_reason',
'moderation_checked',
];
public $incrementing = true;
@@ -23,6 +34,10 @@ class ForumPost extends Model
protected $casts = [
'is_edited' => 'boolean',
'edited_at' => 'datetime',
'spam_score' => 'integer',
'quality_score' => 'integer',
'flagged' => 'boolean',
'moderation_checked' => 'boolean',
];
public function thread(): BelongsTo

View 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 Notification extends Model
{
use HasFactory;
protected $table = 'notifications';
protected $fillable = [
'user_id',
'type',
'data',
'read_at',
];
protected $casts = [
'data' => 'array',
'read_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function markAsRead(): void
{
if ($this->read_at === null) {
$this->forceFill(['read_at' => now()])->save();
}
}
}

View File

@@ -4,11 +4,16 @@ declare(strict_types=1);
namespace App\Models;
use App\Models\StoryLike;
use App\Models\StoryView;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model;
/**
* Story editorial content replacing the legacy Interviews module.
* Creator Story model.
*
* @property int $id
* @property string $slug
@@ -16,10 +21,10 @@ use Illuminate\Database\Eloquent\Model;
* @property string|null $excerpt
* @property string|null $content
* @property string|null $cover_image
* @property int|null $author_id
* @property int|null $creator_id
* @property int $views
* @property bool $featured
* @property string $status draft|published
* @property string $status draft|pending_review|published|scheduled|archived|rejected
* @property \Carbon\Carbon|null $published_at
* @property int|null $legacy_interview_id
*/
@@ -35,38 +40,81 @@ class Story extends Model
'excerpt',
'content',
'cover_image',
'author_id',
'creator_id',
'story_type',
'reading_time',
'views',
'likes_count',
'comments_count',
'featured',
'status',
'published_at',
'scheduled_for',
'meta_title',
'meta_description',
'canonical_url',
'og_image',
'submitted_for_review_at',
'reviewed_at',
'reviewed_by_id',
'rejected_reason',
'legacy_interview_id',
];
protected $casts = [
'featured' => 'boolean',
'published_at' => 'datetime',
'scheduled_for' => 'datetime',
'submitted_for_review_at' => 'datetime',
'reviewed_at' => 'datetime',
'views' => 'integer',
'likes_count' => 'integer',
'comments_count' => 'integer',
'reading_time' => 'integer',
];
// ── Relations ────────────────────────────────────────────────────────
public function author()
public function creator(): BelongsTo
{
return $this->belongsTo(StoryAuthor::class, 'author_id');
return $this->belongsTo(User::class, 'creator_id');
}
public function tags()
// Legacy alias used by older views/controllers.
public function author(): BelongsTo
{
return $this->belongsToMany(StoryTag::class, 'stories_tag_relation', 'story_id', 'tag_id');
return $this->creator();
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(StoryTag::class, 'relation_story_tags', 'story_id', 'tag_id');
}
public function storyViews(): HasMany
{
return $this->hasMany(StoryView::class, 'story_id');
}
public function storyLikes(): HasMany
{
return $this->hasMany(StoryLike::class, 'story_id');
}
// ── Scopes ───────────────────────────────────────────────────────────
public function scopePublished($query)
{
return $query->where('status', 'published')
->where(fn ($q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()));
return $query
->where(function ($q): void {
$q->where('status', 'published')
->orWhere(function ($scheduled): void {
$scheduled->where('status', 'scheduled')
->whereNotNull('published_at')
->where('published_at', '<=', now());
});
})
->where(fn ($q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()));
}
public function scopeFeatured($query)
@@ -95,6 +143,10 @@ class Story extends Model
*/
public function getReadingTimeAttribute(): int
{
if (! empty($this->attributes['reading_time'])) {
return max(1, (int) $this->attributes['reading_time']);
}
$wordCount = str_word_count(strip_tags((string) $this->content));
return max(1, (int) ceil($wordCount / 200));

32
app/Models/StoryLike.php Normal file
View 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 StoryLike extends Model
{
use HasFactory;
public const UPDATED_AT = null;
protected $fillable = [
'story_id',
'user_id',
'created_at',
];
public function story(): BelongsTo
{
return $this->belongsTo(Story::class, 'story_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}

View File

@@ -5,10 +5,11 @@ declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Model;
/**
* Story Tag editorial tag for the Stories system.
* Story tag entity for creator stories.
*
* @property int $id
* @property string $slug
@@ -18,7 +19,7 @@ class StoryTag extends Model
{
use HasFactory;
protected $table = 'stories_tags';
protected $table = 'story_tags';
protected $fillable = [
'slug',
@@ -27,9 +28,9 @@ class StoryTag extends Model
// ── Relations ────────────────────────────────────────────────────────
public function stories()
public function stories(): BelongsToMany
{
return $this->belongsToMany(Story::class, 'stories_tag_relation', 'tag_id', 'story_id');
return $this->belongsToMany(Story::class, 'relation_story_tags', 'tag_id', 'story_id');
}
// ── Accessors ────────────────────────────────────────────────────────

33
app/Models/StoryView.php Normal file
View 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\BelongsTo;
class StoryView extends Model
{
use HasFactory;
public const UPDATED_AT = null;
protected $fillable = [
'story_id',
'user_id',
'ip_address',
'created_at',
];
public function story(): BelongsTo
{
return $this->belongsTo(Story::class, 'story_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}

View File

@@ -11,6 +11,7 @@ use App\Models\SocialAccount;
use App\Models\Conversation;
use App\Models\ConversationParticipant;
use App\Models\Message;
use App\Models\Notification;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@@ -30,6 +31,7 @@ class User extends Authenticatable
protected $fillable = [
'username',
'username_changed_at',
'last_username_change_at',
'onboarding_step',
'name',
'email',
@@ -38,6 +40,10 @@ class User extends Authenticatable
'verification_send_window_started_at',
'is_active',
'needs_password_reset',
'cover_hash',
'cover_ext',
'cover_position',
'trust_score',
'password',
'role',
'allow_messages_from',
@@ -66,7 +72,10 @@ class User extends Authenticatable
'verification_send_window_started_at' => 'datetime',
'verification_send_count_24h' => 'integer',
'username_changed_at' => 'datetime',
'last_username_change_at' => 'datetime',
'deleted_at' => 'datetime',
'cover_position' => 'integer',
'trust_score' => 'integer',
'password' => 'hashed',
'allow_messages_from' => 'string',
];
@@ -139,6 +148,19 @@ class User extends Authenticatable
return $this->hasMany(Message::class, 'sender_id');
}
/**
* Skinbase notifications are keyed by user_id (non-polymorphic table).
*/
public function notifications(): HasMany
{
return $this->hasMany(Notification::class, 'user_id')->latest();
}
public function unreadNotifications(): HasMany
{
return $this->notifications()->whereNull('read_at');
}
/**
* Check if this user allows receiving messages from the given user.
*/
@@ -221,6 +243,11 @@ class User extends Authenticatable
return $this->hasMany(Post::class)->orderByDesc('created_at');
}
public function stories(): HasMany
{
return $this->hasMany(Story::class, 'creator_id')->orderByDesc('published_at');
}
// ─── Meilisearch ──────────────────────────────────────────────────────────
/**

View File

@@ -30,12 +30,22 @@ class UserProfile extends Model
'gender',
'website',
'auto_post_upload',
'email_notifications',
'upload_notifications',
'follower_notifications',
'comment_notifications',
'newsletter',
];
protected $casts = [
'birthdate' => 'date',
'avatar_updated_at'=> 'datetime',
'auto_post_upload' => 'boolean',
'email_notifications' => 'boolean',
'upload_notifications' => 'boolean',
'follower_notifications' => 'boolean',
'comment_notifications' => 'boolean',
'newsletter' => 'boolean',
];
public $timestamps = true;