Merge pull request #10 from okorpheus/add-scopes-to-models

Add scopes to models
This commit is contained in:
Matt 2024-06-30 23:00:24 -05:00 committed by GitHub
commit e79f484bfe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 321 additions and 59 deletions

View File

@ -19,7 +19,7 @@ class EntryController extends Controller
$entries = $entries->sortBy(function ($entry) { $entries = $entries->sortBy(function ($entry) {
return $entry->student->last_name.$entry->student->first_name.$entry->audition->score_order; return $entry->student->last_name.$entry->student->first_name.$entry->audition->score_order;
}); });
$auditions = Audition::deadlineNotPast(); $auditions = Audition::open()->get();
$students = Auth::user()->students; $students = Auth::user()->students;
return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]); return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]);

View File

@ -2,6 +2,8 @@
namespace App\Models; namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -25,11 +27,6 @@ class Audition extends Model
protected $scored_entries_count; //Set by TabulationService protected $scored_entries_count; //Set by TabulationService
public static function deadlineNotPast()
{
return Audition::where('entry_deadline', '>=', now())->get();
}
public function event(): BelongsTo public function event(): BelongsTo
{ {
return $this->belongsTo(Event::class); return $this->belongsTo(Event::class);
@ -50,7 +47,7 @@ class Audition extends Model
return $this->belongsTo(ScoringGuide::class); return $this->belongsTo(ScoringGuide::class);
} }
public function dislpay_fee(): string public function display_fee(): string
{ {
return '$'.number_format($this->entry_fee / 100, 2); return '$'.number_format($this->entry_fee / 100, 2);
} }
@ -134,7 +131,7 @@ class Audition extends Model
} }
/** /**
* @return BelongsToMany|\App\Models\User[] * @return BelongsToMany|User[]
*/ */
public function judges(): array|BelongsToMany public function judges(): array|BelongsToMany
{ {
@ -185,4 +182,35 @@ class Audition extends Model
// remove related auditionFlag where flag_name = $flag // remove related auditionFlag where flag_name = $flag
$this->flags()->where('flag_name', $flag)->delete(); $this->flags()->where('flag_name', $flag)->delete();
} }
public function scopeOpen(Builder $query): void
{
$query->where('entry_deadline', '>=', Carbon::now());
}
public function scopeForSeating(Builder $query): void
{
$query->where('for_seating', 1);
}
public function scopeForAdvancement(Builder $query): void
{
$query->where('for_advancement', 1);
}
public function scopeSeatsPublished(Builder $query): Builder
{
return $query->whereHas('flags', function (Builder $query) {
$query->where('flag_name', 'seats_published');
});
}
public function scopeAdvancementPublished(Builder $query): Builder
{
return $query->whereHas('flags', function (Builder $query) {
$query->where('flag_name', 'advancement_published');
});
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -69,7 +70,7 @@ class Entry extends Model
} }
public function addFlag($flag) public function addFlag($flag): void
{ {
if ($this->hasFlag($flag)) { if ($this->hasFlag($flag)) {
return; return;
@ -78,7 +79,7 @@ class Entry extends Model
$this->flags()->create(['flag_name' => $flag]); $this->flags()->create(['flag_name' => $flag]);
} }
public function removeFlag($flag) public function removeFlag($flag): void
{ {
// remove related auditionFlag where flag_name = $flag // remove related auditionFlag where flag_name = $flag
$this->flags()->where('flag_name', $flag)->delete(); $this->flags()->where('flag_name', $flag)->delete();
@ -100,4 +101,23 @@ class Entry extends Model
{ {
return $this->hasOne(Seat::class); return $this->hasOne(Seat::class);
} }
public function scopeForSeating(Builder $query): void
{
$query->where('for_seating', 1);
}
public function scopeForAdvancement(Builder $query): void
{
$query->where('for_advancement', 1);
}
public function scopeAvailable(Builder $query): void
{
$query->whereDoesntHave('flags', function (Builder $query) {
$query->where('flag_name', 'declined')
->orWhere('flag_name', 'no-show')
->orWhere('flag_name', 'failed-prelim');
});
}
} }

View File

@ -27,6 +27,7 @@ class InvoiceDataServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
if (auditionSetting('fee_structure')) {
$this->app->singleton(InvoiceDataService::class, function ($app) { $this->app->singleton(InvoiceDataService::class, function ($app) {
return match (auditionSetting('fee_structure')) { return match (auditionSetting('fee_structure')) {
'oneFeePerEntry' => new InvoiceOneFeePerEntry($app->make(EntryService::class)), 'oneFeePerEntry' => new InvoiceOneFeePerEntry($app->make(EntryService::class)),
@ -36,3 +37,4 @@ class InvoiceDataServiceProvider extends ServiceProvider
}); });
} }
} }
}

View File

