diff --git a/sde_collections/migrations/0076_collection_reindexing_curated_by.py b/sde_collections/migrations/0076_collection_reindexing_curated_by.py new file mode 100644 index 00000000..33853d07 --- /dev/null +++ b/sde_collections/migrations/0076_collection_reindexing_curated_by.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.9 on 2025-01-09 05:07 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("sde_collections", "0075_alter_collection_reindexing_status_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="collection", + name="reindexing_curated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="reindexing_curated_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/sde_collections/migrations/0077_alter_collection_reindexing_curated_by.py b/sde_collections/migrations/0077_alter_collection_reindexing_curated_by.py new file mode 100644 index 00000000..9bd59f6b --- /dev/null +++ b/sde_collections/migrations/0077_alter_collection_reindexing_curated_by.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.9 on 2025-01-09 05:12 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("sde_collections", "0076_collection_reindexing_curated_by"), + ] + + operations = [ + migrations.AlterField( + model_name="collection", + name="reindexing_curated_by", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="reindexing_curated_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/sde_collections/migrations/0078_reindexinghistory_old_curator.py b/sde_collections/migrations/0078_reindexinghistory_old_curator.py new file mode 100644 index 00000000..2cac0895 --- /dev/null +++ b/sde_collections/migrations/0078_reindexinghistory_old_curator.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.9 on 2025-01-29 05:28 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("sde_collections", "0077_alter_collection_reindexing_curated_by"), + ] + + operations = [ + migrations.AddField( + model_name="reindexinghistory", + name="old_curator", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="old_curator", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/sde_collections/models/collection.py b/sde_collections/models/collection.py index 0f1162e1..3c40e28a 100644 --- a/sde_collections/models/collection.py +++ b/sde_collections/models/collection.py @@ -85,6 +85,9 @@ class Collection(models.Model): tracker = FieldTracker(fields=["workflow_status", "reindexing_status"]) curated_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True, blank=True) + reindexing_curated_by = models.ForeignKey( + User, on_delete=models.DO_NOTHING, null=True, blank=True, default=None, related_name="reindexing_curated_by" + ) curation_started = models.DateTimeField("Curation Started", null=True, blank=True) class Meta: @@ -550,6 +553,7 @@ def _create_from_json(cls, json_results): for collection in json_results: print("Creating collection: ", collection["name"]) collection.pop("curated_by") + collection.pop("reindexing_curated_by") cls.objects.create(**collection) @classmethod @@ -662,6 +666,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.old_workflow_status = self.workflow_status self.old_reindexing_status = self.reindexing_status + self.old_reindexing_curated_by = self.reindexing_curated_by class RequiredUrls(models.Model): @@ -742,12 +747,16 @@ def log_workflow_history(sender, instance, created, **kwargs): old_status=instance.old_workflow_status, ) - if instance.reindexing_status != instance.old_reindexing_status: + if ( + instance.reindexing_status != instance.old_reindexing_status + or instance.reindexing_curated_by != instance.old_reindexing_curated_by + ): ReindexingHistory.objects.create( collection=instance, reindexing_status=instance.reindexing_status, - curated_by=instance.curated_by, + curated_by=instance.reindexing_curated_by, old_status=instance.old_reindexing_status, + old_curator=instance.old_reindexing_curated_by, ) @@ -759,6 +768,9 @@ class ReindexingHistory(models.Model): ) old_status = models.IntegerField(choices=ReindexingStatusChoices.choices, null=True) curated_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True, blank=True) + old_curator = models.ForeignKey( + User, on_delete=models.DO_NOTHING, null=True, blank=True, related_name="old_curator" + ) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): diff --git a/sde_collections/serializers.py b/sde_collections/serializers.py index 8159dbb1..4326b039 100644 --- a/sde_collections/serializers.py +++ b/sde_collections/serializers.py @@ -28,6 +28,7 @@ class Meta: "workflow_status_display", "reindexing_status_display", "curated_by", + "reindexing_curated_by", "division", "document_type", "name", diff --git a/sde_collections/tests/test_reindexing_history.py b/sde_collections/tests/test_reindexing_history.py new file mode 100644 index 00000000..3fc1b4d5 --- /dev/null +++ b/sde_collections/tests/test_reindexing_history.py @@ -0,0 +1,85 @@ +# docker-compose -f local.yml run --rm django pytest -s sde_collections/tests/test_reindexing_history.py + +import pytest +from django.contrib.auth import get_user_model + +from sde_collections.models.collection import Collection, ReindexingHistory +from sde_collections.models.collection_choice_fields import ReindexingStatusChoices +from sde_collections.tests.factories import CollectionFactory, UserFactory + +User = get_user_model() + + +@pytest.mark.django_db +class TestReindexingHistory: + """Test suite for ReindexingHistory functionality""" + + def setup_method(self): + """Setup test data""" + self.collection = CollectionFactory() + self.user1 = UserFactory() + self.user2 = UserFactory() + + def test_reindexing_history_status_change(self): + """Should create history entry when reindexing status changes""" + self.collection.reindexing_status = ReindexingStatusChoices.REINDEXING_NEEDED_ON_DEV + self.collection.save() + + history = ReindexingHistory.objects.filter(collection=self.collection) + assert history.count() == 1 + assert history.first().reindexing_status == ReindexingStatusChoices.REINDEXING_NEEDED_ON_DEV + assert history.first().old_status == ReindexingStatusChoices.REINDEXING_NOT_NEEDED + + def test_reindexing_history_curator_change(self): + """Should create history entry when curator changes""" + self.collection.reindexing_curated_by = self.user1 + self.collection.save() + + history = ReindexingHistory.objects.filter(collection=self.collection) + assert history.count() == 1 + assert history.first().curated_by == self.user1 + assert history.first().old_curator is None + + def test_reindexing_history_both_changes(self): + """Should create history entry when both status and curator change""" + self.collection.reindexing_status = ReindexingStatusChoices.REINDEXING_NEEDED_ON_DEV + self.collection.reindexing_curated_by = self.user1 + self.collection.save() + + history = ReindexingHistory.objects.filter(collection=self.collection) + assert history.count() == 1 + entry = history.first() + assert entry.reindexing_status == ReindexingStatusChoices.REINDEXING_NEEDED_ON_DEV + assert entry.old_status == ReindexingStatusChoices.REINDEXING_NOT_NEEDED + assert entry.curated_by == self.user1 + assert entry.old_curator is None + + def test_reindexing_history_multiple_changes(self): + """Should create multiple history entries for sequential changes""" + # First change + self.collection.reindexing_status = ReindexingStatusChoices.REINDEXING_NEEDED_ON_DEV + self.collection.reindexing_curated_by = self.user1 + self.collection.save() + + # Re-fetch the object + self.collection = Collection.objects.get(id=self.collection.id) + + # Second change + self.collection.reindexing_status = ReindexingStatusChoices.REINDEXING_FINISHED_ON_DEV + self.collection.reindexing_curated_by = self.user2 + self.collection.save() + + history = ReindexingHistory.objects.filter(collection=self.collection).order_by("created_at") + assert history.count() == 2 + + first_entry = history[0] + assert first_entry.reindexing_status == ReindexingStatusChoices.REINDEXING_NEEDED_ON_DEV + assert first_entry.old_status == ReindexingStatusChoices.REINDEXING_NOT_NEEDED + assert first_entry.curated_by == self.user1 + assert first_entry.old_curator is None + + second_entry = history[1] + assert second_entry.reindexing_status == ReindexingStatusChoices.REINDEXING_FINISHED_ON_DEV + assert second_entry.old_status == ReindexingStatusChoices.REINDEXING_NEEDED_ON_DEV + assert second_entry.curated_by == self.user2 + assert second_entry.old_curator == self.user1 diff --git a/sde_indexing_helper/static/js/collection_list.js b/sde_indexing_helper/static/js/collection_list.js index 0cd5d8d7..a9904271 100644 --- a/sde_indexing_helper/static/js/collection_list.js +++ b/sde_indexing_helper/static/js/collection_list.js @@ -9,9 +9,11 @@ const COLUMNS = { CURATOR: 6, CONNECTOR_TYPE: 7, REINDEXING_STATUS: 8, - WORKFLOW_STATUS_RAW: 9, - CURATOR_ID: 10, - REINDEXING_STATUS_RAW: 11 + REINDEXING_CURATOR: 9, + WORKFLOW_STATUS_RAW: 10, + CURATOR_ID: 11, + REINDEXING_STATUS_RAW: 12, + REINDEXING_CURATOR_ID: 13 }; var uniqueId; //used for logic related to contents on column customization modal @@ -122,11 +124,25 @@ let table = $("#collection_table").DataTable({ }, }, ], + searchPanes: { + controls: true, + // layout: 'columns-6', + columns: [ + COLUMNS.DIVISION, + COLUMNS.DELTA_URLS, + COLUMNS.CURATED_URLS, + COLUMNS.WORKFLOW_STATUS, + COLUMNS.CURATOR, + COLUMNS.CONNECTOR_TYPE, + COLUMNS.REINDEXING_STATUS + ] + }, + columnDefs: [ // hide the data columns { - targets: [COLUMNS.WORKFLOW_STATUS_RAW, COLUMNS.CURATOR_ID, COLUMNS.REINDEXING_STATUS_RAW], - visible: false, + targets: [COLUMNS.WORKFLOW_STATUS_RAW, COLUMNS.CURATOR_ID, COLUMNS.REINDEXING_STATUS_RAW, COLUMNS.REINDEXING_CURATOR_ID], + visible: false, width: "0px", responsivePriority: -1 }, { width: "200px", targets: COLUMNS.URL }, { @@ -229,13 +245,6 @@ let table = $("#collection_table").DataTable({ targets: [COLUMNS.CURATED_URLS], type: "num-fmt", }, - // hide the data panes - { - searchPanes: { - show: false, - }, - targets: [COLUMNS.WORKFLOW_STATUS_RAW, COLUMNS.CURATOR_ID, COLUMNS.REINDEXING_STATUS_RAW], - }, { searchPanes: { dtOpts: { @@ -253,29 +262,37 @@ let table = $("#collection_table").DataTable({ targets: [COLUMNS.CONNECTOR_TYPE], }, ], + autoWidth: false, }); -$("#collection-dropdown-4").on("change", function () { +$("#workflow-status-selector").on("change", function () { table .columns(COLUMNS.WORKFLOW_STATUS_RAW) .search(this.value ? "^" + this.value + "$" : "", true, false) .draw(); }); -$("#collection-dropdown-5").on("change", function () { +$("#curator-selector").on("change", function () { table .columns(COLUMNS.CURATOR_ID) .search(this.value ? "^" + this.value + "$" : "", true, false) .draw(); }); -$("#collection-dropdown-6").on("change", function () { +$("#reindexing-status-selector").on("change", function () { table .columns(COLUMNS.REINDEXING_STATUS_RAW) .search(this.value ? "^" + this.value + "$" : "", true, false) .draw(); }); +$("#reindexing-curator-selector").on("change", function () { + table + .columns(COLUMNS.REINDEXING_CURATOR_ID) + .search(this.value ? "^" + this.value + "$" : "", true, false) + .draw(); +}); + $("#nameFilter").on("keyup", function () { table.columns(COLUMNS.NAME).search(this.value).draw(); }); @@ -426,6 +443,36 @@ function handleCuratorSelect() { }); } +function handleReindexingCuratorSelect() { + $("body").on("click", ".reindexing_curator_select", function () { + const collection_id = $(this).data("collection-id"); + const reindexing_curator_id = $(this).attr("value"); + const reindexing_curator_text = $(this).text(); + + // Update button text and style + const $button = $(`#reindexing-curator-button-${collection_id}`).last(); + $button + .text(reindexing_curator_text) + .removeClass("btn-light btn-danger btn-warning btn-info btn-success btn-primary btn-dark") + .addClass("btn-success"); + + // Update DataTable + const rowIndex = table.row("#" + collection_id).index(); + table.data()[rowIndex][COLUMNS.REINDEXING_CURATOR] = createReindexingCuratorButton(reindexing_curator_text); + + // Send update to server + postReindexingCurator(collection_id, reindexing_curator_id); + }); +} + +// Helper function to create reindexing curator button HTML +function createReindexingCuratorButton(reindexing_curator_text) { + const buttonClass = reindexing_curator_text === "None" ? "btn-dark" : "btn-success"; + return `