Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
ff44e2b
handle quotes in a datasets title
mike-kaimika Mar 18, 2026
ac0594a
add wysiwyg editor to team and dataset descriptions
mike-kaimika Mar 25, 2026
965c78d
multi_class_score: add database fields and update edit screen
mike-kaimika Apr 10, 2026
d0ce81e
multi_class_score: fix duplicate path check
mike-kaimika Apr 10, 2026
620f716
multi_class_score: initial implmentation of multiple class scores
mike-kaimika Apr 13, 2026
f60b3c9
multi_class_score: update migration
mike-kaimika Apr 21, 2026
b6c3909
multi_class_score: refactor
mike-kaimika Apr 21, 2026
fdb910d
multi_class_score: prevent duplicate model values
mike-kaimika Apr 21, 2026
205dc16
metadata_commands: first draft
mike-kaimika Apr 22, 2026
87b772c
metadata_commands: add helper text
mike-kaimika Apr 22, 2026
038974d
metadata_commands: restrict datasets for non super admins
mike-kaimika Apr 22, 2026
08b6a06
export_quotes: wrap export metadata fields with quotes
mike-kaimika Apr 22, 2026
398c0aa
add_team_to_breadcrumb: add team to breadcrumb on the bin page
mike-kaimika Apr 22, 2026
6fafabe
add_bin_modified_date: add bin modified date and keep it updated
mike-kaimika Mar 25, 2026
1e4dde8
origin/add_bin_modified_date: add accessioned date and display to bin…
mike-kaimika Apr 23, 2026
bd4a04c
add team name to dataset list
mike-kaimika Apr 28, 2026
bf3a34f
update version
mike-kaimika Apr 28, 2026
9491fae
Merge branch 'fix_dataset_page_title' into staging/5.2
mike-kaimika Apr 28, 2026
7f47a13
Merge branch 'dependabot/pip/django-4.2.30' into staging/5.2
mike-kaimika Apr 28, 2026
639f16c
Merge branch 'add_team_to_breadcrumb' into staging/5.2
mike-kaimika Apr 28, 2026
202babc
Merge branch 'team_on_dataset_list' into staging/5.2
mike-kaimika Apr 28, 2026
cff54e3
Merge branch 'blob-png-endpoint' into staging/5.2
mike-kaimika Apr 28, 2026
129d233
Merge branch 'add_bin_modified_date' into staging/5.2
mike-kaimika Apr 28, 2026
d2b42c7
Merge branch 'team_description_wysiwyg' into staging/5.2
mike-kaimika Apr 28, 2026
f3da574
Merge branch 'multi_class_score' into staging/5.2
mike-kaimika Apr 28, 2026
ab5141b
Merge branch 'metadata_commands' into staging/5.2
mike-kaimika Apr 28, 2026
88e8810
Merge branch 'export_quotes' into staging/5.2
mike-kaimika Apr 28, 2026
1d9ccaa
renumber migrations
mike-kaimika Apr 28, 2026
b9c2ac3
store previously used dataset names
mike-kaimika May 14, 2026
814d1ed
redirect from previously used dataset names
mike-kaimika May 29, 2026
12e789b
improve edit dataset ui
mike-kaimika Jun 1, 2026
fa7be35
fix getpage() generated from within the admin
mike-kaimika Jun 2, 2026
32cfb6f
Merge branch 'fix_routes_from_admin' into staging/5.2
mike-kaimika Jun 7, 2026
97fc015
Merge branch 'reserve_dataset_names' into staging/5.2
mike-kaimika Jun 7, 2026
9adac59
renumber migrations
mike-kaimika Jun 7, 2026
728f8ce
allow managers to manage tags and the skip flag
mike-kaimika Jun 7, 2026
fdb436b
update version in footer
mike-kaimika Jun 7, 2026
bea1124
fix modified date not being updated properly
mike-kaimika Jun 28, 2026
774908b
fix modified date not being updated properly
mike-kaimika Jun 28, 2026
4b813d1
Merge branch 'add_bin_modified_date' into release/5.2
mike-kaimika Jun 29, 2026
6dc694d
bump pyifcb version to 1.3.1
joefutrelle Jul 1, 2026
fee39ea
Merge branch 'pyifcb131' into release/5.2
mike-kaimika Jul 1, 2026
2409813
update version
mike-kaimika Jul 1, 2026
a7eda3c
Delete datasets (#522)
mike-kaimika Jul 1, 2026
01d085e
search for bins without a dataset or team (#524)
mike-kaimika Jul 1, 2026
282611f
management command to recompute ml_analyzed for all bins in a dataset…
joefutrelle Jul 7, 2026
959aaf9
fix manage links on dataset list when teams is not enabled
mike-kaimika Jul 8, 2026
5cf280e
Merge pull request #536 from WHOIGit/fix-dataset-list-without-teams
joefutrelle Jul 8, 2026
384bf41
Merge branch 'master' into release/5.3
mike-kaimika Aug 27, 2026
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
202 changes: 202 additions & 0 deletions ifcbdb/dashboard/management/commands/recomputemlanalyzed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import glob
import multiprocessing as mp
import os

import ifcb
from ifcb.data.files import Fileset, FilesetBin

from django.core.management.base import BaseCommand, CommandError
from django.db import connections
from tqdm import tqdm

from dashboard.models import Bin, DataDirectory, Dataset, bin_query


def _resolve_bin(pid, cached_path, search_dirs):
"""Locate the raw FilesetBin for a pid using only the filesystem (no ORM).

`cached_path` is the bin's cached basepath (may be empty); `search_dirs` is
a list of (directory_path, whitelist, blacklist) tuples to search in order.
This mirrors Bin._get_bin() but is safe to run in a worker process.
"""
if cached_path and os.path.exists(cached_path + '.adc'):
return FilesetBin(Fileset(cached_path))
for directory_path, whitelist, blacklist in search_dirs:
data_directory = ifcb.DataDirectory(
directory_path, whitelist=whitelist, blacklist=blacklist)
try:
return data_directory[pid]
except KeyError:
continue
return None


def _load_excluded_pids(directory):
"""Collect the set of pids present in all *.parquet files in a directory.

Only the pid column is read. These bins already have ml_analyzed from the
parquet workflow, so we skip them entirely (no raw-file read).
"""
import pyarrow.parquet as pq

parquet_paths = sorted(glob.glob(os.path.join(directory, '*.parquet')))
if not parquet_paths:
raise CommandError('no parquet files found in {}'.format(directory))
pids = set()
for parquet_path in parquet_paths:
table = pq.read_table(parquet_path, columns=['pid'])
pids.update(table.column('pid').to_pylist())
return pids, len(parquet_paths)


def _worker(task):
"""Compute ml_analyzed for one bin. Returns (pid, ml_analyzed, resolved_path, error)."""
pid, cached_path, search_dirs = task
try:
raw_bin = _resolve_bin(pid, cached_path, search_dirs)
if raw_bin is None:
return pid, None, None, 'fileset not found'
ml_analyzed = raw_bin.ml_analyzed
resolved_path = os.path.splitext(raw_bin.fileset.adc_path)[0]
return pid, ml_analyzed, resolved_path, None
except Exception as exc:
return pid, None, None, '{}: {}'.format(type(exc).__name__, exc)


class Command(BaseCommand):
help = 'recompute ml_analyzed for every bin in a dataset from its raw files'

def add_arguments(self, parser):
parser.add_argument('dataset', type=str, help='name of dataset to process')
parser.add_argument('-j', '--jobs', type=int, default=1,
help='number of parallel worker processes (default: 1)')
parser.add_argument('--exclude-parquet-dir', type=str, default=None,
help='directory of bin_ml_analyzed parquet files; bins whose '
'pid appears in them are skipped (catch stragglers only)')
parser.add_argument('--batch-size', type=int, default=1000,
help='number of bins to process per chunk (default: 1000)')

def _build_task(self, bin_obj):
# candidate raw directories, mirroring Bin._get_bin() search order:
# the cached data_directory first, then each dataset's raw dirs by priority
search_dirs = []
seen_ids = set()

def add(directory):
if (directory is not None and directory.kind == DataDirectory.RAW
and directory.id not in seen_ids):
seen_ids.add(directory.id)
search_dirs.append((directory.path,
directory.whitelist.split(','),
directory.blacklist.split(',')))

add(bin_obj.data_directory)
raw_dirs = [directory
for dataset in bin_obj.datasets.all()
for directory in dataset.directories.all()
if directory.kind == DataDirectory.RAW]
for directory in sorted(raw_dirs, key=lambda d: d.priority):
add(directory)
return bin_obj.pid, bin_obj.path, search_dirs

def _apply(self, results, bins_by_pid, pbar):
"""Update the in-memory Bin objects from worker results, bulk_update them."""
batch = []
updated = failed = 0
for pid, ml_analyzed, resolved_path, err in results:
pbar.update(1)
bin_obj = bins_by_pid[pid]
if err is not None:
failed += 1
pbar.write('{}: {}'.format(pid, err))
continue
if ml_analyzed is None or ml_analyzed <= 0:
failed += 1
pbar.write('{}: skipping non-positive ml_analyzed: {}'.format(pid, ml_analyzed))
continue
bin_obj.set_ml_analyzed(ml_analyzed)
if resolved_path:
bin_obj.path = resolved_path
batch.append(bin_obj)
if batch:
Bin.objects.bulk_update(batch, ['ml_analyzed', 'concentration', 'path'])
updated = len(batch)
return updated, failed

def handle(self, *args, **options):
dataset_name = options['dataset']
jobs = options['jobs']
batch_size = options['batch_size']
exclude_dir = options['exclude_parquet_dir']

if not Dataset.objects.filter(name=dataset_name).exists():
raise CommandError('no such dataset: {}'.format(dataset_name))

excluded = set()
if exclude_dir is not None:
if not os.path.isdir(exclude_dir):
raise CommandError('not a directory: {}'.format(exclude_dir))
excluded, num_files = _load_excluded_pids(exclude_dir)
self.stdout.write('excluding {} pids from {} parquet file(s)'.format(
len(excluded), num_files))

qs = (bin_query(dataset_name=dataset_name)
.select_related('data_directory')
.prefetch_related('datasets__directories')
.order_by('pid'))
total = qs.count()
if total == 0:
self.stdout.write('no bins found in dataset {}'.format(dataset_name))
return

self.stdout.write('processing {} bins in dataset {}{}'.format(
total, dataset_name,
' ({} pids known-excluded via parquet)'.format(len(excluded)) if excluded else ''))

total_updated = total_failed = total_excluded = 0
# tqdm total is every bin in the dataset; excluded bins advance the bar
# without a raw-file read, so a single pass over the queryset suffices.
pbar = tqdm(total=total)

pool = None
if jobs > 1:
# drop inherited DB connections so forked workers don't share sockets
connections.close_all()
pool = mp.Pool(jobs)

try:
chunk = []

def flush(chunk):
bins_by_pid = {bin_obj.pid: bin_obj for bin_obj in chunk}
tasks = [self._build_task(bin_obj) for bin_obj in chunk]
if pool is None:
results = (_worker(task) for task in tasks)
else:
results = pool.imap_unordered(_worker, tasks, chunksize=8)
return self._apply(results, bins_by_pid, pbar)

for bin_obj in qs.iterator(chunk_size=batch_size):
if bin_obj.pid in excluded:
total_excluded += 1
pbar.update(1)
continue
chunk.append(bin_obj)
if len(chunk) >= batch_size:
updated, failed = flush(chunk)
total_updated += updated
total_failed += failed
chunk = []
if chunk:
updated, failed = flush(chunk)
total_updated += updated
total_failed += failed
finally:
if pool is not None:
pool.close()
pool.join()
pbar.close()

self.stdout.write(self.style.SUCCESS(
'{}: {} bins updated, {} skipped/failed, {} excluded via parquet'.format(
dataset_name, total_updated, total_failed, total_excluded)))
2 changes: 1 addition & 1 deletion ifcbdb/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
<div class="col-sm-12">
<ul class="nav justify-content-center">
<li class="nav-item">
<span class="px-2">Version 5.2.1 - July 2026</span>
<span class="px-2">Version 5.3 - August 2026</span>
</li>
<li class="nav-item">
<a class="px-2" href="{% url 'about_page' %}">About</a>
Expand Down
12 changes: 1 addition & 11 deletions ifcbdb/templates/secure/dataset-management.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,7 @@
<hr class="my-2">
<div class="row py-2 px-3">
<div class="col">
<table id="datasets" class="table table-sm table-striped table-bordered" style="width:100%">
<thead>
<tr>
<th>Name</th>
<th>Title</th>
<th>Active?</th>
{% if is_teams_enabled %}<th>Team</th>{% endif %}
<th></th>
</tr>
</thead>
</table>
<table id="datasets" class="table table-sm table-striped table-bordered" style="width:100%"></table>
</div>
</div>

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ scikit-image==0.24.0
pysmb==1.2.10
pyyaml==6.0.2
django-waffle==5.0.0
pyarrow==24.0.0
git+https://github.com/joefutrelle/pyifcb@v1.3.1