@ -2,6 +2,8 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\Event;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
@ -16,8 +18,44 @@ class AuditionFactory extends Factory
*/ */
public function definition(): array public function definition(): array
{ {
$instruments = [
'Flute',
'Oboe',
'Clarinet',
'Bass Clarinet',
'Contra Clarinet',
'Bassoon',
'Alto Sax',
'Tenor Sax',
'Bari Sax',
'Trumpet',
'Horn',
'Trombone',
'Euphonium',
'Tuba',
'String Bass',
'Percussion',
];
$event = Event::factory()->create();
return [ return [
// 'event_id' => $event->id,
'name' => $this->faker->randomElement($instruments).$this->faker->randomNumber(1),
'score_order' => 1,
'entry_deadline' => Carbon::tomorrow(),
'entry_fee' => 1000,
'minimum_grade' => 7,
'maximum_grade' => 12,
'for_seating' => 1,
'for_advancement' => 1,
]; ];
} }
public function closed(?Carbon $entryDeadline = null): self
{
return $this->state(
fn (array $attributes) => ['entry_deadline' => $entryDeadline ?? Carbon::yesterday()]
);
}
} }

View File

@ -2,6 +2,8 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\Audition;
use App\Models\Student;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
@ -16,9 +18,28 @@ class EntryFactory extends Factory
*/ */
public function definition(): array public function definition(): array
{ {
$student = Student::factory()->create();
$audition = Audition::factory()->create();
return [ return [
'student_id' => 3, 'student_id' => $student->id,
'audition_id' =>3 'audition_id' => $audition->id,
'draw_number' => null,
'for_seating' => 1,
'for_advancement' => 1,
]; ];
} }
public function seatingOnly(): self
{
return $this->state(
fn (array $attributes) => ['for_advancement' => 0]
);
}
public function advanceOnly(): self
{
return $this->state(
fn (array $attributes) => ['for_seating' => 0]
);
}
} }

View File

@ -17,7 +17,7 @@ class EventFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'name' => 'Concert Band Auditions' 'name' => $this->faker->name(),
]; ];
} }
} }

View File

@ -2,6 +2,7 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\School;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
@ -16,7 +17,9 @@ class StudentFactory extends Factory
*/ */
public function definition(): array public function definition(): array
{ {
$school = School::factory()->create();
return [ return [
'school_id' => $school->id,
'first_name' => fake()->firstName(), 'first_name' => fake()->firstName(),
'last_name' => fake()->lastName(), 'last_name' => fake()->lastName(),
'grade' => rand(7,12), 'grade' => rand(7,12),

View File

@ -23,7 +23,11 @@ class UserFactory extends Factory
*/ */
public function definition(): array public function definition(): array
{ {
$judingPrefPossibilities = ['woodwinds','flute','clarinet','saxophones', 'low clarinets','oboe','bassoon','double reeds','brass','low brass','trumpet','trombone','horn','tuba','euphonium','percussion']; $judingPrefPossibilities = [
'woodwinds', 'flute', 'clarinet', 'saxophones', 'low clarinets', 'oboe', 'bassoon', 'double reeds', 'brass',
'low brass', 'trumpet', 'trombone', 'horn', 'tuba', 'euphonium', 'percussion',
];
return [ return [
'first_name' => fake()->firstName(), 'first_name' => fake()->firstName(),
'last_name' => fake()->lastName(), 'last_name' => fake()->lastName(),
@ -46,4 +50,18 @@ class UserFactory extends Factory
'email_verified_at' => null, 'email_verified_at' => null,
]); ]);
} }
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'is_admin' => 1,
]);
}
public function tab(): static
{
return $this->state(fn (array $attributes) => [
'is_tab' => 1,
]);
}
} }

View File

@ -27,11 +27,12 @@
<div x-data="sortableList()" x-init="init"> <div x-data="sortableList()" x-init="init">
<x-table.body id="sortable-list"> <x-table.body id="sortable-list">
@foreach($auditions as $audition) @foreach($auditions as $audition)
<tr data-id="{{ $audition->id }}" @dblclick="window.location.href='/admin/auditions/{{ $audition->id }}/edit'"> <tr data-id="{{ $audition->id }}"
@dblclick="window.location.href='/admin/auditions/{{ $audition->id }}/edit'">
<x-table.td>{{ $audition->event->name }}</x-table.td> <x-table.td>{{ $audition->event->name }}</x-table.td>
<x-table.td>{{ $audition->name }}</x-table.td> <x-table.td>{{ $audition->name }}</x-table.td>
<x-table.td>{{ $audition->entry_deadline }}</x-table.td> <x-table.td>{{ $audition->entry_deadline }}</x-table.td>
<x-table.td>{{ $audition->dislpay_fee() }}</x-table.td> <x-table.td>{{ $audition->display_fee() }}</x-table.td>
<x-table.td>{{ $audition->minimum_grade }} - {{ $audition->maximum_grade }}</x-table.td> <x-table.td>{{ $audition->minimum_grade }} - {{ $audition->maximum_grade }}</x-table.td>
@if(auditionSetting('advanceTo')) @if(auditionSetting('advanceTo'))
<x-table.td> <x-table.td>
@ -80,8 +81,7 @@
.then(data => { .then(data => {
if (data.status === 'success') { if (data.status === 'success') {
console.log('Order updated successfully'); console.log('Order updated successfully');
} } else {
else {
console.log(data.status); console.log(data.status);
} }
}); });

