diff --git a/specifyweb/backend/barvis/tests/__init__.py b/specifyweb/backend/barvis/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/specifyweb/backend/barvis/tests/test_views.py b/specifyweb/backend/barvis/tests/test_views.py new file mode 100644 index 00000000000..be63f485a33 --- /dev/null +++ b/specifyweb/backend/barvis/tests/test_views.py @@ -0,0 +1,71 @@ +from specifyweb.specify.models import Collection, Determination, Taxon, Taxontreedefitem +from specifyweb.specify.tests.test_api import ApiTests + +from specifyweb.backend.barvis.views import get_taxon_bar_data + + +class TaxonBarTests(ApiTests): + def setUp(self): + super().setUp() + self.discipline.taxontreedef = self.taxontreedef + self.discipline.save() + self.taxontreedef.discipline = self.discipline + self.taxontreedef.save() + + root_rank = Taxontreedefitem.objects.create( + name='Root', rankid=0, treedef=self.taxontreedef + ) + species_rank = Taxontreedefitem.objects.create( + name='Species', rankid=220, parent=root_rank, treedef=self.taxontreedef + ) + self.root = Taxon.objects.create( + name='Root', rankid=0, definition=self.taxontreedef, + definitionitem=root_rank, + ) + self.taxon = Taxon.objects.create( + name='Test species', rankid=220, parent=self.root, + definition=self.taxontreedef, definitionitem=species_rank, + ) + + def test_returns_current_determination_counts_without_changing_rows(self): + collection_2 = Collection.objects.create( + catalognumformatname='test', + collectionname='TestCollection2', + isembeddedcollectingevent=False, + discipline=self.discipline, + ) + Determination.objects.create( + collectionobject=self.collectionobjects[0], taxon=self.taxon, + collectionmemberid=self.collection.id, iscurrent=True, + createdbyagent=self.agent, modifiedbyagent=self.agent, + ) + Determination.objects.create( + collectionobject=self.collectionobjects[1], taxon=self.taxon, + collectionmemberid=self.collection.id, iscurrent=True, + createdbyagent=self.agent, modifiedbyagent=self.agent, + ) + Determination.objects.bulk_create([ + Determination( + collectionobject=self.collectionobjects[0], taxon=self.taxon, + collectionmemberid=self.collection.id, iscurrent=True, + createdbyagent=self.agent, modifiedbyagent=self.agent, + ), + ]) + Determination.objects.create( + collectionobject=self.collectionobjects[2], taxon=self.taxon, + collectionmemberid=self.collection.id, iscurrent=False, + createdbyagent=self.agent, modifiedbyagent=self.agent, + ) + Determination.objects.create( + collectionobject=self.collectionobjects[2], taxon=self.taxon, + collectionmemberid=collection_2.id, iscurrent=True, + createdbyagent=self.agent, modifiedbyagent=self.agent, + ) + + self.assertCountEqual( + get_taxon_bar_data(self.collection), + [ + (self.root.id, 0, None, 'Root', 0), + (self.taxon.id, 220, self.root.id, 'Test species', 2), + ], + ) diff --git a/specifyweb/backend/barvis/views.py b/specifyweb/backend/barvis/views.py index bff43890db9..3e37f749ae9 100644 --- a/specifyweb/backend/barvis/views.py +++ b/specifyweb/backend/barvis/views.py @@ -1,60 +1,46 @@ +from django.db.models import Count, IntegerField, OuterRef, Subquery +from django.db.models.functions import Coalesce from django.http import HttpResponse -from django.db.models import Count, Q from specifyweb.middleware.general import require_GET -from specifyweb.specify.views import login_maybe_required from specifyweb.specify.api.filter_by_col import filter_by_collection from specifyweb.specify.api.serializers import toJson -from specifyweb.specify.models import Taxon +from specifyweb.specify.models import Determination, Taxon +from specifyweb.specify.views import login_maybe_required -from django.db import connection + +def get_taxon_bar_data(collection): + """Return taxon tile rows with a correlated current-count lookup.""" + current_determination_counts = ( + Determination.objects + .filter( + taxon_id=OuterRef('pk'), + collectionmemberid=collection.id, + iscurrent=True, + ) + .values('taxon_id') + .annotate(count=Count('collectionobject', distinct=True)) + .values('count') + ) + taxons = Taxon.objects.annotate( + current_determination_count=Coalesce( + Subquery( + current_determination_counts, + output_field=IntegerField(), + ), + 0, + ) + ).values_list( + 'id', 'rankid', 'parent_id', 'name', 'current_determination_count' + ) + return list(filter_by_collection(taxons, collection)) @require_GET @login_maybe_required def taxon_bar(request): - # "Returns the data for creating a taxon tiles visualization." - # cursor = connection.cursor() - # cursor.execute(""" - # SELECT t.TaxonID, - # t.RankID, - # t.ParentID, - # t.Name, - # (SELECT COUNT(*) FROM determination d WHERE t.TaxonID = d.TaxonID AND d.IsCurrent = 1) - # FROM taxon t - # WHERE t.TaxonTreeDefID = %s - # """, [request.specify_collection.discipline.taxontreedef_id]) - - # Implementing the previous SQL query in Django ORM: - taxons = ( - Taxon.objects.annotate( - current_determination_count=Count( - 'determinations', filter=Q(determinations__iscurrent=True)) - ) - .values_list("id", "rankid", "parent_id", "name", "current_determination_count") + """Return the data for creating a taxon tiles visualization.""" + return HttpResponse( + toJson(get_taxon_bar_data(request.specify_collection)), + content_type='application/json', ) - filtered_taxons = filter_by_collection(taxons, request.specify_collection) - result = toJson(list(filtered_taxons)) - - # SELECT d.TaxonID, COUNT(DISTINCT d.CollectionObjectID), t.ParentID - # FROM determination d - # INNER JOIN taxon t ON t.TaxonID = d.TaxonID - # WHERE d.CollectionMemberID = %s - # AND d.IsCurrent = 1 - # GROUP BY d.TaxonId - # ORDER BY d.TaxonId - # """, [request.specify_collection.id]) - # result = toJson(cursor.fetchall()) - # session = Session() - # query = session.query( - # Determination.TaxonID, - # func.count(distinct(Determination.CollectionObjectID)), - # Taxon.ParentID) \ - # .join(Taxon, Determination.TaxonID == Taxon.taxonId) \ - # .filter(Determination.collectionMemberId == request.specify_collection.id) \ - # .filter(Determination.isCurrent == True) \ - # .group_by(Determination.TaxonID).order_by(Determination.TaxonID) - - # result = toJson(list(query)) - # session.close() - return HttpResponse(result, content_type='application/json') diff --git a/specifyweb/specify/migrations/0049_determination_current_taxon_index.py b/specifyweb/specify/migrations/0049_determination_current_taxon_index.py new file mode 100644 index 00000000000..4e93f1aa6d0 --- /dev/null +++ b/specifyweb/specify/migrations/0049_determination_current_taxon_index.py @@ -0,0 +1,18 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('specify', '0048_taxontreedefitem_parent_context_delete'), + ] + + operations = [ + migrations.AddIndex( + model_name='determination', + index=models.Index( + fields=['collectionmemberid', 'iscurrent', 'taxon'], + name='DetColMemCurrentTaxonIDX', + ), + ), + ] diff --git a/specifyweb/specify/models.py b/specifyweb/specify/models.py index 6ab316a15f8..f479befb0c3 100644 --- a/specifyweb/specify/models.py +++ b/specifyweb/specify/models.py @@ -2888,6 +2888,10 @@ class Meta: indexes = [ models.Index(fields=['determineddate'], name='DeterminedDateIDX'), models.Index(fields=['collectionmemberid'], name='DetMemIDX'), + models.Index( + fields=['collectionmemberid', 'iscurrent', 'taxon'], + name='DetColMemCurrentTaxonIDX', + ), models.Index(fields=['alternatename'], name='AlterNameIDX'), models.Index(fields=['guid'], name='DeterminationGuidIDX'), models.Index(fields=['typestatusname'], name='TypeStatusNameIDX')