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 @@ +
+
Modified:
+
+
+
Accessioned:
+

Download: