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) {
return $entry->student->last_name.$entry->student->first_name.$entry->audition->score_order;
});
$auditions = Audition::deadlineNotPast();
$auditions = Audition::open()->get();
$students = Auth::user()->students;
return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]);

View File

@ -2,6 +2,8 @@
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -25,11 +27,6 @@ class Audition extends Model
protected $scored_entries_count; //Set by TabulationService
public static function deadlineNotPast()
{
return Audition::where('entry_deadline', '>=', now())->get();
}
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
@ -50,7 +47,7 @@ class Audition extends Model
return $this->belongsTo(ScoringGuide::class);
}
public function dislpay_fee(): string
public function display_fee(): string
{
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
{
@ -185,4 +182,35 @@ class Audition extends Model
// remove related auditionFlag where flag_name = $flag
$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;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
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)) {
return;
@ -78,7 +79,7 @@ class Entry extends Model
$this->flags()->create(['flag_name' => $flag]);
}
public function removeFlag($flag)
public function removeFlag($flag): void
{
// remove related auditionFlag where flag_name = $flag
$this->flags()->where('flag_name', $flag)->delete();
@ -100,4 +101,23 @@ class Entry extends Model
{
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
{
if (auditionSetting('fee_structure')) {
$this->app->singleton(InvoiceDataService::class, function ($app) {
return match (auditionSetting('fee_structure')) {
'oneFeePerEntry' => new InvoiceOneFeePerEntry($app->make(EntryService::class)),
@ -35,4 +36,5 @@ class InvoiceDataServiceProvider extends ServiceProvider
};
});
}
}
}

View File

@ -2,6 +2,8 @@
namespace Database\Factories;
use App\Models\Event;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
@ -16,8 +18,44 @@ class AuditionFactory extends Factory
*/
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 [
//
'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;
use App\Models\Audition;
use App\Models\Student;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
@ -16,9 +18,28 @@ class EntryFactory extends Factory
*/
public function definition(): array
{
$student = Student::factory()->create();
$audition = Audition::factory()->create();
return [
'student_id' => 3,
'audition_id' =>3
'student_id' => $student->id,
'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
{
return [
'name' => 'Concert Band Auditions'
'name' => $this->faker->name(),
];
}
}

View File

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

View File

@ -23,7 +23,11 @@ class UserFactory extends Factory
*/
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 [
'first_name' => fake()->firstName(),
'last_name' => fake()->lastName(),
@ -46,4 +50,18 @@ class UserFactory extends Factory
'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,25 +27,26 @@
<div x-data="sortableList()" x-init="init">
<x-table.body id="sortable-list">
@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->name }}</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>
@if(auditionSetting('advanceTo'))
<x-table.td>
@if($audition->for_seating)
<x-icons.checkmark color="green" />
<x-icons.checkmark color="green"/>
@else
<x-icons.circle-slash-no color="red" />
<x-icons.circle-slash-no color="red"/>
@endif
</x-table.td>
<x-table.td>
@if($audition->for_advancement)
<x-icons.checkmark color="green" />
<x-icons.checkmark color="green"/>
@else
<x-icons.circle-slash-no color="red" />
<x-icons.circle-slash-no color="red"/>
@endif
</x-table.td>
@endif
@ -58,7 +59,7 @@
</x-card.card>
<div class="mt-3 mx-3">
{{-- {{ $auditions->links('vendor.pagination.simple-audition') }}--}}
{{-- {{ $auditions->links('vendor.pagination.simple-audition') }}--}}
</div>
<script>
@ -75,13 +76,12 @@
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ order })
body: JSON.stringify({order})
}).then(response => response.json())
.then(data => {
if (data.status === 'success') {
console.log('Order updated successfully');
}
else {
} else {
console.log(data.status);
}
});

View File

@ -16,9 +16,7 @@
<x-layout.app>
<x-slot:page_title>Test Page</x-slot:page_title>
@php
dump($totalFees);
dump($lines);
dump(Audition::open()->get());
@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
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
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
get('/register')
->assertStatus(200)
@ -24,13 +25,35 @@ it('shows a registration page', function () {
'Registration Code',
'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')
->assertStatus(200)
->assertSeeText([
'Log In',
'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');
});