diff --git a/ami/jobs/tests.py b/ami/jobs/tests.py index 870d4ffb8..bd15394e3 100644 --- a/ami/jobs/tests.py +++ b/ami/jobs/tests.py @@ -48,6 +48,7 @@ def setUp(self): self.job = Job.objects.create(project=self.project, name="Test job", delay=0) self.user = User.objects.create_user( # type: ignore email="testuser@insectai.org", + is_staff=True, ) self.factory = APIRequestFactory() @@ -99,7 +100,6 @@ def test_run_job(self): jobs_run_url = reverse_with_params("api:job-run", args=[self.job.pk], params={"no_async": True}) self.client.force_authenticate(user=self.user) resp = self.client.post(jobs_run_url) - self.client.force_authenticate(user=None) self.assertEqual(resp.status_code, 200) data = resp.json() self.assertEqual(data["id"], self.job.pk) diff --git a/ami/main/api/serializers.py b/ami/main/api/serializers.py index 32c293566..448225291 100644 --- a/ami/main/api/serializers.py +++ b/ami/main/api/serializers.py @@ -552,6 +552,7 @@ class Meta: "withdrawn", "agreed_with_identification_id", "agreed_with_prediction_id", + "comment", "created_at", "updated_at", ] @@ -944,6 +945,7 @@ class Meta: "taxon", "user", "withdrawn", + "comment", "created_at", ] diff --git a/ami/main/migrations/0030_identification_comment.py b/ami/main/migrations/0030_identification_comment.py new file mode 100644 index 000000000..4e7404dd0 --- /dev/null +++ b/ami/main/migrations/0030_identification_comment.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.2 on 2024-04-16 18:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("main", "0029_alter_deployment_device_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="identification", + name="comment", + field=models.TextField(blank=True), + ), + ] diff --git a/ami/main/models.py b/ami/main/models.py index 7825952ef..47a94631a 100644 --- a/ami/main/models.py +++ b/ami/main/models.py @@ -1285,6 +1285,7 @@ class Identification(BaseModel): related_name="agreed_identifications", ) score = 1.0 # Always 1 for humans, at this time + comment = models.TextField(blank=True) class Meta: ordering = [ @@ -1390,6 +1391,10 @@ class Classification(BaseModel): ) # job = models.CharField(max_length=255, null=True) + # Type hints for auto-generated fields + taxon_id: int + algorithm_id: int + class Meta: ordering = ["-created_at", "-score"] diff --git a/ami/main/tests.py b/ami/main/tests.py index 0740e4867..476bc11c1 100644 --- a/ami/main/tests.py +++ b/ami/main/tests.py @@ -3,6 +3,7 @@ from django.db import connection from django.test import TestCase +from rest_framework.test import APIRequestFactory, APITestCase from rich import print from ami.main.models import ( @@ -17,6 +18,7 @@ TaxonRank, group_images_into_events, ) +from ami.users.models import User def setup_test_project(reuse=True) -> tuple[Project, Deployment]: @@ -547,3 +549,51 @@ def test_taxon_detail(self): response = self.client.get(f"/api/v2/taxa/{taxon.pk}/") self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["name"], taxon.name) + + +class TestIdentification(APITestCase): + def setUp(self) -> None: + project, deployment = setup_test_project() + create_taxa(project=project) + create_captures(deployment=deployment) + group_images_into_events(deployment=deployment) + create_occurrences(deployment=deployment, num=5) + self.project = project + self.user = User.objects.create_user( # type: ignore + email="testuser@insectai.org", + is_staff=True, + ) + self.factory = APIRequestFactory() + self.client.force_authenticate(user=self.user) + return super().setUp() + + def test_identification(self): + from ami.main.models import Identification, Taxon + + """ + Post a new identification suggestion and check that it changed the occurrence's determination. + """ + + suggest_id_endpoint = "/api/v2/identifications/" + taxa = Taxon.objects.filter(projects=self.project) + assert taxa.count() > 1 + + occurrence = Occurrence.objects.filter(project=self.project).exclude(determination=None)[0] + original_taxon = occurrence.determination + assert original_taxon is not None + new_taxon = Taxon.objects.exclude(pk=original_taxon.pk)[0] + comment = "Test identification comment" + + response = self.client.post( + suggest_id_endpoint, + { + "occurrence_id": occurrence.pk, + "taxon_id": new_taxon.pk, + "comment": comment, + }, + ) + self.assertEqual(response.status_code, 201) + occurrence.refresh_from_db() + self.assertEqual(occurrence.determination, new_taxon) + identification = Identification.objects.get(pk=response.json()["id"]) + self.assertEqual(identification.comment, comment) diff --git a/ui/src/data-services/hooks/identifications/useCreateIdentification.ts b/ui/src/data-services/hooks/identifications/useCreateIdentification.ts index 4db2d0e53..32347e7cc 100644 --- a/ui/src/data-services/hooks/identifications/useCreateIdentification.ts +++ b/ui/src/data-services/hooks/identifications/useCreateIdentification.ts @@ -11,6 +11,7 @@ interface IdentificationFieldValues { } occurrenceId: string taxonId: string + comment?: string } const convertToServerFieldValues = ( @@ -20,6 +21,7 @@ const convertToServerFieldValues = ( agreed_with_prediction_id: fieldValues.agreeWith?.predictionId, occurrence_id: fieldValues.occurrenceId, taxon_id: fieldValues.taxonId, + comment: fieldValues.comment, }) export const useCreateIdentification = (onSuccess?: () => void) => { diff --git a/ui/src/data-services/models/occurrence-details.ts b/ui/src/data-services/models/occurrence-details.ts index 438386012..e5603bd93 100644 --- a/ui/src/data-services/models/occurrence-details.ts +++ b/ui/src/data-services/models/occurrence-details.ts @@ -11,11 +11,13 @@ export interface Identification { id: string overridden?: boolean taxon: Taxon + comment?: string userPermissions: UserPermission[] createdAt: string } export interface HumanIdentification extends Identification { + comment: string user: { id: string name: string @@ -57,6 +59,7 @@ export class OccurrenceDetails extends Occurrence { overridden, taxon, user: { id: `${i.user.id}`, name: i.user.name, image: i.user.image }, + comment: i.comment, userPermissions: i.user_permissions, createdAt: i.created_at, } diff --git a/ui/src/pages/occurrence-details/identification-card/identification-card.module.scss b/ui/src/pages/occurrence-details/identification-card/identification-card.module.scss index e8a745d9a..3387367b7 100644 --- a/ui/src/pages/occurrence-details/identification-card/identification-card.module.scss +++ b/ui/src/pages/occurrence-details/identification-card/identification-card.module.scss @@ -13,7 +13,6 @@ .content { display: flex; flex-direction: column; - gap: 16px; padding: 16px; position: relative; } @@ -23,3 +22,10 @@ align-items: center; justify-content: flex-end; } + +.comment { + display: block; + padding-top: 2px; + @include paragraph-small(); + color: $color-neutral-600; +} diff --git a/ui/src/pages/occurrence-details/identification-card/identification-card.tsx b/ui/src/pages/occurrence-details/identification-card/identification-card.tsx index 95498fac9..6cf5eb10b 100644 --- a/ui/src/pages/occurrence-details/identification-card/identification-card.tsx +++ b/ui/src/pages/occurrence-details/identification-card/identification-card.tsx @@ -73,6 +73,9 @@ export const IdentificationCard = ({ }) } /> +