content: import datetime as dt
  from io import StringIO
  from unittest import mock
  
  from django.contrib.auth.models import User
  from django.contrib.gis.geos import MultiPolygon, Point, Polygon
  from django.db import IntegrityError
  from django.db.models.signals import post_save
  from django.test import TestCase, override_settings
  from django.utils import translation
  
  import pandas as pd
  from htimeseries import HTimeseries
  from model_mommy import mommy
  from parler.utils.context import switch_language
  
  from enhydris import models
  from enhydris.tests import TimeseriesDataMixin
  
  
  class TestTimeseriesMixin:
      @classmethod
      def _create_test_timeseries(cls, data=""):
          cls.station = mommy.make(
              models.Station,
              name="Celduin",
              original_srid=2100,
              geom=Point(x=21.06071, y=39.09518, srid=4326),
              altitude=219,
          )
          cls.timeseries_group = mommy.make(
              models.TimeseriesGroup,
              name="Daily temperature",
              gentity=cls.station,
              unit_of_measurement__symbol="mm",
              time_zone__code="IST",
              time_zone__utc_offset=330,
              variable__descr="Temperature",
              precision=1,
              remarks="This timeseries group rocks",
          )
          cls.timeseries = mommy.make(
              models.Timeseries, timeseries_group=cls.timeseries_group, time_step="H"
          )
          cls.timeseries.set_data(StringIO(data))
  
  
  class PersonTestCase(TestCase):
      def test_create(self):
          person = models.Person(last_name="Brown", first_name="Alice", initials="A.")
          person.save()
          self.assertEqual(models.Person.objects.first().last_name, "Brown")
  
      def test_update(self):
          mommy.make(models.Person)
          person = models.Person.objects.first()
          person.first_name = "Bob"
          person.save()
          self.assertEqual(models.Person.objects.first().first_name, "Bob")
  
      def test_delete(self):
          mommy.make(models.Person)
          person = models.Person.objects.first()
          person.delete()
          self.assertEqual(models.Person.objects.count(), 0)
  
      def test_str(self):
          person = mommy.make(
              models.Person, last_name="Brown", first_name="Alice", initials="A."
          )
          self.assertEqual(str(person), "Brown A.")
  
      def test_ordering_string(self):
          mommy.make(models.Person, last_name="Brown", first_name="Alice", initials="A.")
          person = models.Person.objects.first()
          self.assertEqual(person.ordering_string, "Brown Alice")
  
  
  class OrganizationTestCase(TestCase):
      def test_create(self):
          organization = models.Organization(name="Crooks Intl", acronym="Crooks")
          organization.save()
          self.assertEqual(models.Organization.objects.first().name, "Crooks Intl")
  
      def test_update(self):
          mommy.make(models.Organization)
          organization = models.Organization.objects.first()
          organization.acronym = "Crooks"
          organization.save()
          self.assertEqual(models.Organization.objects.first().acronym, "Crooks")
  
      def test_delete(self):
          mommy.make(models.Organization)
          organization = models.Organization.objects.first()
          organization.delete()
          self.assertEqual(models.Organization.objects.count(), 0)
  
      def test_str(self):
          org = mommy.make(models.Organization, name="Crooks Intl", acronym="Crooks")
          self.assertEqual(str(org), "Crooks")
  
      def test_ordering_string(self):
          mommy.make(models.Organization, name="Crooks Intl", acronym="Crooks")
          organization = models.Organization.objects.first()
          self.assertEqual(organization.ordering_string, "Crooks Intl")
  
  
  class VariableTestCase(TestCase):
      def test_create(self):
          gact = models.Variable(descr="Temperature")
          gact.save()
          self.assertEqual(models.Variable.objects.first().descr, "Temperature")
  
      def test_update(self):
          mommy.make(models.Variable, descr="Irrelevant")
          gact = models.Variable.objects.first()
          gact.descr = "Temperature"
          gact.save()
          self.assertEqual(models.Variable.objects.first().descr, "Temperature")
  
      def test_delete(self):
          mommy.make(models.Variable, descr="Temperature")
          gact = models.Variable.objects.first()
          gact.delete()
          self.assertEqual(models.Variable.objects.count(), 0)
  
      def test_str(self):
          gact = self._create_variable("Temperature", "Θερμοκρασία")
          self.assertEqual(str(gact), "Temperature")
          with switch_language(gact, "el"):
              self.assertEqual(str(gact), "Θερμοκρασία")
  
      def test_manager_includes_objects_with_missing_translations(self):
          variable = mommy.make(models.Variable, descr="hello")
          self.assertEqual(str(variable), "hello")
          with switch_language(variable, "el"):
              models.Variable.objects.get(id=variable.id)  # Shouldn't raise anything
  
      def test_sort(self):
          self._create_variable("Temperature", "Θερμοκρασία")
          self._create_variable("Humidity", "Υγρασία")
          self.assertEqual(
              [v.descr for v in models.Variable.objects.all()],
              ["Humidity", "Temperature"],
          )
          with translation.override("el"):
              self.assertEqual(
                  [v.descr for v in models.Variable.objects.all()],
                  ["Θερμοκρασία", "Υγρασία"],
              )
  
      def _create_variable(self, english_name, greek_name):
          mommy.make(models.Variable, descr=english_name)
          variable = models.Variable.objects.get(translations__descr=english_name)
          variable.translations.create(language_code="el", descr=greek_name)
          return variable
  
      @override_settings(
          ENHYDRIS_USERS_CAN_ADD_CONTENT=True,
          LANGUAGE_CODE="en",
          LANGUAGES={("en", "English"), ("el", "Ελληνικά")},
      )
      def test_translation_bug(self):
          # Normally Variable.__str__() should return a simple "return self.descr".
          # However, there's a tricky bug somewhere, most probably in django-parler, but I
          # can't nail it.  Sometimes, when there's no registered translation in the
          # active language, self.descr is None (when it should fall back to the fallback
          # language). It occurs when trying to visit /admin/enhydris/station/add/, the
          # active language is Greek, and one of the variables has an English translation
          # but not a Greek translation. We work around it by changing Variable.__str__()
          # to return whatever translation exists.
          #
          # Unfortunately, PARLER_LANGUAGES seems to not be overridable in tests;
          # therefore, if you change Variable.__str__() to a simple "return self.descr",
          # the only way to make this test fail is by manually specifying this in the
          # settings:
          #     PARLER_LANGUAGES={
          #         SITE_ID: [{"code": "en"}, {"code": "el"}],
          #         "default": {"fallbacks": ["en"], "hide_untranslated": True},
          #     }
          User.objects.create_user(
              username="alice", password="topsecret", is_active=True, is_staff=True
          )
          self.client.login(username="alice", password="topsecret")
          mommy.make(models.Variable, descr="pH")
          response = self.client.get(
              "/admin/enhydris/station/add/", HTTP_ACCEPT_LANGUAGE="el"
          )
          self.assertEqual(response.status_code, 200)
  
  
  class GentityFileTestCase(TestCase):
      def test_create(self):
          station = mommy.make(models.Station)
          gentity_file = models.GentityFile(gentity=station, descr="North view")
          gentity_file.save()
          self.assertEqual(models.GentityFile.objects.first().descr, "North view")
  
      def test_update(self):
          mommy.make(models.GentityFile)
          gentity_file = models.GentityFile.objects.first()
          gentity_file.descr = "North view"
          gentity_file.save()
          self.assertEqual(models.GentityFile.objects.first().descr, "North view")
  
      def test_delete(self):
          mommy.make(models.GentityFile)
          gentity_file = models.GentityFile.objects.first()
          gentity_file.delete()
          self.assertEqual(models.GentityFile.objects.count(), 0)
  
      def test_str(self):
          gentity_file = mommy.make(models.GentityFile, descr="North view")
          self.assertEqual(str(gentity_file), "North view")
  
      def test_related_station(self):
          station = mommy.make(models.Station)
          gentity_file = mommy.make(models.GentityFile, gentity=station)
          self.assertEqual(gentity_file.related_station, station)
  
      def test_related_station_is_empty_when_gentity_is_not_station(self):
          garea = mommy.make(models.Garea)
          gentity_file = mommy.make(models.GentityFile, gentity=garea)
          self.assertIsNone(gentity_file.related_station)
  
  
  class GentityImageTestCase(TestCase):
      def test_str_desc(self):
          image = mommy.make(models.GentityImage, descr="hello")
          self.assertEqual(str(image), "hello")
  
      def test_str_date(self):
          image = mommy.make(models.GentityImage, descr="", date=dt.datetime(2021, 1, 22))
          self.assertEqual(str(image), "2021-01-22")
  
      def test_str_id(self):
          image = mommy.make(models.GentityImage, descr="", date=None, id=85)
          self.assertEqual(str(image), "85")
  
  
  class GentityEventTestCase(TestCase):
      def test_create(self):
          station = mommy.make(models.Station)
          type = mommy.make(models.EventType)
          gentity_event = models.GentityEvent(
              gentity=station,
              type=type,
              date=dt.datetime.now(),
              user="Alice",
              report="Station exploded",
          )
          gentity_event.save()
          self.assertEqual(models.GentityEvent.objects.first().report, "Station exploded")
  
      def test_update(self):
          mommy.make(models.GentityEvent)
          gentity_event = models.GentityEvent.objects.first()
          gentity_event.report = "Station exploded"
          gentity_event.save()
          self.assertEqual(models.GentityEvent.objects.first().report, "Station exploded")
  
      def test_delete(self):
          mommy.make(models.GentityEvent)
          gentity_event = models.GentityEvent.objects.first()
          gentity_event.delete()
          self.assertEqual(models.GentityEvent.objects.count(), 0)
  
      def test_str(self):
          gentity_event = mommy.make(
              models.GentityEvent, date="2018-11-14", type__descr="Explosion"
          )
          self.assertEqual(str(gentity_event), "2018-11-14 Explosion")
  
      def test_related_station(self):
          station = mommy.make(models.Station)
          gentity_event = mommy.make(models.GentityEvent, gentity=station)
          self.assertEqual(gentity_event.related_station, station)
  
      def test_related_station_is_empty_when_gentity_is_not_station(self):
          garea = mommy.make(models.Garea)
          gentity_event = mommy.make(models.GentityEvent, gentity=garea)
          self.assertIsNone(gentity_event.related_station)
  
  
  class GareaTestCase(TestCase):
      def test_create(self):
          category = mommy.make(models.GareaCategory)
          garea = models.Garea(
              name="Esgalduin",
              category=category,
              geom=MultiPolygon(Polygon(((30, 20), (45, 40), (10, 40), (30, 20)))),
          )
          garea.save()
          self.assertEqual(models.Garea.objects.first().name, "Esgalduin")
  
      def test_update(self):
          mommy.make(models.Garea)
          garea = models.Garea.objects.first()
          garea.name = "Esgalduin"
          garea.save()
          self.assertEqual(models.Garea.objects.first().name, "Esgalduin")
  
      def test_delete(self):
          mommy.make(models.Garea)
          garea = models.Garea.objects.first()
          garea.delete()
          self.assertEqual(models.Garea.objects.count(), 0)
  
      def test_str(self):
          garea = mommy.make(models.Garea, name="Esgalduin")
          self.assertEqual(str(garea), "Esgalduin")
  
  
  class StationTestCase(TestCase):
      def test_create(self):
          person = mommy.make(models.Person)
          station = models.Station(
              owner=person,
              name="Hobbiton",
              geom=Point(x=21.06071, y=39.09518, srid=4326),
          )
          station.save()
          self.assertEqual(models.Station.objects.first().name, "Hobbiton")
  
      def test_update(self):
          mommy.make(models.Station)
          station = models.Station.objects.first()
          station.name = "Hobbiton"
          station.save()
          self.assertEqual(models.Station.objects.first().name, "Hobbiton")
  
      def test_delete(self):
          mommy.make(models.Station)
          station = models.Station.objects.first()
          station.delete()
          self.assertEqual(models.Station.objects.count(), 0)
  
      def test_str(self):
          station = mommy.make(models.Station, name="Hobbiton")
          self.assertEqual(str(station), "Hobbiton")
  
  
  class StationOriginalCoordinatesTestCase(TestCase):
      def setUp(self):
          mommy.make(
              models.Station,
              name="Komboti",
              geom=Point(x=21.06071, y=39.09518, srid=4326),
              original_srid=2100,
          )
          self.station = models.Station.objects.get(name="Komboti")
  
      def test_original_abscissa(self):
          self.assertAlmostEqual(self.station.original_abscissa(), 245648.96, places=1)
  
      def test_original_ordinate(self):
          self.assertAlmostEqual(self.station.original_ordinate(), 4331165.20, places=1)
  
  
  class StationOriginalCoordinatesWithNullSridTestCase(TestCase):
      def setUp(self):
          mommy.make(
              models.Station,
              name="Komboti",
              geom=Point(x=21.06071, y=39.09518, srid=4326),
              original_srid=None,
          )
          self.station = models.Station.objects.get(name="Komboti")
  
      def test_original_abscissa(self):
          self.assertAlmostEqual(self.station.original_abscissa(), 21.06071)
  
      def test_original_ordinate(self):
          self.assertAlmostEqual(self.station.original_ordinate(), 39.09518)
  
  
  class StationLastUpdateTestCase(TestCase):
      def setUp(self):
          self.station = mommy.make(models.Station)
          self.time_zone = mommy.make(models.TimeZone, code="EET", utc_offset=120)
          self.timeseries_group = mommy.make(
              models.TimeseriesGroup,
              gentity=self.station,
              time_zone=self.time_zone,
              variable__descr="irrelevant",
              precision=2,
          )
  
      def _create_timeseries(
          self,
          ye=None,
          mo=None,
          da=None,
          ho=None,
          mi=None,
          type=models.Timeseries.INITIAL,
      ):
          if ye:
              end_date_utc = dt.datetime(ye, mo, da, ho, mi, tzinfo=dt.timezone.utc)
          else:
              end_date_utc = None
          timeseries = mommy.make(
              models.Timeseries, timeseries_group=self.timeseries_group, type=type
          )
          if end_date_utc:
              timeseries.timeseriesrecord_set.create(
                  timestamp=end_date_utc, value=0, flags=""
              )
  
      def test_last_update_naive_when_all_timeseries_have_end_date(self):
          self._create_timeseries(2019, 7, 24, 11, 26, type=models.Timeseries.INITIAL)
          self._create_timeseries(2019, 7, 23, 5, 10, type=models.Timeseries.CHECKED)
          self.assertEqual(
              self.station.last_update_naive, dt.datetime(2019, 7, 24, 13, 26)
          )
  
      def test_last_update_naive_when_one_timeseries_has_no_data(self):
          self._create_timeseries(2019, 7, 24, 11, 26, type=models.Timeseries.INITIAL)
          self._create_timeseries(type=models.Timeseries.CHECKED)
          self.assertEqual(
              self.station.last_update_naive, dt.datetime(2019, 7, 24, 13, 26)
          )
  
      def test_last_update_naive_when_all_timeseries_has_no_data(self):
          self._create_timeseries(type=models.Timeseries.INITIAL)
          self._create_timeseries(type=models.Timeseries.CHECKED)
          self.assertIsNone(self.station.last_update_naive)
  
      def test_last_update_naive_when_no_timeseries(self):
          self.assertIsNone(self.station.last_update_naive)
  
      def test_last_update(self):
          self._create_timeseries(2019, 7, 24, 11, 26, type=models.Timeseries.INITIAL)
          tzinfo = dt.timezone(dt.timedelta(hours=2), "EET")
          self.assertEqual(
              self.station.last_update, dt.datetime(2019, 7, 24, 13, 26, tzinfo=tzinfo)
          )
  
      def test_last_update_cache_when_atleast_one_timeseries_has_end_date(self):
          # Since it's not possible to cache `None` values, ensure to create
          # at least one timeseries with an end date value.
          self._create_timeseries(2019, 7, 24, 11, 26, type=models.Timeseries.INITIAL)
  
          # Make sure to fetch the `end_date` value of the timeseries
          timeseries = models.Timeseries.objects.filter(
              timeseries_group__gentity_id=self.station.id
          )
          for t in timeseries:
              t.end_date
  
          with self.assertNumQueries(1):
              self.station.last_update
  
          station = models.Station.objects.get(id=self.station.id)
          with self.assertNumQueries(0):
              station.last_update
  
      def test_last_update_naive_cace_when_atleast_one_timeseries_has_end_date(self):
          # Since it's not possible to cache `None` values, ensure to create
          # at least one timeseries with an end date value.
          self._create_timeseries(2019, 7, 24, 11, 26, type=models.Timeseries.INITIAL)
  
          # Make sure to fetch the `end_date` value of the timeseries
          timeseries = models.Timeseries.objects.filter(
              timeseries_group__gentity_id=self.station.id
          )
          for t in timeseries:
              t.end_date
  
          with self.assertNumQueries(1):
              self.station.last_update_naive
  
          station = models.Station.objects.get(id=self.station.id)
          with self.assertNumQueries(0):
              station.last_update_naive
  
  
  class UnitOfMeasurementTestCase(TestCase):
      def test_str(self):
          unit = mommy.make(models.UnitOfMeasurement, symbol="mm")
          self.assertEqual(str(unit), "mm")
  
      def test_str_when_symbol_is_empty(self):
          unit = mommy.make(models.UnitOfMeasurement, symbol="")
          self.assertEqual(str(unit), str(unit.id))
  
  
  class TimeZoneTestCase(TestCase):
      def test_create(self):
          time_zone = models.TimeZone(code="EET", utc_offset=120)
          time_zone.save()
          self.assertEqual(models.TimeZone.objects.first().code, "EET")
  
      def test_update(self):
          mommy.make(models.TimeZone)
          time_zone = models.TimeZone.objects.first()
          time_zone.code = "EET"
          time_zone.save()
          self.assertEqual(models.TimeZone.objects.first().code, "EET")
  
      def test_delete(self):
          mommy.make(models.TimeZone)
          time_zone = models.TimeZone.objects.first()
          time_zone.delete()
          self.assertEqual(models.TimeZone.objects.count(), 0)
  
      def test_str(self):
          time_zone = mommy.make(models.TimeZone, code="EET", utc_offset=120)
          self.assertEqual(str(time_zone), "EET (UTC+0200)")
  
      def test_as_tzinfo(self):
          time_zone = mommy.make(models.TimeZone, code="EET", utc_offset=120)
          self.assertEqual(time_zone.as_tzinfo, dt.timezone(dt.timedelta(hours=2), "EET"))
  
  
  class TimeseriesGroupGetNameTestCase(TestCase):
      def setUp(self):
          self.timeseries_group = mommy.make(
              models.TimeseriesGroup, variable__descr="Temperature", name=""
          )
  
      def test_get_name_when_name_is_blank(self):
          self.assertEqual(self.timeseries_group.get_name(), "Temperature")
  
      def test_get_name_when_name_is_not_blank(self):
          self.timeseries_group.name = "Temperature from sensor 1"
          self.assertEqual(self.timeseries_group.get_name(), "Temperature from sensor 1")
  
      def test_get_name_when_translations_are_inactive(self):
          with translation.override(None):
              self.timeseries_group.variable._current_language = None
              self.assertEqual(
                  self.timeseries_group.get_name(),
                  f"Timeseries group {self.timeseries_group.id}",
              )
  
  
  class TimeseriesGroupDefaultTimeseriesTestCase(TestCase):
      def setUp(self):
          self.timeseries_group = mommy.make(
              models.TimeseriesGroup, variable__descr="Temperature", name=""
          )
          self.initial_timeseries = self._make_timeseries(models.Timeseries.INITIAL)
          self.checked_timeseries = self._make_timeseries(models.Timeseries.CHECKED)
          self.regularized_timeseries = self._make_timeseries(
              models.Timeseries.REGULARIZED
          )
  
      def _make_timeseries(self, type):
          return mommy.make(
              models.Timeseries, timeseries_group=self.timeseries_group, type=type
          )
  
      def test_returns_regularized(self):
          self.assertEqual(
              self.timeseries_group.default_timeseries, self.regularized_timeseries
          )
  
      def test_returns_checked(self):
          self.regularized_timeseries.delete()
          self.assertEqual(
              self.timeseries_group.default_timeseries, self.checked_timeseries
          )
  
      def test_returns_initial(self):
          self.regularized_timeseries.delete()
          self.checked_timeseries.delete()
          self.assertEqual(
              self.timeseries_group.default_timeseries, self.initial_timeseries
          )
  
      def test_returns_none(self):
          self.regularized_timeseries.delete()
          self.checked_timeseries.delete()
          self.initial_timeseries.delete()
          self.assertIsNone(self.timeseries_group.default_timeseries)
  
      def test_caching(self):
          with self.assertNumQueries(1):
              self.timeseries_group.default_timeseries
          with self.assertNumQueries(0):
              self.timeseries_group.default_timeseries
  
      def test_num_queries(self):
          with self.assertNumQueries(2):
              # The following should cause two queries.
              group = models.TimeseriesGroup.objects.prefetch_related(
                  "timeseries_set"
              ).first()
              # The following should cause no queries since the time series have
              # been prefetched.
              group.default_timeseries
  
  
  class TimeseriesGroupStartAndEndDateTestCase(TestCase, TimeseriesDataMixin):
      def setUp(self):
          self.create_timeseries()
  
      def test_start_date(self):
          self.assertEqual(
              self.timeseries_group.start_date,
              dt.datetime(2017, 11, 23, 17, 23, tzinfo=self.time_zone.as_tzinfo),
          )
  
      def test_start_date_cache(self):
          # Make sure to retrieve the `default_timeseries` first.
          self.timeseries_group.default_timeseries
  
          with self.assertNumQueries(1):
              self.timeseries_group.start_date
  
          timeseries_group = models.TimeseriesGroup.objects.get(
              id=self.timeseries_group.id
          )
          with self.assertNumQueries(0):
              timeseries_group.start_date
  
      def test_end_date(self):
          self.assertEqual(
              self.timeseries_group.end_date,
              dt.datetime(2018, 11, 25, 1, 0, tzinfo=self.time_zone.as_tzinfo),
          )
  
      def test_end_date_cache(self):
          # Make sure to retrieve the `default_timeseries` first.
          self.timeseries_group.default_timeseries
  
          with self.assertNumQueries(1):
              self.timeseries_group.end_date
  
          timeseries_group = models.TimeseriesGroup.objects.get(
              id=self.timeseries_group.id
          )
          with self.assertNumQueries(0):
              timeseries_group.end_date
  
      def test_start_date_when_timeseries_is_empty(self):
          self.timeseries.set_data(StringIO(""))
          self.assertIsNone(self.timeseries_group.start_date)
  
      def test_end_date_when_timeseries_is_empty(self):
          self.timeseries.set_data(StringIO(""))
          self.assertIsNone(self.timeseries_group.end_date)
  
      def test_start_date_when_timeseries_does_not_exist(self):
          self.timeseries.delete()
          self.assertIsNone(self.timeseries_group.start_date)
  
      def test_end_date_when_timeseries_does_not_exist(self):
          self.timeseries.delete()
          self.assertIsNone(self.timeseries_group.end_date)
  
      def test_start_date_naive(self):
          self.assertEqual(
              self.timeseries_group.start_date_naive, dt.datetime(2017, 11, 23, 17, 23)
          )
  
      def test_start_date_naive_cache(self):
          # Make sure to have access to the `start_date` first
          self.timeseries_group.start_date
  
          timeseries_group = models.TimeseriesGroup.objects.get(
              id=self.timeseries_group.id
          )
          with self.assertNumQueries(0):
              timeseries_group.start_date_naive
  
      def test_end_date_naive(self):
          self.assertEqual(
              self.timeseries_group.end_date_naive, dt.datetime(2018, 11, 25, 1, 0)
          )
  
      def test_end_date_naive_cache(self):
          # Make sure to have access to the `end_date` first
          self.timeseries_group.end_date
  
          timeseries_group = models.TimeseriesGroup.objects.get(
              id=self.timeseries_group.id
          )
          with self.assertNumQueries(0):
              timeseries_group.end_date_naive
  
      def test_start_date_naive_when_timeseries_is_empty(self):
          self.timeseries.set_data(StringIO(""))
          self.assertIsNone(self.timeseries_group.start_date_naive)
  
      def test_end_date_naive_when_timeseries_is_empty(self):
          self.timeseries.set_data(StringIO(""))
          self.assertIsNone(self.timeseries_group.end_date_naive)
  
  
  class TimeseriesTestCase(TestCase):
      def test_create(self):
          timeseries_group = mommy.make(models.TimeseriesGroup)
          timeseries = models.Timeseries(
              type=models.Timeseries.AGGREGATED, timeseries_group=timeseries_group
          )
          timeseries.save()
          self.assertEqual(
              models.Timeseries.objects.first().type, models.Timeseries.AGGREGATED
          )
  
      def test_update(self):
          mommy.make(models.Timeseries, type=models.Timeseries.INITIAL)
          timeseries = models.Timeseries.objects.first()
          timeseries.type = models.Timeseries.AGGREGATED
          timeseries.save()
          self.assertEqual(
              models.Timeseries.objects.first().type, models.Timeseries.AGGREGATED
          )
  
      def test_delete(self):
          mommy.make(models.Timeseries)
          timeseries = models.Timeseries.objects.first()
          timeseries.delete()
          self.assertEqual(models.Timeseries.objects.count(), 0)
  
      def test_str_initial(self):
          self._test_str(type=models.Timeseries.INITIAL, result="Initial")
  
      def test_str_checked(self):
          self._test_str(type=models.Timeseries.CHECKED, result="Checked")
  
      def test_str_regularized(self):
          self._test_str(type=models.Timeseries.REGULARIZED, result="Regularized")
  
      def test_str_aggregated(self):
          self._test_str(type=models.Timeseries.AGGREGATED, result="Aggregated (H)")
  
      def _make_timeseries(self, timeseries_group, type):
          return mommy.make(
              models.Timeseries,
              timeseries_group=timeseries_group,
              type=type,
              time_step="H",
          )
  
      def _test_str(self, type, result):
          timeseries_group = mommy.make(models.TimeseriesGroup, name="Temperature")
          timeseries = self._make_timeseries(timeseries_group, type)
          self.assertEqual(str(timeseries), result)
  
      def test_only_one_initial_per_group(self):
          timeseries_group = mommy.make(models.TimeseriesGroup, name="Temperature")
          self._make_timeseries(timeseries_group, models.Timeseries.INITIAL)
          with self.assertRaises(IntegrityError):
              models.Timeseries(
                  timeseries_group=timeseries_group,
                  type=models.Timeseries.INITIAL,
                  time_step="D",
              ).save()
  
      def test_only_one_checked_per_group(self):
          timeseries_group = mommy.make(models.TimeseriesGroup, name="Temperature")
          self._make_timeseries(timeseries_group, models.Timeseries.CHECKED)
          with self.assertRaises(IntegrityError):
              models.Timeseries(
                  timeseries_group=timeseries_group,
                  type=models.Timeseries.CHECKED,
                  time_step="D",
              ).save()
  
      def test_only_one_regularized_per_group(self):
          timeseries_group = mommy.make(models.TimeseriesGroup, name="Temperature")
          self._make_timeseries(timeseries_group, models.Timeseries.REGULARIZED)
          with self.assertRaises(IntegrityError):
              models.Timeseries(
                  timeseries_group=timeseries_group,
                  type=models.Timeseries.REGULARIZED,
                  time_step="D",
              ).save()
  
      def test_uniqueness(self):
          timeseries_group = mommy.make(models.TimeseriesGroup, name="Temperature")
          self._make_timeseries(timeseries_group, models.Timeseries.AGGREGATED)
          with self.assertRaises(IntegrityError):
              models.Timeseries(
                  timeseries_group=timeseries_group,
                  type=models.Timeseries.AGGREGATED,
                  time_step="H",
              ).save()
  
      def test_many_aggregated_per_group(self):
          timeseries_group = mommy.make(models.TimeseriesGroup, name="Temperature")
          self._make_timeseries(timeseries_group, models.Timeseries.AGGREGATED)
          models.Timeseries(
              timeseries_group=timeseries_group,
              type=models.Timeseries.AGGREGATED,
              time_step="D",
          ).save()
  
  
  def make_timeseries(*, start_date, end_date, **kwargs):
      """Make a test timeseries, setting start_date and end_date.
      This is essentially the same as mommy.make(models.Timeseries, **kwargs), except
      that it also creates two records with the specified dates.
      """
      result = mommy.make(models.Timeseries, **kwargs)
      result.timeseriesrecord_set.create(timestamp=start_date, value=0, flags="")
      result.timeseriesrecord_set.create(timestamp=end_date, value=0, flags="")
      return result
  
  
  class TimeseriesDatesTestCase(TestCase):
      def setUp(self):
          self.timeseries = make_timeseries(
              timeseries_group__time_zone__utc_offset=120,
              timeseries_group__precision=2,
              start_date=dt.datetime(2018, 11, 15, 16, 0, tzinfo=dt.timezone.utc),
              end_date=dt.datetime(2018, 11, 17, 23, 0, tzinfo=dt.timezone.utc),
          )
  
      def test_start_date(self):
          self.assertEqual(
              self.timeseries.start_date,
              dt.datetime(
                  2018,
                  11,
                  15,
                  18,
                  0,
                  tzinfo=self.timeseries.timeseries_group.time_zone.as_tzinfo,
              ),
          )
  
      def test_start_date_tzinfo(self):
          self.assertEqual(
              self.timeseries.start_date.tzinfo,
              self.timeseries.timeseries_group.time_zone.as_tzinfo,
          )
  
      def test_start_date_cache(self):
          with self.assertNumQueries(1):
              self.timeseries.start_date
  
          timeseries = models.Timeseries.objects.get(id=self.timeseries.id)
          with self.assertNumQueries(0):
              timeseries.start_date
  
      def test_end_date(self):
          self.assertEqual(
              self.timeseries.end_date,
              dt.datetime(
                  2018,
                  11,
                  18,
                  1,
                  0,
                  tzinfo=self.timeseries.timeseries_group.time_zone.as_tzinfo,
              ),
          )
  
      def test_end_date_tzinfo(self):
          self.assertEqual(
              self.timeseries.end_date.tzinfo,
              self.timeseries.timeseries_group.time_zone.as_tzinfo,
          )
  
      def test_end_date_cache(self):
          with self.assertNumQueries(1):
              self.timeseries.end_date
  
          timeseries = models.Timeseries.objects.get(id=self.timeseries.id)
          with self.assertNumQueries(0):
              timeseries.end_date
  
      def test_start_date_naive(self):
          self.assertEqual(
              self.timeseries.start_date_naive, dt.datetime(2018, 11, 15, 18, 0)
          )
  
      def test_start_date_naive_cache(self):
          with self.assertNumQueries(1):
              self.timeseries.start_date_naive
  
          timeseries = models.Timeseries.objects.get(id=self.timeseries.id)
          with self.assertNumQueries(0):
              timeseries.start_date_naive
  
      def test_end_date_naive(self):
          self.assertEqual(
              self.timeseries.end_date_naive, dt.datetime(2018, 11, 18, 1, 0)
          )
  
      def test_end_date_naive_cache(self):
          with self.assertNumQueries(1):
              self.timeseries.end_date_naive
  
          timeseries = models.Timeseries.objects.get(id=self.timeseries.id)
          with self.assertNumQueries(0):
              timeseries.end_date_naive
  
  
  class DataTestCase(TestCase, TestTimeseriesMixin):
      @classmethod
      def setUpClass(cls):
          super().setUpClass()
          cls._create_test_timeseries("2017-11-23 17:23,1,\n2018-11-25 01:00,2,\n")
          cls.expected_result = pd.DataFrame(
              data={"value": [1.0, 2.0], "flags": ["", ""]},
              columns=["value", "flags"],
              index=[dt.datetime(2017, 11, 23, 17, 23), dt.datetime(2018, 11, 25, 1, 0)],
          )
          cls.expected_result.index.name = "date"
  
  
  class TimeseriesGetDataTestCase(DataTestCase):
      @classmethod
      def setUpClass(cls):
          super().setUpClass()
          cls.data = cls.timeseries.get_data()
  
      def test_abscissa(self):
          self.assertAlmostEqual(self.data.location["abscissa"], 245648.96, places=2)
  
      def test_ordinate(self):
          self.assertAlmostEqual(self.data.location["ordinate"], 4331165.20, places=2)
  
      def test_srid(self):
          self.assertAlmostEqual(self.data.location["srid"], 2100)
  
      def test_altitude(self):
          self.assertAlmostEqual(self.data.location["altitude"], 219)
  
      def test_time_step(self):
          self.assertEqual(self.data.time_step, "H")
  
      def test_unit(self):
          self.assertEqual(self.data.unit, "mm")
  
      def test_title(self):
          self.assertEqual(self.data.title, "Daily temperature")
  
      def test_timezone(self):
          self.assertEqual(self.data.timezone, "IST (UTC+0530)")
  
      def test_negative_timezone(self):
          self.timeseries.timeseries_group.time_zone.code = "NST"
          self.timeseries.timeseries_group.time_zone.utc_offset = -210
          data = self.timeseries.get_data()
          self.assertEqual(data.timezone, "NST (UTC-0330)")
  
      def test_variable(self):
          self.assertEqual(self.data.variable, "Temperature")
  
      def test_precision(self):
          self.assertEqual(self.data.precision, 1)
  
      def test_comment(self):
          self.assertEqual(self.data.comment, "Celduin\n\nThis timeseries group rocks")
  
      def test_location_is_none(self):
          self.timeseries.timeseries_group.gentity.geom = None
          data = self.timeseries.get_data()
          self.assertIsNone(data.location)
  
      def test_data(self):
          pd.testing.assert_frame_equal(self.data.data, self.expected_result)
  
  
  class TimeseriesGetDataWithNullTestCase(TestCase, TestTimeseriesMixin):
      @classmethod
      def setUpClass(cls):
          super().setUpClass()
          cls._create_test_timeseries("2017-11-23 17:23,,\n2018-11-25 01:00,2,\n")
          cls.expected_result = pd.DataFrame(
              data={"value": [float("NaN"), 2.0], "flags": ["", ""]},
              columns=["value", "flags"],
              index=[dt.datetime(2017, 11, 23, 17, 23), dt.datetime(2018, 11, 25, 1, 0)],
          )
          cls.expected_result.index.name = "date"
          cls.data = cls.timeseries.get_data()
  
      def test_data(self):
          pd.testing.assert_frame_equal(self.data.data, self.expected_result)
  
  
  class TimeseriesGetDataWithStartAndEndDateTestCase(DataTestCase):
      def _check(self, start_index=None, end_index=None):
          """Check self.htimeseries.data against the initial timeseries sliced from
          start_index to end_index.
          """
          full_result = pd.DataFrame(
              data={"value": [1.0, 2.0], "flags": ["", ""]},
              columns=["value", "flags"],
              index=[dt.datetime(2017, 11, 23, 17, 23), dt.datetime(2018, 11, 25, 1, 0)],
          )
          full_result.index.name = "date"
          expected_result = full_result.iloc[start_index:end_index]
          pd.testing.assert_frame_equal(self.ahtimeseries.data, expected_result)
  
      def test_with_start_date_just_before_start_of_timeseries(self):
          tzinfo = self.timeseries.timeseries_group.time_zone.as_tzinfo
          start_date = dt.datetime(2017, 11, 23, 17, 22, tzinfo=tzinfo)
          self.ahtimeseries = self.timeseries.get_data(start_date=start_date)
          self._check()
  
      def test_with_start_date_on_start_of_timeseries(self):
          tzinfo = self.timeseries.timeseries_group.time_zone.as_tzinfo
          start_date = dt.datetime(2017, 11, 23, 17, 23, tzinfo=tzinfo)
          self.ahtimeseries = self.timeseries.get_data(start_date=start_date)
          self._check()
  
      def test_with_start_date_just_after_start_of_timeseries(self):
          tzinfo = self.timeseries.timeseries_group.time_zone.as_tzinfo
          start_date = dt.datetime(2017, 11, 23, 17, 24, tzinfo=tzinfo)
          self.ahtimeseries = self.timeseries.get_data(start_date=start_date)
          self._check(start_index=1)
  
      def test_with_end_date_just_after_end_of_timeseries(self):
          tzinfo = self.timeseries.timeseries_group.time_zone.as_tzinfo
          end_date = dt.datetime(2018, 11, 25, 1, 1, tzinfo=tzinfo)
          self.ahtimeseries = self.timeseries.get_data(end_date=end_date)
          self._check()
  
      def test_with_end_date_on_end_of_timeseries(self):
          tzinfo = self.timeseries.timeseries_group.time_zone.as_tzinfo
          end_date = dt.datetime(2018, 11, 25, 1, 0, tzinfo=tzinfo)
          self.ahtimeseries = self.timeseries.get_data(end_date=end_date)
          self._check()
  
      def test_with_end_date_just_before_end_of_timeseries(self):
          tzinfo = self.timeseries.timeseries_group.time_zone.as_tzinfo
          end_date = dt.datetime(2018, 11, 25, 0, 59, tzinfo=tzinfo)
          self.ahtimeseries = self.timeseries.get_data(end_date=end_date)
          self._check(end_index=1)
  
  
  @override_settings(
      CACHES={"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
  )
  class TimeseriesGetDataCacheTestCase(DataTestCase):
      def test_cache(self):
          # Make sure we've accessed gpoint already, otherwise it screws up the number of
          # queries later
          self.timeseries.timeseries_group.gentity.gpoint.altitude
  
          self._get_data_and_check_num_queries(1)
          self._get_data_and_check_num_queries(0)
  
          # Check cache invalidation
          self.timeseries.save()
          self._get_data_and_check_num_queries(1)
  
      def _get_data_and_check_num_queries(self, num_queries):
          with self.assertNumQueries(num_queries):
              data = self.timeseries.get_data()
          pd.testing.assert_frame_equal(data.data, self.expected_result)
  
  
  class TimeseriesSetDataTestCase(TestCase, TestTimeseriesMixin):
      def setUp(self):
          self._create_test_timeseries()
  
      def test_call_with_file_object(self):
          self.returned_length = self.timeseries.set_data(
              StringIO("2017-11-23 17:23,1,\n" "2018-11-25 01:00,2,\n")
          )
          self._check_results()
  
      def test_call_with_dataframe(self):
          self.returned_length = self.timeseries.set_data(self._get_dataframe())
          self._check_results()
  
      def test_call_with_htimeseries(self):
          self.returned_length = self.timeseries.set_data(
              HTimeseries(self._get_dataframe())
          )
          self._check_results()
  
      def _get_dataframe(self):
          result = pd.DataFrame(
              data={"value": [1.0, 2.0], "flags": ["", ""]},
              columns=["value", "flags"],
              index=[dt.datetime(2017, 11, 23, 17, 23), dt.datetime(2018, 11, 25, 1, 0)],
          )
          result.index.name = "date"
          return result
  
      def _check_results(self):
          self.assertEqual(self.returned_length, 2)
          self.assertEqual(
              list(self.timeseries.timeseriesrecord_set.values()),
              [
                  {
                      "timeseries_id": self.timeseries.id,
                      "timestamp": dt.datetime(
                          2017,
                          11,
                          23,
                          17,
                          23,
                          tzinfo=models.TimeZone(code="IST", utc_offset=330).as_tzinfo,
                      ),
                      "value": 1.0,
                      "flags": "",
                  },
                  {
                      "timeseries_id": self.timeseries.id,
                      "timestamp": dt.datetime(
                          2018,
                          11,
                          25,
                          1,
                          0,
                          tzinfo=models.TimeZone(code="IST", utc_offset=330).as_tzinfo,
                      ),
                      "value": 2.0,
                      "flags": "",
                  },
              ],
          )
  
  
  class TimeseriesAppendDataTestCase(TestCase, TestTimeseriesMixin):
      def setUp(self):
          self._create_test_timeseries("2016-01-01 00:00,42,\n")
  
      def test_call_with_file_object(self):
          returned_length = self.timeseries.append_data(
              StringIO("2017-11-23 17:23,1,\n" "2018-11-25 01:00,2,\n")
          )
          self.assertEqual(returned_length, 2)
          self._assert_wrote_data()
  
      def test_call_with_dataframe(self):
          returned_length = self.timeseries.append_data(self._get_dataframe())
          self.assertEqual(returned_length, 2)
          self._assert_wrote_data()
  
      def test_call_with_htimeseries(self):
          returned_length = self.timeseries.append_data(
              HTimeseries(self._get_dataframe())
          )
          self.assertEqual(returned_length, 2)
          self._assert_wrote_data()
  
      def _get_dataframe(self):
          result = pd.DataFrame(
              data={"value": [1.0, 2.0], "flags": ["", ""]},
              columns=["value", "flags"],
              index=[dt.datetime(2017, 11, 23, 17, 23), dt.datetime(2018, 11, 25, 1, 0)],
          )
          result.index.name = "date"
          return result
  
      def _assert_wrote_data(self):
          expected_result = pd.DataFrame(
              data={"value": [42.0, 1.0, 2.0], "flags": ["", "", ""]},
              columns=["value", "flags"],
              index=[
                  dt.datetime(2016, 1, 1, 0, 0),
                  dt.datetime(2017, 11, 23, 17, 23),
                  dt.datetime(2018, 11, 25, 1, 0),
              ],
          )
          expected_result.index.name = "date"
          pd.testing.assert_frame_equal(self.timeseries.get_data().data, expected_result)
  
  
  class TimeseriesAppendDataToEmptyTimeseriesTestCase(TestCase, TestTimeseriesMixin):
      def setUp(self):
          self._create_test_timeseries()
  
      def test_call_with_dataframe(self):
          returned_length = self.timeseries.append_data(self._get_dataframe())
          self.assertEqual(returned_length, 2)
          self._assert_wrote_data()
  
      def _get_dataframe(self):
          result = pd.DataFrame(
              data={"value": [1.0, 2.0], "flags": ["", ""]},
              columns=["value", "flags"],
              index=[dt.datetime(2017, 11, 23, 17, 23), dt.datetime(2018, 11, 25, 1, 0)],
          )
          result.index.name = "date"
          return result
  
      def _assert_wrote_data(self):
          pd.testing.assert_frame_equal(
              self.timeseries.get_data().data, self._get_dataframe()
          )
  
  
  class TimeseriesAppendErrorTestCase(TestCase, TestTimeseriesMixin):
      def test_does_not_update_if_data_to_append_are_not_later(self):
          self._create_test_timeseries("2018-01-01 00:00,42,\n")
          with self.assertRaises(IntegrityError):
              self.timeseries.append_data(
                  StringIO("2017-11-23 17:23,1,\n2018-11-25 01:00,2,\n")
              )
  
  
  class TimeseriesGetLastRecordAsStringTestCase(TestCase, TestTimeseriesMixin):
      def test_when_record_exists(self):
          self._create_test_timeseries("2017-11-23 17:23,1,\n2018-11-25 01:00,2,\n")
          self.assertEqual(
              self.timeseries.get_last_record_as_string(), "2018-11-25 01:00,2.0,"
          )
  
      def test_when_record_does_not_exist(self):
          self._create_test_timeseries()
          self.assertEqual(self.timeseries.get_last_record_as_string(), "")
  
  
  class TimeseriesExecutesTriggersUponAddingRecordsTestCase(DataTestCase):
      def setUp(self):
          self.trigger = mock.MagicMock()
          post_save.connect(self.trigger, sender="enhydris.Timeseries")
  
      def tearDown(self):
          post_save.disconnect(self.trigger, sender="enhydris.Timeseries")
  
      def test_calls_trigger_upon_setting_data(self):
          self.timeseries.set_data(StringIO("2020-10-26 09:34,18,\n"))
          self.trigger.assert_called()
  
      def test_calls_trigger_upon_appending_data(self):
          self.timeseries.append_data(StringIO("2020-10-26 09:34,18,\n"))
          self.trigger.assert_called()
  
  
  class TimestepTestCase(TestCase):
      def setUp(self):
          self.timeseries = mommy.make(models.Timeseries)
  
      def set_time_step(self, time_step):
          self.timeseries.time_step = time_step
          self.timeseries.save()
  
      def test_min(self):
          self.set_time_step("27min")
          self.assertEqual(models.Timeseries.objects.first().time_step, "27min")
  
      def test_hour(self):
          self.set_time_step("3H")
          self.assertEqual(models.Timeseries.objects.first().time_step, "3H")
  
      def test_day(self):
          self.set_time_step("3D")
          self.assertEqual(models.Timeseries.objects.first().time_step, "3D")
  
      def test_month(self):
          self.set_time_step("3M")
          self.assertEqual(models.Timeseries.objects.first().time_step, "3M")
  
      def test_3Y(self):
          self.set_time_step("3Y")
          self.assertEqual(models.Timeseries.objects.first().time_step, "3Y")
  
      def test_Y(self):
          self.set_time_step("Y")
          self.assertEqual(models.Timeseries.objects.first().time_step, "Y")
  
      def test_garbage(self):
          with self.assertRaisesRegex(ValueError, '"hello" is not a valid time step'):
              self.set_time_step("hello")
  
      def test_wrong_number(self):
          with self.assertRaisesRegex(ValueError, '"FM" is not a valid time step'):
              self.set_time_step("FM")
  
      def test_wrong_unit(self):
          with self.assertRaisesRegex(ValueError, '"3B" is not a valid time step'):
              self.set_time_step("3B")
  
  
  class TimeseriesRecordTestCase(TestCase, TestTimeseriesMixin):
      def test_str(self):
          self._create_test_timeseries("2017-11-23 17:23,3.14159,\n")
          record = models.TimeseriesRecord.objects.first()
          self.assertAlmostEqual(record.value, 3.14159)
          self.assertEqual(str(record), "2017-11-23 17:23,3.1,")
  
      def test_str_when_no_value(self):
          self._create_test_timeseries("2017-11-23 17:23,,\n")
          record = models.TimeseriesRecord.objects.first()
          record.save()
          self.assertEqual(str(record), "2017-11-23 17:23,,")
  
  
  class TimeseriesRecordBulkInsertTestCase(TestCase, TestTimeseriesMixin):
      @classmethod
      def setUpTestData(cls):
          cls._create_test_timeseries()
          ahtimeseries = HTimeseries(
              StringIO("2020-09-08 20:00,15.7,,\n2020-09-08 21:00,,\n")
          )
          models.TimeseriesRecord.bulk_insert(cls.timeseries, ahtimeseries)
          cls.timeseries_records = models.TimeseriesRecord.objects.all()
  
      def test_first_value(self):
          self.assertAlmostEqual(self.timeseries_records[0].value, 15.7)
  
      def test_empty_value(self):
          self.assertIsNone(self.timeseries_records[1].value)
  
  
  class TimeseriesDatesCacheInvalidationTestCase(TestCase):
      def setUp(self):
          self.station = mommy.make(models.Station, name="Celduin")
          self.timeseries_group = mommy.make(models.TimeseriesGroup, gentity=self.station)
          self.timeseries = mommy.make(
              models.Timeseries,
              timeseries_group=self.timeseries_group,
              type=models.Timeseries.INITIAL,
          )
  
      def test_station_last_update_cache_invalidation(self):
          with self.assertNumQueries(2):
              self.station.last_update
  
          # Check cache invalidation
          self.timeseries.save()
          with self.assertNumQueries(2):
              self.station.last_update
  
      def test_station_last_update_naive_cache_invalidation(self):
          with self.assertNumQueries(2):
              self.station.last_update_naive
  
          # Check cache invalidation
          self.timeseries.save()
          with self.assertNumQueries(2):
              self.station.last_update_naive
  
      def test_timeseries_group_start_date_cache_invalidation(self):
          self.timeseries_group.default_timeseries
          with self.assertNumQueries(1):
              self.timeseries_group.start_date
  
          # Check cache invalidation
          self.timeseries.save()
          self.timeseries_group.default_timeseries
          with self.assertNumQueries(1):
              self.timeseries_group.start_date
  
      def test_timeseries_group_start_date_naive_cache_invalidation(self):
          # Make sure to retrieve the `default_timeseries` first.
          self.timeseries_group.default_timeseries
  
          with self.assertNumQueries(1):
              self.timeseries_group.start_date_naive
  
          # Check cache invalidation
          self.timeseries.save()
          self.timeseries_group.default_timeseries
          with self.assertNumQueries(1):
              self.timeseries_group.start_date_naive
  
      def test_timeseries_group_end_date_cache_invalidation(self):
          # Make sure to retrieve the `default_timeseries` first.
          self.timeseries_group.default_timeseries
  
          with self.assertNumQueries(1):
              self.timeseries_group.end_date
  
          # Check cache invalidation
          self.timeseries.save()
          self.timeseries_group.default_timeseries
          with self.assertNumQueries(1):
              self.timeseries_group.end_date
  
      def test_timeseries_group_end_date_naive_cache_invalidation(self):
          # Make sure to retrieve the `default_timeseries` first.
          self.timeseries_group.default_timeseries
  
          with self.assertNumQueries(1):
              self.timeseries_group.end_date_naive
  
          # Check cache invalidation
          self.timeseries.save()
          self.timeseries_group.default_timeseries
          with self.assertNumQueries(1):
              self.timeseries_group.end_date_naive
  
      def test_timeseries_start_date_cache_invalidation(self):
          with self.assertNumQueries(1):
              self.timeseries.start_date
  
          # Check cache invalidation
          self.timeseries.save()
          with self.assertNumQueries(1):
              self.timeseries.start_date
  
      def test_timeseries_start_date_naive_cache_invalidation(self):
          with self.assertNumQueries(1):
              self.timeseries.start_date_naive
  
          # Check cache invalidation
          self.timeseries.save()
          with self.assertNumQueries(1):
              self.timeseries.start_date_naive
  
      def test_timeseries_end_date_cache_invalidation(self):
          with self.assertNumQueries(1):
              self.timeseries.end_date
  
          # Check cache invalidation
          self.timeseries.save()
          with self.assertNumQueries(1):
              self.timeseries.end_date
  
      def test_timeseries_end_date_naive_cache_invalidation(self):
          with self.assertNumQueries(1):
              self.timeseries.end_date_naive
  
          # Check cache invalidation
          self.timeseries.save()
          with self.assertNumQueries(1):
              self.timeseries.end_date_naive
  