diff --git a/tom_base/settings.py b/tom_base/settings.py index ffe3b3f85..8e29250bd 100644 --- a/tom_base/settings.py +++ b/tom_base/settings.py @@ -320,10 +320,6 @@ 'PAGE_SIZE': 100 } -# Silence system checks that are not applicable to how the TOM Toolkit is designed. -# These are likely temporary and can be removed in downstream TOMs if desired. -SILENCED_SYSTEM_CHECKS = ['models.W047'] - try: from local_settings import * # noqa except ImportError: diff --git a/tom_dataproducts/migrations/0020_remove_astrometryreduceddatum_unique_astrometry_and_more.py b/tom_dataproducts/migrations/0020_remove_astrometryreduceddatum_unique_astrometry_and_more.py new file mode 100644 index 000000000..8d64896e7 --- /dev/null +++ b/tom_dataproducts/migrations/0020_remove_astrometryreduceddatum_unique_astrometry_and_more.py @@ -0,0 +1,42 @@ +# Generated by Django 5.2.13 on 2026-09-23 22:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tom_dataproducts', '0019_alter_astrometryreduceddatum_options_and_more'), + ('tom_targets', '0031_basetarget_shared_by_basetarget_shared_from'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='astrometryreduceddatum', + name='unique_astrometry', + ), + migrations.RemoveConstraint( + model_name='photometryreduceddatum', + name='unique_photometry', + ), + migrations.RemoveConstraint( + model_name='spectroscopyreduceddatum', + name='unique_spectroscopy', + ), + migrations.AddConstraint( + model_name='astrometryreduceddatum', + constraint=models.UniqueConstraint(fields=('target', 'timestamp', 'telescope', 'instrument', 'ra', 'dec', 'reduction_version'), name='unique_astrometry'), + ), + migrations.AddConstraint( + model_name='photometryreduceddatum', + constraint=models.UniqueConstraint(fields=('target', 'bandpass', 'timestamp', 'brightness', 'instrument', 'reduction_version'), name='unique_photometry_brightness'), + ), + migrations.AddConstraint( + model_name='photometryreduceddatum', + constraint=models.UniqueConstraint(fields=('target', 'bandpass', 'timestamp', 'limit', 'instrument', 'reduction_version'), name='unique_photometry_limit'), + ), + migrations.AddConstraint( + model_name='spectroscopyreduceddatum', + constraint=models.UniqueConstraint(fields=('target', 'timestamp', 'telescope', 'instrument', 'flux', 'reduction_version'), name='unique_spectroscopy'), + ), + ] diff --git a/tom_dataproducts/models.py b/tom_dataproducts/models.py index ec79e054e..7d7b2521f 100644 --- a/tom_dataproducts/models.py +++ b/tom_dataproducts/models.py @@ -456,9 +456,12 @@ class PhotometryReducedDatum(ReducedDatumCommon): class Meta(ReducedDatumCommon.Meta): constraints = [ models.UniqueConstraint( - fields=["target", "bandpass", "timestamp", "limit", "brightness", "instrument", "reduction_version"], - name="unique_photometry", - nulls_distinct=False + fields=["target", "bandpass", "timestamp", "brightness", "instrument", "reduction_version"], + name="unique_photometry_brightness", + ), + models.UniqueConstraint( + fields=["target", "bandpass", "timestamp", "limit", "instrument", "reduction_version"], + name="unique_photometry_limit", ) ] @@ -481,7 +484,6 @@ class Meta(ReducedDatumCommon.Meta): models.UniqueConstraint( fields=["target", "timestamp", "telescope", "instrument", "flux", "reduction_version"], name="unique_spectroscopy", - nulls_distinct=False ) ] @@ -503,7 +505,6 @@ class Meta(ReducedDatumCommon.Meta): models.UniqueConstraint( fields=["target", "timestamp", "telescope", "instrument", "ra", "dec", "reduction_version"], name="unique_astrometry", - nulls_distinct=False ) ] diff --git a/tom_dataproducts/tests/tests.py b/tom_dataproducts/tests/tests.py index 0959f5066..84a188380 100644 --- a/tom_dataproducts/tests/tests.py +++ b/tom_dataproducts/tests/tests.py @@ -1,3 +1,4 @@ +from django.db import IntegrityError from tom_targets.base_models import get_target_model_app_label from http import HTTPStatus import os @@ -635,6 +636,17 @@ def test_create_reduced_datum(self): self.assertEqual(2, ReducedDatum.objects.count()) + def test_create_reduced_datum_without_legacy_value(self): + PhotometryReducedDatum.objects.create( + target=self.target, + timestamp=self.timestamp, + brightness=None, + bandpass="r", + limit=2.0, + ) + + self.assertEqual(1, ReducedDatum.objects.count()) + def test_create_reduced_datum_duplicate(self): """Test that we cannot add a second PhotometryReducedDatum with the same target, timestamp, brightness, and bandpass""" @@ -653,6 +665,106 @@ def test_create_reduced_datum_duplicate(self): bandpass="r" ) + def test_create_reduced_datum_duplicate_limit_none(self): + """Test that we cannot add a duplicate ReducedDataum, even when limit is None. + exposure_time is null in both cases, but we still expect a ValidationError.""" + PhotometryReducedDatum.objects.create( + target=self.target, + timestamp=self.timestamp, + brightness=1.0, + bandpass="r", + limit=None, + ) + + # This raises a ValidationError because by using create the uniqueness constraint + # is enforced in code, and never reaches the database level constraint. + with self.assertRaises(ValidationError): + PhotometryReducedDatum.objects.create( + target=self.target, + timestamp=self.timestamp, + brightness=1.0, + bandpass="r", + limit=None, + ) + + def test_create_reduced_datum_duplicate_brightness_none(self): + """Test that we cannot add a duplicate ReducedDataum, even when brightness is None. + exposure_time is null in both cases, but we still expect a ValidationError.""" + PhotometryReducedDatum.objects.create( + target=self.target, + timestamp=self.timestamp, + brightness=None, + bandpass="r", + limit=2.0, + ) + + # This raises a ValidationError because by using create the uniqueness constraint + # is enforced in code, and never reaches the database level constraint. + with self.assertRaises(ValidationError): + PhotometryReducedDatum.objects.create( + target=self.target, + timestamp=self.timestamp, + brightness=None, + bandpass="r", + limit=2.0, + ) + + def test_create_reduced_datum_duplicate_none_limit_bulk(self): + """Test that we cannot add duplicate ReducedDatums, even when limit is None. + Tests that this applies to bulk operations. + """ + # Ensure we can create one first + PhotometryReducedDatum.objects.create( + target=self.target, + timestamp=self.timestamp, + brightness=1.0, + bandpass="r", + limit=None, + ) + self.assertEqual(1, PhotometryReducedDatum.objects.count()) + datums = [ + PhotometryReducedDatum( + target=self.target, + timestamp=self.timestamp, + brightness=1.0, + bandpass="r", + limit=None, + ) + for _ in range(3) + ] + # This raises an IntegrityError because Python level validation does not occur in + # bulk operations, here we rely on the database level constraint to prevent duplicates + with self.assertRaises(IntegrityError): + PhotometryReducedDatum.objects.bulk_create(datums) + + def test_create_reduced_datum_duplicate_none_brightness_bulk(self): + """Test that we cannot add duplicate ReducedDatums, even when brightness is None. + Tests that this applies to bulk operations. + """ + # Ensure we can create one first + PhotometryReducedDatum.objects.create( + target=self.target, + timestamp=self.timestamp, + brightness=None, + bandpass="r", + limit=2.0, + ) + self.assertEqual(1, PhotometryReducedDatum.objects.count()) + datums = [ + PhotometryReducedDatum( + target=self.target, + timestamp=self.timestamp, + brightness=None, + bandpass="r", + limit=2.0, + ) + for _ in range(3) + ] + # This raises an IntegrityError because Python level validation does not occur in + # bulk operations, here we rely on the database level constraint to prevent duplicates + with self.assertRaises(IntegrityError): + PhotometryReducedDatum.objects.bulk_create(datums) + @override_settings(TOM_FACILITY_CLASSES=['tom_observations.tests.utils.FakeRoboticFacility'], TARGET_PERMISSIONS_ONLY=True, diff --git a/tom_setup/templates/tom_setup/settings.tmpl b/tom_setup/templates/tom_setup/settings.tmpl index 0c2f0d8c7..6b8f53838 100644 --- a/tom_setup/templates/tom_setup/settings.tmpl +++ b/tom_setup/templates/tom_setup/settings.tmpl @@ -332,10 +332,6 @@ PLOTLY_THEME = 'plotly_white' # Set this to False if you have a particularly large DB and paginated views are slow. SHOW_PAGINATION_INFO = True -# Silence system checks that are not applicable to how the TOM Toolkit is designed. -# These are likely temporary and can be removed in downstream TOMs if desired. -SILENCED_SYSTEM_CHECKS = ['models.W047'] - try: from local_settings import * # noqa except ImportError: