Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/Console/Commands/DashWidgetsRefresh.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Console\Commands;

use App\Models\Collection;
use App\Support\CollectionAnnotationScores;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
Expand Down Expand Up @@ -177,5 +178,8 @@ public function handle()
}

$this->info('Processing collection wiget counts complete');

CollectionAnnotationScores::forget();
$this->info('Cache for collection annotation scores cleared.');
}
}
117 changes: 83 additions & 34 deletions app/Livewire/CollectionList.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace App\Livewire;

use App\Models\Collection;
use App\Support\CollectionAnnotationScores;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
Expand All @@ -19,67 +21,114 @@ class CollectionList extends Component
#[Url(as: 'limit')]
public $size = 20;

#[Url()]
public $sort = 'created_at';
#[Url(as: 'sortBy')]
public $sortBy = 'release_date';

#[Url(as: 'dir')]
public $sortDir = 'desc';

#[Url()]
public $page = null;

public function updatingQuery()
public function updatingQuery(): void
{
$this->resetPage();
}

public function updatingSortBy(): void
{
$this->resetPage();
}

public function toggleSortDir(): void
{
$this->sortDir = $this->sortDir === 'desc' ? 'asc' : 'desc';
$this->resetPage();
}

public function updatingSort()
public function clearSearch(): void
{
$this->query = '';
$this->resetPage();
}

public function render()
{
$search = $this->query;
// $search = strtolower($this->query ?? '');
$query = Collection::query()
->where('status', 'PUBLISHED')
->published()
->where(function ($query) use ($search) {
$query->whereRaw('LOWER(title) ILIKE ?', ['%'.$search.'%'])
->orWhereRaw('LOWER(description) ILIKE ?', ['%'.$search.'%']);
});

// When searching, prioritize relevance first, then apply user sorting as secondary
if (! empty($search)) {
$query->orderByRaw('title ILIKE ? DESC', [$search.'%'])
->orderByRaw('description ILIKE ? DESC', [$search.'%']);

// Apply user sorting as secondary criteria for items with same relevance
switch ($this->sort) {
case 'title':
$query->orderBy('title', 'asc');
break;
case 'created_at':
$query->orderBy('created_at', 'desc');
break;
default:
$query->orderBy('title', 'asc');
break;
}
} else {
// When not searching, apply only user-selected sorting
switch ($this->sort) {
case 'title':
$query->orderBy('title', 'asc');
break;
case 'created_at':
$query->orderBy('created_at', 'desc');
break;
default:
$query->orderBy('title', 'asc');
break;
}
}

$this->applySort($query);

$collections = $query->paginate($this->size);

return view('livewire.collection-list', ['collections' => $collections]);
return view('livewire.collection-list', [
'collections' => $collections,
'sortByOptions' => $this->sortByOptions(),
]);
}

/**
* @return array<string, string>
*/
public function sortByOptions(): array
{
return [
'release_date' => 'Release date',
'updated_at' => 'Last updated',
'created_at' => 'Date added',
'title' => 'Title',
'molecules_count' => 'Molecule count',
'avg_annotation_level' => 'Annotation score',
'citations_count' => 'Citations',
'organisms_count' => 'Organisms',
'geo_count' => 'Geo locations',
];
}

/**
* @param Builder<Collection> $query
*/
private function applySort(Builder $query): void
{
$dir = $this->normalizedSortDir();

match ($this->normalizedSortBy()) {
'title' => $query->orderBy('title', $dir),
'release_date', 'updated_at', 'created_at',
'molecules_count', 'citations_count', 'organisms_count', 'geo_count' => $this->orderByColumn($query, $this->normalizedSortBy(), $dir),
'avg_annotation_level' => CollectionAnnotationScores::applySort($query, $dir),
default => $this->orderByColumn($query, 'release_date', 'desc'),
};
}

/**
* @param Builder<Collection> $query
*/
private function orderByColumn(Builder $query, string $column, string $dir): void
{
$direction = $dir === 'asc' ? 'ASC' : 'DESC';
$query->orderByRaw("{$column} {$direction} NULLS LAST");
}

private function normalizedSortBy(): string
{
return array_key_exists($this->sortBy, $this->sortByOptions())
? $this->sortBy
: 'release_date';
}

private function normalizedSortDir(): string
{
return in_array($this->sortDir, ['asc', 'desc'], true) ? $this->sortDir : 'desc';
}
}
10 changes: 10 additions & 0 deletions app/Models/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Models;

use App\Filament\Traits\MutatesCollectionFormData;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand Down Expand Up @@ -98,4 +99,13 @@ public function transformAudit(array $data): array
{
return changeAudit($data);
}

/**
* @param Builder<Collection> $query
* @return Builder<Collection>
*/
public function scopePublished(Builder $query): Builder
{
return $query->where('status', 'PUBLISHED');
}
}
66 changes: 66 additions & 0 deletions app/Support/CollectionAnnotationScores.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace App\Support;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

class CollectionAnnotationScores
{
public const CACHE_KEY = 'collections.avg_annotation_levels';

/**
* @var array{0: int, 1: int}
*/
public const CACHE_TTL = [172800, 259200];

/**
* @return Collection<int, float>
*/
public static function scores(): Collection
{
/** @var Collection<int, float> $scores */
$scores = Cache::flexible(self::CACHE_KEY, self::CACHE_TTL, function () {
return DB::table('collection_molecule as cm')
->join('molecules as m', 'm.id', '=', 'cm.molecule_id')
->join('collections as c', 'c.id', '=', 'cm.collection_id')
->where('c.status', 'PUBLISHED')
->where('m.active', true)
->whereRaw('NOT (m.is_parent = true AND m.has_variants = true)')
->groupBy('cm.collection_id')
->pluck(DB::raw('AVG(m.annotation_level) as avg_score'), 'cm.collection_id')
->map(fn ($score) => (float) $score);
});

return $scores;
}

/**
* @param Builder<\App\Models\Collection> $query
*/
public static function applySort(Builder $query, string $direction = 'desc'): void
{
$sorted = static::scores();
$sortedIds = ($direction === 'asc' ? $sorted->sort() : $sorted->sortDesc())
->keys()
->map(fn ($id) => (int) $id)
->values()
->all();

if ($sortedIds === []) {
$query->orderByRaw('release_date DESC NULLS LAST');

return;
}

$idList = implode(',', $sortedIds);
$query->orderByRaw("array_position(ARRAY[{$idList}]::bigint[], collections.id) ASC NULLS LAST");
}

public static function forget(): void
{
Cache::forget(self::CACHE_KEY);
}
}
7 changes: 7 additions & 0 deletions database/factories/CollectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,11 @@ public function definition(): array
'uuid' => $this->faker->uuid(),
];
}

public function published(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'PUBLISHED',
]);
}
}
Loading
Loading