Repository navigation
Feat: detections csv export #1395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9caa057
4cba842
e605263
800d6f7
7f7f46a
cd785ea
fc3186f
a5ebaf5
1dc55a3
792e508
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Generated by Django 4.2.10 on 2026-09-02 14:04 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("exports", "0001_initial"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AlterField( | ||
| model_name="dataexport", | ||
| name="format", | ||
| field=models.CharField( | ||
| choices=[ | ||
| ("occurrences_api_json", "occurrences_api_json"), | ||
| ("occurrences_simple_csv", "occurrences_simple_csv"), | ||
| ("detections_csv", "detections_csv"), | ||
| ], | ||
| max_length=255, | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| from rest_framework.test import APIClient | ||
|
|
||
| from ami.exports.models import DataExport | ||
| from ami.exports.registry import ExportRegistry | ||
| from ami.main.models import Detection, Identification, Occurrence, SourceImageCollection, Taxon | ||
| from ami.ml.models import Algorithm | ||
| from ami.tests.fixtures.main import ( | ||
|
|
@@ -38,7 +39,11 @@ def setUp(self): | |
| # Create a collection using the provided method | ||
| self.collection = self._create_collection() | ||
| # Define export formats | ||
| self.export_formats = ["occurrences_simple_csv", "occurrences_api_json"] | ||
| self.export_formats = [ | ||
| "occurrences_simple_csv", | ||
| "occurrences_api_json", | ||
| "detections_csv", | ||
| ] | ||
|
|
||
| def _create_export_with_file(self, format_type): | ||
| filename = f"exports/test_export_file_{format_type}.json" | ||
|
|
@@ -113,6 +118,9 @@ def run_and_validate_export(self, format_type): | |
| self.validate_csv_records(f) | ||
| elif format_type == "occurrences_api_json": | ||
| self.validate_json_records(f) | ||
| elif format_type == "detections_csv": | ||
| # TODO this checks against Occurrence count not Detections, but 1:1 for now | ||
| self.validate_csv_records(f) | ||
|
Comment on lines
+121
to
+123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Validate detection rows against detections. Lines 121-123 call Add a detection-specific count helper that uses 🤖 Prompt for AI Agents |
||
|
|
||
| # Clean up the exported file after the test | ||
| default_storage.delete(file_path) | ||
|
|
@@ -305,8 +313,8 @@ def test_non_member_cannot_create_export(self): | |
| ) | ||
|
|
||
|
|
||
| class ExportNewFieldsTest(TestCase): | ||
| """Test the new machine prediction, verification, and detection fields in CSV exports.""" | ||
| class ExportDataTestCase(TestCase): | ||
| format_type = None | ||
|
|
||
| def setUp(self): | ||
| self.project, self.deployment = setup_test_project(reuse=False) | ||
|
|
@@ -335,6 +343,23 @@ def setUp(self): | |
| self.taxon_b = Taxon.objects.create(name="Test Taxon B") | ||
| self.taxon_b.projects.add(self.project) | ||
|
|
||
| def _run_csv_export(self): | ||
| """Run a CSV export and return the rows as a list of dicts.""" | ||
| data_export = DataExport.objects.create( | ||
| user=self.user, | ||
| project=self.project, | ||
| format=self.format_type, | ||
| job=None, | ||
| ) | ||
| self.data_export = data_export | ||
| file_url = data_export.run_export() | ||
| self.assertIsNotNone(file_url) | ||
| file_path = file_url.replace("/media/", "") | ||
| with default_storage.open(file_path, "r") as f: | ||
| rows = list(csv.DictReader(f)) | ||
| default_storage.delete(file_path) | ||
| return rows | ||
|
|
||
| def _create_occurrence_with_prediction(self, taxon=None, score=0.85): | ||
| """Create an occurrence with a single detection and ML classification.""" | ||
| taxon = taxon or self.taxon_a | ||
|
|
@@ -355,21 +380,19 @@ def _create_occurrence_with_prediction(self, taxon=None, score=0.85): | |
| occurrence = detection.associate_new_occurrence() | ||
| return occurrence, classification | ||
|
|
||
| def _run_csv_export(self): | ||
| """Run a CSV export and return the rows as a list of dicts.""" | ||
| data_export = DataExport.objects.create( | ||
| user=self.user, | ||
| project=self.project, | ||
| format="occurrences_simple_csv", | ||
| job=None, | ||
| ) | ||
| file_url = data_export.run_export() | ||
| self.assertIsNotNone(file_url) | ||
| file_path = file_url.replace("/media/", "") | ||
| with default_storage.open(file_path, "r") as f: | ||
| rows = list(csv.DictReader(f)) | ||
| default_storage.delete(file_path) | ||
| return rows | ||
| def test_export_filename_label(self): | ||
| if not self.format_type: | ||
| return | ||
| label = ExportRegistry.get_exporter(self.format_type).filename_label | ||
| occurrence, classification = self._create_occurrence_with_prediction() | ||
| self._run_csv_export() | ||
| self.assertIn(label, self.data_export.file_url or "") | ||
|
|
||
|
|
||
| class ExportNewFieldsTest(ExportDataTestCase): | ||
| """Test the new machine prediction, verification, and detection fields in CSV exports.""" | ||
|
|
||
| format_type = "occurrences_simple_csv" | ||
|
|
||
| def test_ml_prediction_only(self): | ||
| """Occurrence with only ML prediction: machine prediction fields populated, verified_by null.""" | ||
|
|
@@ -551,3 +574,50 @@ def test_csv_has_all_new_fields(self): | |
| ] | ||
| for field in expected_fields: | ||
| self.assertIn(field, headers, f"Missing CSV field: {field}") | ||
|
|
||
|
|
||
| class DetectionsExportFieldsTest(ExportDataTestCase): | ||
| format_type = "detections_csv" | ||
|
|
||
| def test_detection_row(self): | ||
| """Detection has expected columns""" | ||
| occurrence, classification = self._create_occurrence_with_prediction() | ||
| detection = occurrence.detections.first() | ||
| rows = self._run_csv_export() | ||
|
|
||
| row = next(r for r in rows if int(r["id"]) == detection.pk) | ||
| self.assertEqual(row["determination_name"], self.taxon_a.name) | ||
| self.assertEqual(row["detection_bbox"], str(detection.bbox)) | ||
| self.assertEqual(row["detection_crop_url"], "/media/" + detection.path) | ||
| self.assertEqual(row["source_image_path"], detection.source_image.path) | ||
| self.assertAlmostEqual(float(row["determination_score"]), 0.85, places=2) | ||
|
|
||
| def test_csv_has_expected_fields(self): | ||
| """fields are present as CSV column headers.""" | ||
| self._create_occurrence_with_prediction() | ||
| rows = self._run_csv_export() | ||
| self.assertGreater(len(rows), 0) | ||
| headers = rows[0].keys() | ||
| expected_fields = [ | ||
| "id", | ||
| "event_id", | ||
| "event_name", | ||
| "deployment_id", | ||
| "deployment_name", | ||
| "project_id", | ||
| "project_name", | ||
| "source_image_id", | ||
| "source_image_path", | ||
| "source_image_timestamp", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Assert values for detection export fields.
🤖 Prompt for AI Agents |
||
| "detection_bbox", | ||
| "detection_crop_url", | ||
| "detection_score", | ||
| "detection_algorithm_id", | ||
| "detection_algorithm_key", | ||
| "detection_algorithm_name", | ||
| "determination_id", | ||
| "determination_name", | ||
| "determination_score", | ||
| ] | ||
| for field in expected_fields: | ||
| self.assertIn(field, headers, f"Missing CSV field: {field}") | ||
|
loppear marked this conversation as resolved.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: RolnickLab/antenna
Length of output: 50374
🏁 Script executed:
Repository: RolnickLab/antenna
Length of output: 50375
🏁 Script executed:
Repository: RolnickLab/antenna
Length of output: 23867
Injection (CWE-1236): Improper Neutralization of Formula Elements in a CSV File ('CSV Injection')
Reachability: External · Exploitability: Moderate
Neutralize formula prefixes in exported CSV paths.
Escape values beginning with
=,+,-, or@before CSV serialization, and add a regression test for a formula-prefixed path.🤖 Prompt for AI Agents