Allow bulk changing release category

This commit is contained in:
DariusIII
2026-05-15 15:45:08 +02:00
parent f62336305a
commit b33bfd2bd8
7 changed files with 390 additions and 22 deletions
@@ -33,16 +33,40 @@ class AdminReleasesController extends BasePageController
$meta_title = $title = 'Release List';
$page = $request->input('page', 1);
$releaseList = Release::getReleasesRange($page);
$page = (int) $request->input('page', 1);
$search = trim((string) $request->input('search', ''));
$categoryId = $request->filled('category_id') ? (int) $request->input('category_id') : null;
if ($categoryId === -1) {
$categoryId = null;
}
$releaseList = Release::getReleasesRange($page, $search !== '' ? $search : null, $categoryId);
$releaseList->appends($request->only(['search', 'category_id']));
return view('admin.releases.index', [
'releaselist' => $releaseList,
'catlist' => Category::getForSelect(true),
'title' => $title,
'meta_title' => $meta_title,
]);
}
public function bulkCategory(Request $request): RedirectResponse
{
$validated = $request->validate([
'guids' => 'required|array|min:1',
'guids.*' => 'string',
'categories_id' => 'required|integer|min:1|exists:categories,id',
]);
$count = $this->releaseManagement->bulkUpdateCategory(
$validated['guids'],
(int) $validated['categories_id'],
);
return back()->with('success', $count.' release(s) re-categorised.');
}
/**
* @return \Illuminate\Contracts\Foundation\Application|Application|RedirectResponse|Redirector|View
*
+93 -13
View File
@@ -12,6 +12,8 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Pagination\LengthAwarePaginator as PaginatorLengthAware;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
@@ -381,16 +383,99 @@ class Release extends Model
* @param int $page The page number to retrieve
* @return LengthAwarePaginator|mixed
*/
public static function getReleasesRange(int $page = 1): mixed
public static function getReleasesRange(int $page = 1, ?string $search = null, ?int $categoryId = null): mixed
{
$expiresAt = now()->addMinutes(config('nntmux.cache_expiry_long'));
$cacheKey = md5('releasesRange_'.$page);
$releases = Cache::get($cacheKey);
if ($releases !== null) {
return $releases;
$search = $search !== null && $search !== '' ? $search : null;
$hasFilters = $search !== null || $categoryId !== null;
if ($search !== null && Search::isAvailable()) {
return self::getReleasesRangeFromSearchIndex($page, $search, $categoryId);
}
$releases = self::query()
if (! $hasFilters) {
$expiresAt = now()->addMinutes(config('nntmux.cache_expiry_long'));
$cacheKey = md5('releasesRange_'.$page);
$releases = Cache::get($cacheKey);
if ($releases !== null) {
return $releases;
}
}
$query = self::adminReleaseListQuery();
if ($search !== null) {
$query->where('releases.searchname', 'like', '%'.$search.'%');
}
if ($categoryId !== null) {
$query->where('releases.categories_id', $categoryId);
}
$releases = $query->paginate(config('nntmux.items_per_page'), ['*'], 'page', $page);
if (! $hasFilters) {
$expiresAt = now()->addMinutes(config('nntmux.cache_expiry_long'));
Cache::put(md5('releasesRange_'.$page), $releases, $expiresAt);
}
return $releases;
}
/**
* @return PaginatorLengthAware<int, mixed>
*/
private static function getReleasesRangeFromSearchIndex(int $page, string $search, ?int $categoryId): PaginatorLengthAware
{
$perPage = (int) config('nntmux.items_per_page');
$offset = ($page - 1) * $perPage;
$criteria = [
'phrases' => ['searchname' => $search],
'category_ids' => $categoryId !== null ? [$categoryId] : null,
'sort_field' => 'postdate_ts',
'sort_dir' => 'desc',
'try_fuzzy' => true,
];
$filtered = Search::searchReleasesFiltered($criteria, $perPage, $offset);
$ids = array_map(static fn (int|string $id): int => (int) $id, $filtered['ids'] ?? []);
$total = (int) ($filtered['total'] ?? 0);
if ($ids === []) {
return new PaginatorLengthAware([], 0, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => 'page',
]);
}
$idList = implode(',', $ids);
$rows = self::adminReleaseListQuery()
->whereIn('releases.id', $ids)
->orderByRaw('FIELD(releases.id, '.$idList.')')
->get();
return new PaginatorLengthAware($rows, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => 'page',
]);
}
/**
* Clear cached admin release-list pages after bulk updates.
*/
public static function clearAdminReleasesRangeCache(int $maxPages = 25): void
{
for ($page = 1; $page <= $maxPages; $page++) {
Cache::forget(md5('releasesRange_'.$page));
}
}
/**
* @return Builder<Release>
*/
private static function adminReleaseListQuery(): Builder
{
return self::query()
->select(
[
'releases.id',
@@ -409,12 +494,7 @@ class Release extends Model
)
->leftJoin('categories as c', 'c.id', '=', 'releases.categories_id')
->leftJoin('root_categories as cp', 'cp.id', '=', 'c.root_categories_id')
->orderByDesc('releases.postdate')
->paginate(config('nntmux.items_per_page'), ['*'], 'page', $page);
Cache::put($cacheKey, $releases, $expiresAt);
return $releases;
->orderByDesc('releases.postdate');
}
public static function getByGuid(mixed $guid): mixed
@@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
/**
* Service for managing releases (delete, update, export).
@@ -111,6 +112,60 @@ class ReleaseManagementService
return $updated;
}
/**
* @param list<string> $guids
*/
public function bulkUpdateCategory(array $guids, int $categoryId): int
{
$guids = array_values(array_filter($guids));
if ($guids === [] || $categoryId <= 0) {
return 0;
}
$updated = 0;
DB::transaction(function () use ($guids, $categoryId, &$updated): void {
$releaseIds = Release::query()
->whereIn('guid', $guids)
->pluck('id');
$updated = Release::query()
->whereIn('guid', $guids)
->update(['categories_id' => $categoryId, 'iscategorized' => 1]);
if ($updated > 0) {
$this->syncReleasesToSearchIndex($releaseIds);
Release::clearAdminReleasesRangeCache();
}
});
return $updated;
}
/**
* Re-index releases after query-builder updates that bypass {@see ReleaseObserver}.
*
* @param Collection<int, int|string>|iterable<int|string> $releaseIds
*/
private function syncReleasesToSearchIndex(iterable $releaseIds): void
{
foreach ($releaseIds as $releaseId) {
$intId = (int) $releaseId;
if ($intId <= 0) {
continue;
}
try {
Search::updateRelease($intId);
} catch (\Throwable $e) {
Log::error('ReleaseManagementService: Failed to sync release to search index after category change', [
'release_id' => $intId,
'error' => $e->getMessage(),
]);
}
}
}
/**
* @return Release[]|Builder[]|\Illuminate\Database\Eloquent\Collection<int, mixed>|\Illuminate\Database\Query\Builder[]|Collection<int, mixed>
*/