View File

@ -16,9 +16,7 @@
<x-layout.app> <x-layout.app>
<x-slot:page_title>Test Page</x-slot:page_title> <x-slot:page_title>Test Page</x-slot:page_title>
@php @php
dump($totalFees); dump(Audition::open()->get());
dump($lines);
@endphp @endphp

View File

@ -0,0 +1,65 @@
<?php
use App\Models\Audition;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('only returns open auditions for open scope', function () {
// Arrange
$openAudition = Audition::factory()->create();
Audition::factory()->closed()->create();
// Act & Assert
expect(Audition::open()->get())
->toHaveCount(1)
->first()->id->toEqual($openAudition->id);
});
it('only returns auditions for seating with forSeating scope', function () {
// Arrange
Audition::factory(['for_seating' => 0])->create();
$seatingAudition = Audition::factory()->create();
// Act & Assert
expect(Audition::forSeating()->get())
->toHaveCount(1)
->first()->id->toEqual($seatingAudition->id);
});
it('only returns auditions for advancement with for forAdvancement scope', function () {
// Arrange
Audition::factory(['for_advancement' => 0])->create();
$advancementAudition = Audition::factory()->create();
// Act & Assert
expect(Audition::forAdvancement()->get())
->toHaveCount(1)
->first()->id->toEqual($advancementAudition->id);
});
it('only returns published auditions for seatsPublished scope', function () {
// Arrange
Audition::factory()->create();
$published = Audition::factory()->create();
$published->addFlag('seats_published');
// Act & Assert
expect(Audition::seatsPublished()->get())
->toHaveCount(1)
->first()->id->toEqual($published->id);
});
it('only returns published advancement auditions for advancementPublished scope', function () {
// Arrange
Audition::factory()->create();
$published = Audition::factory()->create();
$published->addFlag('advancement_published');
// Act & Assert
expect(Audition::advancementPublished()->get())
->toHaveCount(1)
->first()->id->toEqual($published->id);
});

View File

@ -0,0 +1,46 @@
<?php
use App\Models\Entry;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('only returns entries for seating with forSeating scope', function () {
// Arrange
Entry::factory()->advanceOnly()->create();
$seatingEntry = Entry::factory()->create();
// Act & Assert
expect(Entry::forSeating()->get())
->toHaveCount(1)
->first()->id->toEqual($seatingEntry->id);
});
it('only returns entries for advancement with for forAdvancement scope', function () {
// Arrange
Entry::factory()->seatingOnly()->create();
$advancementEntry = Entry::factory()->create();
// Act & Assert
expect(Entry::forAdvancement()->get())
->toHaveCount(1)
->first()->id->toEqual($advancementEntry->id);
});
it('only returns entries that do not have a declined, no-show, or failed-prelim flag with available scope',
function () {
// Arrange
$availableEntry = Entry::factory()->create();
$declinedEntry = Entry::factory()->create();
$noShowEntry = Entry::factory()->create();
$failedPrelimEntry = Entry::factory()->create();
$declinedEntry->addFlag('declined');
$noShowEntry->addFlag('no-show');
$failedPrelimEntry->addFlag('failed-prelim');
// Act & Assert
expect(Entry::available()->get())
->toHaveCount(1)
->first()->id->toEqual($availableEntry->id);
});

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\get; use function Pest\Laravel\get;
@ -16,7 +17,7 @@ it('shows appropriate screens when not logged in', function () {
]); ]);
}); });
it('shows a registration page', function () { it('shows a registration page only if not logged in', function () {
// Act & Assert // Act & Assert
get('/register') get('/register')
->assertStatus(200) ->assertStatus(200)
@ -24,13 +25,35 @@ it('shows a registration page', function () {
'Registration Code', 'Registration Code',
'Email address', 'Email address',
]); ]);
$user = User::factory()->create();
$this->actingAs($user);
get('/register')
->assertStatus(302)
->assertRedirect(route('dashboard'));
}); });
it('shows a login page', function () { it('shows a login page only if not logged in', function () {
get('/login') get('/login')
->assertStatus(200) ->assertStatus(200)
->assertSeeText([ ->assertSeeText([
'Log In', 'Log In',
'Click here to register', 'Click here to register',
]); ]);
$user = User::factory()->create();
$this->actingAs($user);
get('/register')
->assertStatus(302)
->assertRedirect(route('dashboard'));
});
it('shows dashboard only if logged in', function () {
get(route('dashboard'))
->assertStatus(302)
->assertRedirect(route('home'));
$user = User::factory()->create();
$this->actingAs($user);
get(route('dashboard'))
->assertStatus(200)
->assertSeeText('My School')
->assertSeeText('Dashboard');
}); });