diff --git a/ifcbdb/assets/js/bin.js b/ifcbdb/assets/js/bin.js
index 9ab97bd7..7ea938ff 100644
--- a/ifcbdb/assets/js/bin.js
+++ b/ifcbdb/assets/js/bin.js
@@ -135,20 +135,30 @@ function showWorkspace(workspace) {
}
}
+function updateDateTimeLabel(selector, value) {
+ if (!value) {
+ $(selector).closest(".flex-row").hide();
+ return;
+ }
+
+ const timestamp = moment.utc(value);
+ const dateString = timestamp.format("YYYY-MM-DD");
+ const timeString = timestamp.format("HH:mm:ss z");
+ const relativeTime = timestamp.fromNow();
+
+ const text =
+ `${dateString}
${timeString}
` +
+ `(${relativeTime})`;
+
+ $(selector).closest(".flex-row").show();
+ $(selector).html(text);
+}
+
//************* Bin Methods ***********************/
function updateBinStats(data) {
- var timestamp_iso = data["timestamp_iso"];
- var timestamp = moment.utc(timestamp_iso);
-
- var date_string = timestamp.format("YYYY-MM-DD");
- var time_string = timestamp.format("HH:mm:ss z");
- var initial_relative_time = timestamp.fromNow();
-
- $("#stat-date-time").html(
- date_string + "
" +
- time_string + "
" +
- "("+initial_relative_time+")"
- );
+ updateDateTimeLabel("#stat-date-time", data.timestamp_iso);
+ updateDateTimeLabel("#stat-modified", data.modified);
+ updateDateTimeLabel("#stat-accessioned", data.accessioned);
function showField(id, text) {
$("#show-"+id).removeClass("d-none").addClass("d-flex");
diff --git a/ifcbdb/dashboard/accession.py b/ifcbdb/dashboard/accession.py
index 444f218e..c9a2e294 100644
--- a/ifcbdb/dashboard/accession.py
+++ b/ifcbdb/dashboard/accession.py
@@ -9,6 +9,7 @@
from django.db import IntegrityError, transaction
from django.db.models import Count, Max
from django.contrib.postgres.aggregates.general import StringAgg
+from django.utils import timezone
import pandas as pd
import numpy as np
@@ -96,6 +97,8 @@ def sync_one(self, pid):
'data_directory': dd_found,
'skip': True, # in case accession is interrupted
'team': team,
+ 'modified': timezone.now(),
+ 'accessioned': timezone.now(),
})
# For existing bins, if the team value is not set, and there is one, save that value. This handles pre-existing
@@ -167,6 +170,8 @@ def sync(self, progress_callback=do_nothing, log_callback=do_nothing):
'data_directory': dd,
'skip': True, # in case accession is interrupted
'team': team,
+ 'modified': timezone.now(),
+ 'accessioned': timezone.now(),
})
# For existing bins, if the team value is not set, and there is one, save that value. This handles pre-existing
diff --git a/ifcbdb/dashboard/management/commands/bintool.py b/ifcbdb/dashboard/management/commands/bintool.py
index 243593f6..9fcf0134 100644
--- a/ifcbdb/dashboard/management/commands/bintool.py
+++ b/ifcbdb/dashboard/management/commands/bintool.py
@@ -59,6 +59,9 @@ def handle(self, *args, **options):
except Dataset.DoesNotExist:
self.stderr.write(f"Dataset '{add_dataset_name}' does not exist.")
+ # Update timestamp on affected bins
+ bins.update(modified=timezone.now())
+
if options['cache_paths']:
for b in bins:
b._get_bin() # this will cache the path if it isn't already
diff --git a/ifcbdb/dashboard/management/commands/updatetriggers.py b/ifcbdb/dashboard/management/commands/updatetriggers.py
index a910a3c7..663df777 100644
--- a/ifcbdb/dashboard/management/commands/updatetriggers.py
+++ b/ifcbdb/dashboard/management/commands/updatetriggers.py
@@ -5,6 +5,7 @@
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
+from django.utils import timezone
from tqdm import tqdm
from tqdm._utils import _term_move_up
@@ -42,8 +43,10 @@ def bulk_update(self, chunk, pbar):
pbar.write(self.border)
continue
bin.n_triggers = self.n_triggers(self.last_line(bin.adc_path()))
+ bin.modified = timezone.now()
objs.append(bin)
- res = Bin.objects.bulk_update(objs, ['n_triggers'])
+
+ res = Bin.objects.bulk_update(objs, ['n_triggers', 'modified'])
pbar.update(res)
if res == 0:
pbar.write(self.clear_border + ("Error: Bins, " + str(objs) + " not updated! Continuing ..."))
@@ -77,8 +80,7 @@ def parse_input_csv(self, input_csv):
row = next(reader)
with transaction.atomic():
for row in reader:
- res = 0
- res = Bin.objects.filter(pid=row[0]).update(n_triggers=row[1])
+ res = Bin.objects.filter(pid=row[0]).update(n_triggers=row[1], modified=timezone.now())
if res == 0:
print("Error: Bin, " + bin.pid + " not updated! Continuing ...")
diff --git a/ifcbdb/dashboard/migrations/0053_bin_modified.py b/ifcbdb/dashboard/migrations/0053_bin_modified.py
new file mode 100644
index 00000000..e99ef6f6
--- /dev/null
+++ b/ifcbdb/dashboard/migrations/0053_bin_modified.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.29 on 2026-03-25 03:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0052_team_short_description'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='bin',
+ name='modified',
+ field=models.DateTimeField(auto_now=True, null=True),
+ ),
+ ]
diff --git a/ifcbdb/dashboard/migrations/0054_bin_accessioned_alter_bin_modified.py b/ifcbdb/dashboard/migrations/0054_bin_accessioned_alter_bin_modified.py
new file mode 100644
index 00000000..a6ea0fac
--- /dev/null
+++ b/ifcbdb/dashboard/migrations/0054_bin_accessioned_alter_bin_modified.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.30 on 2026-04-23 14:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0053_bin_modified'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='bin',
+ name='accessioned',
+ field=models.DateTimeField(null=True),
+ ),
+ migrations.AlterField(
+ model_name='bin',
+ name='modified',
+ field=models.DateTimeField(null=True),
+ ),
+ ]
diff --git a/ifcbdb/dashboard/models.py b/ifcbdb/dashboard/models.py
index 40170396..7a25e00a 100644
--- a/ifcbdb/dashboard/models.py
+++ b/ifcbdb/dashboard/models.py
@@ -510,6 +510,8 @@ class Bin(models.Model):
data_directory = models.ForeignKey('DataDirectory', null=True, blank=True, on_delete=models.SET_NULL)
# accession
added = models.DateTimeField(auto_now_add=True, null=True)
+ modified = models.DateTimeField(auto_now=False, null=True)
+ accessioned = models.DateTimeField(auto_now=False, null=True)
# qaqc flags
qc_bad = models.BooleanField(default=False) # is this bin invalid
qc_no_rois = models.BooleanField(default=False)
@@ -816,6 +818,11 @@ def add_tag(self, tag_name, user=None):
event, created = TagEvent.objects.get_or_create(bin=self, tag=tag)
if created and user is not None:
event.user = user
+
+ # Update the timestamp on the bin
+ if created:
+ self.save(update_fields=['modified'])
+
return event
def delete_tag(self, tag_name, normalize=True):
@@ -825,6 +832,9 @@ def delete_tag(self, tag_name, normalize=True):
event = TagEvent.objects.get(bin=self, tag=tag)
event.delete()
+ # Update the timestamp on the bin
+ self.save(update_fields=['modified'])
+
# comments
def add_comment(self, content, user=None, skip_duplicates=False):
@@ -835,11 +845,17 @@ def add_comment(self, content, user=None, skip_duplicates=False):
comment = Comment(bin=self, content=content, user=user)
comment.save()
+ # Update the timestamp on the bin
+ self.save(update_fields=['modified'])
+
def delete_comment(self, comment_id, user):
try:
comment = Comment.objects.get(bin=self, pk=comment_id)
if user.is_staff:
comment.delete()
+
+ # Update the timestamp on the bin
+ self.save(update_fields=['modified'])
except:
pass
diff --git a/ifcbdb/dashboard/views.py b/ifcbdb/dashboard/views.py
index 97aab275..cc4dd092 100644
--- a/ifcbdb/dashboard/views.py
+++ b/ifcbdb/dashboard/views.py
@@ -881,6 +881,8 @@ def _bin_details(bin, dataset=None, view_size=None, scale_factor=None, preload_a
"cruise": bin.cruise,
"cast": bin.cast,
"niskin": bin.niskin,
+ "modified": bin.modified,
+ "accessioned": bin.accessioned,
}
def _mosaic_page_image(request, bin_id):
diff --git a/ifcbdb/secure/views.py b/ifcbdb/secure/views.py
index 11c6c668..a9be4a5d 100644
--- a/ifcbdb/secure/views.py
+++ b/ifcbdb/secure/views.py
@@ -6,6 +6,7 @@
from django.contrib.auth.models import User, Group
from django.db.models import Count
from django import forms
+from django.utils import timezone
from django.views.decorators.http import require_POST, require_GET
from django.http import JsonResponse, Http404, HttpResponseForbidden, HttpResponse, StreamingHttpResponse
from django.shortcuts import render, get_object_or_404, redirect, reverse
@@ -399,12 +400,18 @@ def merge_tag(request, id):
# Get the list of bins already assigned to the target tag to prevent creating duplicates
assigned_bins = TagEvent.query(tag=target, dataset=dataset).values("bin")
+ # Get the bins that will be affected so we can update the modified date
+ affected_bin_ids = list(tag_events.values_list("bin_id", flat=True))
+
# Update the tag on any records not already assigned to the target tag
tag_events.exclude(bin__in=assigned_bins).update(tag=target)
# Remove any records already assigned to the target tag
tag_events.filter(bin__in=assigned_bins).delete()
+ # Update the timestamp on affected bins
+ Bin.objects.filter(id__in=affected_bin_ids).update(modified=timezone.now())
+
# If the tag is no longer in use on any bins, remove it
if TagEvent.objects.filter(tag=tag).count() == 0:
tag.delete()
@@ -721,6 +728,9 @@ def update_comment(request, bin_id):
comment.content = content
comment.save()
+ # Update the timestamp on the bin
+ bin.save(update_fields=['modified'])
+
return JsonResponse({
"id": comment.id,
"comments": bin.comment_list,
@@ -1126,6 +1136,9 @@ def assign_dataset(bin_qs, dataset):
num_already_assigned = dataset.bins.filter(id__in=bin_qs.values_list("id", flat=True)).count()
dataset.bins.add(*bin_qs)
+
+ # Update the timestamp on all updated bins
+ bin_qs.update(modified=timezone.now())
total = bin_qs.count()
num_assigned = total - num_already_assigned
@@ -1155,6 +1168,9 @@ def unassign_dataset(bin_qs, dataset):
})
dataset.bins.remove(*bin_qs)
+
+ # Update the timestamp on all updated bins
+ bin_qs.update(modified=timezone.now())
label = "bins" if num_assigned != 1 else "bin"
return JsonResponse({
diff --git a/ifcbdb/templates/dashboard/bin.html b/ifcbdb/templates/dashboard/bin.html
index 5d993fe0..246ca98f 100644
--- a/ifcbdb/templates/dashboard/bin.html
+++ b/ifcbdb/templates/dashboard/bin.html
@@ -566,6 +566,12 @@
+
Download: