Multiple Mod Authors

The mod-to-user relationship has been modified to be a many-to-many relationship. Mods can now have multiple authors, and an author can have multiple mods.
This commit is contained in:
Refringe 2024-07-04 22:10:48 -04:00
parent a1504fe622
commit c1a58e29dd
Signed by: Refringe
SSH Key Fingerprint: SHA256:t865XsQpfTeqPRBMN2G6+N8wlDjkgUCZF3WGW6O9N/k
17 changed files with 92 additions and 84 deletions

View File

@ -30,7 +30,7 @@ class ModController extends Controller
$mod = Mod::select()
->withLatestSptVersion()
->withTotalDownloads()
->with('user:id,name')
->with('users:id,name')
->with('license:id,name,link')
->find($modId);

View File

@ -20,22 +20,27 @@ class ModResource extends JsonResource
'attributes' => [
'name' => $this->name,
'slug' => $this->slug,
'teaser' => $this->teaser,
'description' => $this->when(
$request->routeIs('api.v0.mods.show'),
$this->description
),
'source_code_link' => $this->source_code_link,
'user_id' => $this->user_id,
'license_id' => $this->license_id,
'source_code_link' => $this->source_code_link,
'featured' => $this->featured,
'contains_ai_content' => $this->contains_ai_content,
'contains_ads' => $this->contains_ads,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
],
'relationships' => [
'user' => [
'data' => [
'users' => [
'data' => $this->users->map(fn ($user) => [
'type' => 'user',
'id' => $this->user_id,
],
// TODO: Provide 'links.self' to user profile:
'id' => $user->id,
])->toArray(),
// TODO: Provide 'links.self' to user profile
//'links' => ['self' => '#'],
],
'license' => [
@ -45,11 +50,11 @@ class ModResource extends JsonResource
],
],
],
'included' => [
new UserResource($this->user),
// TODO: Provide 'included' data for attached 'license':
//new LicenseResource($this->license),
],
'included' => $this->users->map(fn ($user) => new UserResource($user)),
// TODO: Provide 'included' data for attached 'license':
//new LicenseResource($this->license)
'links' => [
'self' => route('mod.show', [
'mod' => $this->id,

View File

@ -16,7 +16,6 @@ class ModResource extends JsonResource
'slug' => $this->slug,
'description' => $this->description,
'source_code_link' => $this->source_code_link,
'user_id' => $this->user_id,
'license_id' => $this->license_id,
'license' => new LicenseResource($this->whenLoaded('license')),
'created_at' => $this->created_at,

View File

@ -11,11 +11,6 @@ class License extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'link',
];
public function mods(): HasMany
{
return $this->hasMany(Mod::class);

View File

@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
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;
@ -20,25 +21,18 @@ class Mod extends Model
{
use HasFactory, Searchable, SoftDeletes;
protected $fillable = [
'user_id',
'name',
'slug',
'teaser',
'description',
'license_id',
'source_code_link',
];
protected static function booted(): void
{
// Apply the global scope to exclude disabled mods.
static::addGlobalScope(new DisabledScope);
}
public function user(): BelongsTo
/**
* The users that belong to the mod.
*/
public function users(): BelongsToMany
{
return $this->belongsTo(User::class);
return $this->belongsToMany(User::class);
}
public function license(): BelongsTo
@ -155,6 +149,16 @@ class Mod extends Model
};
}
protected function casts(): array
{
return [
'featured' => 'boolean',
'contains_ai_content' => 'boolean',
'contains_ads' => 'boolean',
'disabled' => 'boolean',
];
}
/**
* Ensure the slug is always lower case when retrieved and slugified when saved.
*/

View File

@ -12,15 +12,6 @@ class ModVersion extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'mod_id',
'version',
'description',
'spt_version_id',
'virus_total_link',
'downloads',
];
protected static function booted(): void
{
static::addGlobalScope(new DisabledScope);

View File

@ -11,11 +11,6 @@ class SptVersion extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'version',
'color_class',
];
public function modVersions(): HasMany
{
return $this->hasMany(ModVersion::class);

View File

@ -5,7 +5,7 @@ namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
@ -25,12 +25,6 @@ class User extends Authenticatable implements MustVerifyEmail
use Searchable;
use TwoFactorAuthenticatable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
@ -42,9 +36,9 @@ class User extends Authenticatable implements MustVerifyEmail
'profile_photo_url',
];
public function mods(): HasMany
public function mods(): BelongsToMany
{
return $this->hasMany(Mod::class);
return $this->belongsToMany(Mod::class);
}
public function toSearchableArray(): array

View File

@ -10,13 +10,6 @@ class UserRole extends Model
{
use HasFactory;
protected $fillable = [
'name',
'short_name',
'description',
'color_class',
];
public function users(): HasMany
{
return $this->hasMany(User::class);

View File

@ -3,6 +3,7 @@
namespace App\Providers;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
@ -21,6 +22,9 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
// Allow mass assignment for all models. Be careful!
Model::unguard();
// This gate determines who can access the Pulse dashboard.
Gate::define('viewPulse', function (User $user) {
return $user->isAdmin();

View File

@ -24,10 +24,10 @@ class ModListSection extends Component
private function fetchFeaturedMods(): Collection
{
return Mod::select(['id', 'user_id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
->withLatestSptVersion()
->withTotalDownloads()
->with('user:id,name')
->with('users:id,name')
->where('featured', true)
->latest()
->limit(6)
@ -36,10 +36,10 @@ class ModListSection extends Component
private function fetchLatestMods(): Collection
{
return Mod::select(['id', 'user_id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
->withLatestSptVersion()
->withTotalDownloads()
->with('user:id,name')
->with('users:id,name')
->latest()
->limit(6)
->get();
@ -47,15 +47,22 @@ class ModListSection extends Component
private function fetchUpdatedMods(): Collection
{
return Mod::select(['id', 'user_id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
->withLastUpdatedVersion()
->withTotalDownloads()
->with('user:id,name')
->with('users:id,name')
->latest()
->limit(6)
->get();
}
public function render(): View
{
return view('components.mod-list-section', [
'sections' => $this->getSections(),
]);
}
public function getSections(): array
{
return [
@ -76,11 +83,4 @@ class ModListSection extends Component
],
];
}
public function render(): View
{
return view('components.mod-list-section', [
'sections' => $this->getSections(),
]);
}
}

View File

@ -4,7 +4,6 @@ namespace Database\Factories;
use App\Models\License;
use App\Models\Mod;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use Random\RandomException;
@ -18,10 +17,10 @@ class ModFactory extends Factory
*/
public function definition(): array
{
$name = fake()->catchPhrase();
return [
'user_id' => User::factory(),
'name' => $name,
'slug' => Str::slug($name),
'teaser' => fake()->sentence(),

View File

@ -1,7 +1,6 @@
<?php
use App\Models\License;
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
@ -16,10 +15,6 @@ return new class extends Migration
->nullable()
->default(null)
->unique();
$table->foreignIdFor(User::class)
->constrained('users')
->cascadeOnDelete()
->cascadeOnUpdate();
$table->string('name');
$table->string('slug');
$table->string('teaser');

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('mod_user', function (Blueprint $table) {
$table->id();
$table->foreignId('mod_id')->constrained()->cascadeOnUpdate()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnUpdate()->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('mod_user');
}
};

View File

@ -38,7 +38,12 @@ class DatabaseSeeder extends Seeder
$users = User::factory(100)->create();
// Add 200 mods, assigning them to the users we just created.
$mods = Mod::factory(200)->recycle([$users, $licenses])->create();
$allUsers = $users->merge([$administrator, $moderator]);
$mods = Mod::factory(200)->recycle([$licenses])->create();
foreach ($mods as $mod) {
$userIds = $allUsers->random(rand(1, 3))->pluck('id')->toArray();
$mod->users()->attach($userIds);
}
// Add 1000 mod versions, assigning them to the mods we just created.
ModVersion::factory(1000)->recycle([$mods, $spt_versions])->create();

View File

@ -24,7 +24,7 @@
{{ $mod->{$versionScope}->sptVersion->version }}
</span>
</div>
<p class="text-sm italic text-slate-600 dark:text-gray-200">By {{ $mod->user->name }}</p>
<p class="text-sm italic text-slate-600 dark:text-gray-200">By {{ $mod->users->pluck('name')->implode(', ') }}</p>
<p class="mt-2 text-slate-500 dark:text-gray-300">{{ $mod->teaser }}</p>
</div>
<x-mod-list-stats :mod="$mod" :modVersion="$mod->{$versionScope}"/>

View File

@ -25,7 +25,7 @@
{{ $mod->latestSptVersion->version }}
</span>
</h2>
<p>{{ __('Created by') }} {{ $mod->user->name }}</p>
<p>{{ __('Created by') }} {{ $mod->users->pluck('name')->implode(', ') }}</p>
<p>{{ $mod->latestSptVersion->sptVersion->version }} {{ __('Compatible') }}</p>
<p>{{ $mod->total_downloads }} {{ __('Downloads') }}</p>
</div>