Move scoring functions to their own controller. ScoringGuide cache service and listener to update
This commit is contained in:
parent
e8732293ba
commit
d8e20d5f64
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Broadcasting\PresenceChannel;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ScoringGuideChange
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channels the event should broadcast on.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||||
|
*/
|
||||||
|
public function broadcastOn(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new PrivateChannel('channel-name'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Events\ScoringGuideChange;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\ScoringGuide;
|
use App\Models\ScoringGuide;
|
||||||
use App\Models\SubscoreDefinition;
|
use App\Models\SubscoreDefinition;
|
||||||
|
|
@ -37,6 +38,8 @@ class ScoringGuideController extends Controller
|
||||||
'name' => request('name')
|
'name' => request('name')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
ScoringGuideChange::dispatch();
|
||||||
|
|
||||||
return redirect('/admin/scoring');
|
return redirect('/admin/scoring');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +65,7 @@ class ScoringGuideController extends Controller
|
||||||
$guide->update([
|
$guide->update([
|
||||||
'name' => request('name')
|
'name' => request('name')
|
||||||
]);
|
]);
|
||||||
|
ScoringGuideChange::dispatch();
|
||||||
return redirect('/admin/scoring/guides/' . $guide->id . '/edit' )->with('success','Scoring guide updated');
|
return redirect('/admin/scoring/guides/' . $guide->id . '/edit' )->with('success','Scoring guide updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +97,7 @@ class ScoringGuideController extends Controller
|
||||||
'for_seating' => $for_seating,
|
'for_seating' => $for_seating,
|
||||||
'for_advance' => $for_advance,
|
'for_advance' => $for_advance,
|
||||||
]);
|
]);
|
||||||
|
ScoringGuideChange::dispatch();
|
||||||
return redirect('/admin/scoring/guides/' . $guide->id . '/edit' )->with('success','Subscore added');
|
return redirect('/admin/scoring/guides/' . $guide->id . '/edit' )->with('success','Subscore added');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,6 +109,7 @@ class ScoringGuideController extends Controller
|
||||||
$subscore = SubscoreDefinition::find($id);
|
$subscore = SubscoreDefinition::find($id);
|
||||||
$subscore->update(['display_order' => $index]);
|
$subscore->update(['display_order' => $index]);
|
||||||
}
|
}
|
||||||
|
ScoringGuideChange::dispatch();
|
||||||
return response()->json(['status'=>'success']);
|
return response()->json(['status'=>'success']);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +122,7 @@ class ScoringGuideController extends Controller
|
||||||
$subscore = SubscoreDefinition::find($id);
|
$subscore = SubscoreDefinition::find($id);
|
||||||
$subscore->update(['tiebreak_order' => $index]);
|
$subscore->update(['tiebreak_order' => $index]);
|
||||||
}
|
}
|
||||||
|
ScoringGuideChange::dispatch();
|
||||||
return response()->json(['status'=>'success']);
|
return response()->json(['status'=>'success']);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Tabulation;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Models\ScoreSheet;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
class ScoreController extends Controller
|
||||||
|
{
|
||||||
|
public function chooseEntry(Request $request)
|
||||||
|
{
|
||||||
|
return view('tabulation.choose_entry');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyScore(ScoreSheet $score) {
|
||||||
|
$score->delete();
|
||||||
|
return redirect()->back()->with('success','Score Deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function entryScoreSheet(Request $request)
|
||||||
|
{
|
||||||
|
$existing_sheets = [];
|
||||||
|
$entry = Entry::with(['student','audition.room.judges'])->find($request->input('entry_id'));
|
||||||
|
$judges = $entry->audition->room->judges;
|
||||||
|
foreach ($judges as $judge) {
|
||||||
|
$scoreSheet = ScoreSheet::where('entry_id',$entry->id)->where('user_id',$judge->id)->first();
|
||||||
|
if ($scoreSheet) {
|
||||||
|
Session::flash('caution','Scores exist for this entry. Now editing existing scores');
|
||||||
|
$existing_sheets[$judge->id] = $scoreSheet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scoring_guide = $entry->audition->scoringGuide;
|
||||||
|
$subscores = $entry->audition->scoringGuide->subscores->sortBy('display_order');
|
||||||
|
if (!$entry) {
|
||||||
|
return redirect()->route('tabulation.chooseEntry')->with('error','Entry not found');
|
||||||
|
}
|
||||||
|
return view('tabulation.entry_score_sheet', compact('entry','judges','scoring_guide','subscores','existing_sheets'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveEntryScoreSheet(Request $request, Entry $entry)
|
||||||
|
{
|
||||||
|
$judges = $entry->audition->room->judges;
|
||||||
|
|
||||||
|
$subscores = $entry->audition->scoringGuide->subscores->sortBy('tiebreak_order');
|
||||||
|
$scoringGuide = $entry->audition->scoringGuide;
|
||||||
|
$preparedScoreSheets = [];
|
||||||
|
foreach ($judges as $judge) {
|
||||||
|
$preparedScoreSheets[$judge->id]['user_id'] = $judge->id;
|
||||||
|
$preparedScoreSheets[$judge->id]['entry_id'] = $entry->id;
|
||||||
|
|
||||||
|
$scoreValidation = $scoringGuide->validateScores($request->input('judge'.$judge->id));
|
||||||
|
if ($scoreValidation != 'success') {
|
||||||
|
return redirect(url()->previous())->with('error', $judge->full_name() . ': ' . $scoreValidation)->with('oldScores',$request->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
$scoreSubmission = $request->input('judge'.$judge->id);
|
||||||
|
$scoresToSave = [];
|
||||||
|
foreach ($subscores as $subscore) {
|
||||||
|
$scoresToSave[$subscore->id] = [
|
||||||
|
'subscore_id'=>$subscore->id,
|
||||||
|
'subscore_name' => $subscore->name,
|
||||||
|
'score' => intval($scoreSubmission[$subscore->id])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$preparedScoreSheets[$judge->id]['scores'] = $scoresToSave;
|
||||||
|
}
|
||||||
|
foreach ($preparedScoreSheets as $sheet) {
|
||||||
|
ScoreSheet::updateOrCreate(
|
||||||
|
['entry_id' => $sheet['entry_id'], 'user_id' => $sheet['user_id']],
|
||||||
|
['subscores' => $sheet['scores']]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return redirect()->route('scores.chooseEntry')->with('success',count($preparedScoreSheets) . " Scores saved");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -14,71 +14,7 @@ use function redirect;
|
||||||
|
|
||||||
class TabulationController extends Controller
|
class TabulationController extends Controller
|
||||||
{
|
{
|
||||||
public function chooseEntry(Request $request)
|
|
||||||
{
|
|
||||||
return view('tabulation.choose_entry');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroyScore(ScoreSheet $score) {
|
|
||||||
$score->delete();
|
|
||||||
return redirect()->back()->with('success','Score Deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function entryScoreSheet(Request $request)
|
|
||||||
{
|
|
||||||
$existing_sheets = [];
|
|
||||||
$entry = Entry::with(['student','audition.room.judges'])->find($request->input('entry_id'));
|
|
||||||
$judges = $entry->audition->room->judges;
|
|
||||||
foreach ($judges as $judge) {
|
|
||||||
$scoreSheet = ScoreSheet::where('entry_id',$entry->id)->where('user_id',$judge->id)->first();
|
|
||||||
if ($scoreSheet) {
|
|
||||||
Session::flash('caution','Scores exist for this entry. Now editing existing scores');
|
|
||||||
$existing_sheets[$judge->id] = $scoreSheet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$scoring_guide = $entry->audition->scoringGuide;
|
|
||||||
$subscores = $entry->audition->scoringGuide->subscores->sortBy('display_order');
|
|
||||||
if (!$entry) {
|
|
||||||
return redirect()->route('tabulation.chooseEntry')->with('error','Entry not found');
|
|
||||||
}
|
|
||||||
return view('tabulation.entry_score_sheet', compact('entry','judges','scoring_guide','subscores','existing_sheets'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveEntryScoreSheet(Request $request, Entry $entry)
|
|
||||||
{
|
|
||||||
$judges = $entry->audition->room->judges;
|
|
||||||
|
|
||||||
$subscores = $entry->audition->scoringGuide->subscores->sortBy('tiebreak_order');
|
|
||||||
$scoringGuide = $entry->audition->scoringGuide;
|
|
||||||
$preparedScoreSheets = [];
|
|
||||||
foreach ($judges as $judge) {
|
|
||||||
$preparedScoreSheets[$judge->id]['user_id'] = $judge->id;
|
|
||||||
$preparedScoreSheets[$judge->id]['entry_id'] = $entry->id;
|
|
||||||
|
|
||||||
$scoreValidation = $scoringGuide->validateScores($request->input('judge'.$judge->id));
|
|
||||||
if ($scoreValidation != 'success') {
|
|
||||||
return redirect(url()->previous())->with('error', $judge->full_name() . ': ' . $scoreValidation)->with('oldScores',$request->all());
|
|
||||||
}
|
|
||||||
|
|
||||||
$scoreSubmission = $request->input('judge'.$judge->id);
|
|
||||||
$scoresToSave = [];
|
|
||||||
foreach ($subscores as $subscore) {
|
|
||||||
$scoresToSave[$subscore->id] = [
|
|
||||||
'subscore_id'=>$subscore->id,
|
|
||||||
'subscore_name' => $subscore->name,
|
|
||||||
'score' => intval($scoreSubmission[$subscore->id])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
$preparedScoreSheets[$judge->id]['scores'] = $scoresToSave;
|
|
||||||
}
|
|
||||||
foreach ($preparedScoreSheets as $sheet) {
|
|
||||||
ScoreSheet::updateOrCreate(
|
|
||||||
['entry_id' => $sheet['entry_id'], 'user_id' => $sheet['user_id']],
|
|
||||||
['subscores' => $sheet['scores']]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return redirect()->route('tabulation.chooseEntry')->with('success',count($preparedScoreSheets) . " Scores created");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function status()
|
public function status()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Events\ScoringGuideChange;
|
||||||
|
use App\Services\ScoringGuideCacheService;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
|
||||||
|
class RefreshScoringGuideCache
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create the event listener.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the event.
|
||||||
|
*/
|
||||||
|
public function handle(ScoringGuideChange $event): void
|
||||||
|
{
|
||||||
|
ScoringGuideCacheService::refreshCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO To give your application a speed boost, you should cache a manifest of all of your application's listeners using the optimize or event:cache Artisan commands. Typically, this command should be run as part of your application's deployment process. This manifest will be used by the framework to speed up the event registration process. The event:clear command may be used to destroy the event cache.
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Events\ScoringGuideChange;
|
||||||
|
use App\Listeners\RefreshScoringGuideCache;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
|
@ -19,6 +22,9 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
Event::listen(
|
||||||
|
ScoringGuideChange::class,
|
||||||
|
RefreshScoringGuideCache::class
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\ScoringGuide;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class ScoringGuideCacheService
|
||||||
|
{
|
||||||
|
protected $cacheKey = 'scoring_guides';
|
||||||
|
/**
|
||||||
|
* Create a new class instance.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScoringGuides()
|
||||||
|
{
|
||||||
|
return Cache::rememberForever($this->cacheKey, function () {
|
||||||
|
return ScoringGuide::with('subscores')->get();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refreshCache()
|
||||||
|
{
|
||||||
|
Cache::forget($this->cacheKey);
|
||||||
|
$this->getScoringGuides();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
@if(! $score->isValid())
|
@if(! $score->isValid())
|
||||||
<form method="POST" action="/admin/scores/{{ $score->id }}">
|
<form method="POST" action="{{ route('scores.destroy',['score'=>$score->id]) }}">
|
||||||
@csrf
|
@csrf
|
||||||
@method('DELETE')
|
@method('DELETE')
|
||||||
<x-slot:right_link_button class="bg-red-500 text-white">INVALID SCORE - DELETE</x-slot:right_link_button>
|
<x-slot:right_link_button class="bg-red-500 text-white">INVALID SCORE - DELETE</x-slot:right_link_button>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
-->
|
-->
|
||||||
<div class="absolute left-1/2 z-10 mt-5 flex w-screen max-w-min -translate-x-1/2 px-4" x-show="open" x-cloak>
|
<div class="absolute left-1/2 z-10 mt-5 flex w-screen max-w-min -translate-x-1/2 px-4" x-show="open" x-cloak>
|
||||||
<div class="w-56 shrink rounded-xl bg-white p-4 text-sm font-semibold leading-6 text-gray-900 shadow-lg ring-1 ring-gray-900/5">
|
<div class="w-56 shrink rounded-xl bg-white p-4 text-sm font-semibold leading-6 text-gray-900 shadow-lg ring-1 ring-gray-900/5">
|
||||||
<a href="/tabulation/enter_scores" class="block p-2 hover:text-indigo-600">Enter Scores</a>
|
<a href="{{ route('scores.chooseEntry') }}" class="block p-2 hover:text-indigo-600">Enter Scores</a>
|
||||||
<a href="/tabulation/status" class="block p-2 hover:text-indigo-600">Audition Status</a>
|
<a href="/tabulation/status" class="block p-2 hover:text-indigo-600">Audition Status</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
<x-card.card class="mx-auto max-w-sm">
|
<x-card.card class="mx-auto max-w-sm">
|
||||||
<x-card.heading>Choose Entry</x-card.heading>
|
<x-card.heading>Choose Entry</x-card.heading>
|
||||||
<div class="">
|
<div class="">
|
||||||
<x-form.form method="GET" action="/tabulation/entries/" class="mb-4 mt-3">
|
<x-form.form method="GET" action="{{ route('scores.entryScoreSheet') }}" class="mb-4 mt-3">
|
||||||
<x-form.field name="entry_id" label_text="Entry ID"></x-form.field>
|
<x-form.field name="entry_id" label_text="Entry ID"></x-form.field>
|
||||||
</x-form.form>
|
<x-form.footer >
|
||||||
<x-form.footer class="px-4 sm:px-8 pb-4">
|
|
||||||
<x-form.button>Select</x-form.button>
|
<x-form.button>Select</x-form.button>
|
||||||
</x-form.footer>
|
</x-form.footer>
|
||||||
|
</x-form.form>
|
||||||
</div>
|
</div>
|
||||||
</x-card.card>
|
</x-card.card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</x-slot:right_side>
|
</x-slot:right_side>
|
||||||
</x-card.heading>
|
</x-card.heading>
|
||||||
|
|
||||||
<x-form.form method="POST" action="/tabulation/entries/{{ $entry->id }}">
|
<x-form.form method="POST" action="{{ route('scores.saveEntryScoreSheet',[ 'entry' => $entry->id]) }}">
|
||||||
<x-table.table>
|
<x-table.table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,19 @@ Route::middleware(['auth','verified',CheckIfCanJudge::class])->prefix('judging')
|
||||||
Route::post('/entry/{entry}','saveScoreSheet');
|
Route::post('/entry/{entry}','saveScoreSheet');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Score Tabulation Routes
|
// Tabulation Routes
|
||||||
Route::middleware(['auth','verified',CheckIfCanTab::class])->prefix('tabulation/')->group(function() {
|
Route::middleware(['auth','verified',CheckIfCanTab::class])->group(function() {
|
||||||
|
|
||||||
|
// Score Management
|
||||||
|
Route::prefix('scores/')->controller(\App\Http\Controllers\Tabulation\ScoreController::class)->group(function() {
|
||||||
|
Route::get('/choose_entry','chooseEntry')->name('scores.chooseEntry');
|
||||||
|
Route::get('/entry','entryScoreSheet')->name('scores.entryScoreSheet');
|
||||||
|
Route::post('/entry/{entry}','saveEntryScoreSheet')->name('scores.saveEntryScoreSheet');
|
||||||
|
Route::delete('/{score}',[\App\Http\Controllers\Tabulation\ScoreController::class,'destroyScore'])->name('scores.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
// Generic Tabulation Routes
|
// Generic Tabulation Routes
|
||||||
Route::controller(\App\Http\Controllers\Tabulation\TabulationController::class)->group(function() {
|
Route::prefix('tabulation/')->controller(\App\Http\Controllers\Tabulation\TabulationController::class)->group(function() {
|
||||||
Route::get('/enter_scores','chooseEntry')->name('tabulation.chooseEntry');
|
|
||||||
Route::get('/record_noshow','chooseEntry');
|
|
||||||
Route::get('/entries','entryScoreSheet');
|
|
||||||
Route::post('/entries/{entry}','saveEntryScoreSheet');
|
|
||||||
Route::get('/status','status');
|
Route::get('/status','status');
|
||||||
Route::get('/auditions/{audition}','auditionSeating');
|
Route::get('/auditions/{audition}','auditionSeating');
|
||||||
});
|
});
|
||||||
|
|
@ -45,7 +50,8 @@ Route::middleware(['auth','verified',CheckIfCanTab::class])->prefix('tabulation/
|
||||||
// Admin Routes
|
// Admin Routes
|
||||||
Route::middleware(['auth','verified',CheckIfAdmin::class])->prefix('admin/')->group(function() {
|
Route::middleware(['auth','verified',CheckIfAdmin::class])->prefix('admin/')->group(function() {
|
||||||
Route::view('/','admin.dashboard');
|
Route::view('/','admin.dashboard');
|
||||||
Route::delete('/scores/{score}',[TabulationController::class,'destroyScore']);
|
|
||||||
|
|
||||||
Route::post('/auditions/roomUpdate',[\App\Http\Controllers\Admin\AuditionController::class,'roomUpdate']); // Endpoint for JS assigning auditions to rooms
|
Route::post('/auditions/roomUpdate',[\App\Http\Controllers\Admin\AuditionController::class,'roomUpdate']); // Endpoint for JS assigning auditions to rooms
|
||||||
Route::post('/scoring/assign_guide_to_audition',[\App\Http\Controllers\Admin\AuditionController::class,'scoringGuideUpdate']); // Endpoint for JS assigning scoring guides to auditions
|
Route::post('/scoring/assign_guide_to_audition',[\App\Http\Controllers\Admin\AuditionController::class,'scoringGuideUpdate']); // Endpoint for JS assigning scoring guides to auditions
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue